バージョン管理

自分のノートと『Google のソフトウェアエンジニアリング』第16章を1本にまとめたもの。 書籍由来の主張は要約して (書籍16章) のように出典を付けた。本文の引用はしていない。

モノリポの論文 PDF が同ディレクトリの google-monorepo.pdf にある (CACM 2016, Potvin & Levenberg。公開されている論文)。

何を解いている道具なのか (書籍16.1)

VCS は、複数人が同じファイル群に同時に作業できるようにして活動を協調させる道具。 中核にあるのは、「どちらがより最近のものか」という問いを消すこと。 これが曖昧になった瞬間に、作業の上書き合いという厄介な問題が発生する。

得られるもの。

取り消せる
複数の開発者間、あるいは1人の別々の時点間で整合性が取れる
どの変更セットが適用済みかの追跡という、間違いを起こしやすい操作が自動化される
全行への全変更の正式な記録が残る(監査要件を満たす手段にもなる)
コード全行の起源と発祥を追跡できる

単一バージョン原則 (書籍16.3)

この章で最も重要な原則。リポジトリ内の各依存関係について、選ぶべきバージョンは1つだけ。

言い換えると、開発者に「このコンポーネントのどのバージョンに依存するか」という選択の余地を与えない。 選択の余地を残すと、マージ戦略の議論、ダイアモンド依存、無駄な作業、労力の浪費に直結する。

自分のノートでの表現も同じことを言っている。

リポジトリは1つにする
各依存関係(アプリ、API、各種ツール)は常に最新のバージョンを向いているようにする

モノリポ (書籍16.4)

モノリポの最大の利点は、単一バージョンルールを守るのが自明なほど簡単になること。 どのバージョンが正式かを決めるプロセスも、どのリポジトリが重要かを探すプロセスも存在しなくなる。

副次的に、他の全員がやっていることが見えるようになり、 自分の設計上の選択を他者に伝える手段にもなる。

ただし書籍の結論はモノリポ推しではない。 重要なのはモノリポかどうかではなく、単一バージョン原則をできるだけ守ること。

判断基準として書かれているのは、要件が揃っているかどうか。 組織内の全プロジェクトに同じ機密・法律・プライバシー・セキュリティの要件があるなら真のモノリポでよい。 そうでないなら、モノリポの機能性は目指しつつ、実装方式は変えてよい。

技術的な制約が本丸

モノリポへの反対論のほとんどは、思想ではなく単一の巨大リポジトリの技術的制約を指している。

  • Git は数百万コミット後にパフォーマンス問題が出やすく、大きなバイナリを含むとクローンが遅くなる
  • クローンが速く安いほど、開発者は変更を小さく分離した状態に保つ (間違った作業ブランチにコミットする事故も減る)
  • 逆にクローンに何時間もかかるなら、組織がその依存を敬遠するのは当然

Git 側の主要な改善はこの方向に向いている。shallow clone(最新コミットだけ取る)、 sparse checkout(全体ではなく一部だけローカルに展開)など。

社内であれば、コンピュートリソースの可用性・協調・ある程度の中央集権的な権力を前提にできる。 細粒度リポジトリを連合させたバーチャルモノリポなら、 単一バージョンルールを守りつつ、実験的・極秘のプロジェクトを分離できる。

ブランチ戦略

トランクベース開発(リポジトリ1個、開発ブランチなし)が最もスケールする。 (書籍16章)

DORA も、トランクベース開発とパフォーマンスの高いソフトウェア組織の間に予測可能性があると指摘している。 大規模変更を行う場合ほど、トランクベースの重要性が増す。

自分のノートでの運用基準。

開発・機能・リリース用ブランチは作成しない

差分 or ブランチの生存期間は数時間以内にする
トランクが更新されてから各開発者のトランクが最新になるまでの時間は数時間以内にする
コンフリクトを発生させない
マージと再テストをさせない
レビュー負担、レビューミスを減らす

コードロック or コードフリーズ期間を作らない

コミット前には各種チェックが動作(数分以内)して、問題ないときだけコミットすることができる
ビルドとテストプロセスは数分以内

開発ブランチをなぜ使わないか (書籍16.2.2)

開発ブランチが解こうとしている問題(製品の不安定さ)自体は真っ当な問題。 ただしその解き方が見当違い、というのが書籍の立場。

同じ問題は、テスト・CI・徹底したコードレビューの3つを広く使う方がはるかにうまく解ける。

理由は単純で、小さなマージは大きなマージより容易だから。 長期間分離されたブランチが複数あると、マージ操作の協調コストがトランクベースより著しく高くなる。 大きなマージのたびに再同期と再テストが必要になる。

中毒の構造も書かれている。チームが他の開発中ブランチを土台にして新しいブランチを切り始める。 そのブランチが主要開発ブランチと定期的に同期されているとは限らない。 唯一の「信頼できる情報源」が存在しなくなる。

未完成のものをどうトランクに置くか (書籍16.3.4)

作業はトランクに対する小さくインクリメンタルな増分として定期的にコミットする。 準備できていない機能の扱い方は2つ。

  • 実行時に無効化した状態でコミットし、可能なら可視性ポリシーで他の開発者から隠す
  • 新旧2つの選択肢が同じプログラムにリンクされても共存できるように設計する

Google のモノリポで作業する約1,000チームのうち、開発ブランチを持つのは2〜3チームだけ。 しかもそれは、単一バージョンでは足りず 古いクライアントが新しいサーバーで動くことを約束しなければならないような特殊事情のため。

書籍はここで一般則を出している。 時間をまたぐ依存関係は、どんな形式であれ、時間が経っても不変なコードよりはるかに高コストで複雑。 だから本番サービスがその種の約束をする範囲は狭く保ち、 本番ジョブは最長でも6ヶ月ごとに再ビルド・再デプロイする。

リリースブランチ (書籍16.2.3, 16.3.5)

判断はリリース間隔で決まる。

状況判断
リリース間隔が数時間より長いリリースビルドに入ったコードを正確に表すリリースブランチを作るのは筋が通る。致命的な欠陥はトランクからチェリーピックする
1日に何度もトランクからリリースできる(継続的デプロイ)リリースブランチは要らない。 直して再デプロイする方が簡単で、チェリーピックとブランチは余計なオーバーヘッド

書籍の観察では、最もうまく回っている技術組織にリリースブランチはほぼ存在しない。 一方で、月次リリースをしながら次のリリースに向けて作業を進めるなら、リリースブランチは完璧に筋が通る。 書籍も、リリースブランチによる広範囲のコストは経験上なかったと書いている。

いずれにせよ、複数ブランチの利用は生産性の足かせになるという前提は共通。

SSH

SSH キーの作成

  • email は仕事用とプライベート用は分ける
ssh-keygen -t ed25519 -C "${email}"

.ssh/config

Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519

SSH キーを ssh-agent に追加

eval "$(ssh-agent -s)"
ssh-add -K ~/.ssh/id_ed25519

SSH キーの GitHub への追加

New SSH Key

Title: ${host}/${user}/.ssh/id_ed25519.pub
Key: cat ~/.ssh/id_ed25519.pubの内容

~/.gitconfig

[user]
        name =
        email =
[color]
        ui = true
[alias]
        st = status
        co = checkout
        br = branch
        diffs = diff --staged
        rmc = rm --cached
[merge]
        ff = only
        tool = sublime
[pull]
        rebase = true
[init]
        defaultBranch = main
[url "git@github.com:"]
        insteadOf = https://github.com/
[mergetool "sublime"]
        cmd = subl -n -w $MERGED
[core]
        editor = subl -n -w
git config --global user.name ""
git config --global user.email ""
 
git config --global color.ui true
 
sudo ln -s /Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl /usr/local/bin/subl
git config --global core.editor 'subl -n -w'
 
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.diffs 'diff --staged'
git config --global alias.rmc "rm --cached"
 
git config --global pull.rebase true
 
git config --global merge.ff only
git config --global merge.tool sublime
git config --global mergetool.sublime.cmd "subl -n -w \$MERGED"
 
git config --global init.defaultBranch main

日々の操作

クローン

git clone git@github.com:${user}/${repo}.git

作業ブランチ作成

git branch -r
git co -b develop|main origin/develop|main
git co -b ${branch} develop|main

確認

git st
git diff
git diffs

コミット

git add .
git commit -m "コミットメッセージ" | git commit -v
  • revertすることを考えて細かく、複数の変更を1コミットに混ぜない

差分の一時退避・復元

git stash
git stash pop

プッシュ前

git co develop|main
git pull -p
git co ${branch}
git rebase|merge develop|main

プッシュ

git push origin ${branch}

掃除

git gc
git gc --aggressive

コミット履歴

  • コミット履歴を見れば、過去にどういう案件でどういう変更があったか分かるようにする
  • 適宜Squashして見やすくしてからプルリクエストを作成する

GitHub のプルリクのマージ時

  • 差分をApproveする
  • Squash mergeにする
  • コミットメッセージはすべて消し、プルリクのタイトル(案件・修正名と該当のチケットのURL)だけにする

関連