背景:Gitワークフローが本当に必要になる瞬間
一人でコードを書いていた頃は、mainブランチ一本で「fix」「update」「もう一度」みたいなコミットを繰り返していても誰にも文句を言われなかった。でも3人チームに入った途端、状況は一変した——コードが上書きされ、誰が何を修正したかわからなくなり、デプロイしたら他のメンバーがその日の朝に完成させたばかりの機能が壊れた。
チームの能力が低いわけじゃない。ただ、共通のルールがなかっただけだ。ワークフローがなければ、各自が思い思いのやり方でGitを使う。そして「事故」はいつか必ず起きる。
実際、間違ったブランチへのforce pushで大事なコードを失ったことがある——feature branchではなくmainにpushしてしまい、同僚の2日分の作業を消し飛ばした。それ以来、git push --forceには細心の注意を払い、チームにいくつかのハードルールを設けるようにした。この記事では自分が実践しているやり方を紹介する——メンテナンスコストが低く、チーム作業でトラブルが起きにくいバランスを心がけている。
セットアップ:始める前にGit環境を整える
Git userとエディタの設定
最初の一歩——そして最もよく見落とされるステップ——は、ユーザー情報を正しく設定することだ。これを省略すると、コミットに見知らぬ名前や誤ったメールアドレスが表示され、後から追跡するのが非常に難しくなる:
git config --global user.name "あなたの名前"
git config --global user.email "[email protected]"
git config --global core.editor "nano" # またはvim、code --wait
git config --global init.defaultBranch main
複数のリポジトリや複数のアカウント(個人用+会社用)を使い分けている場合は、混在を防ぐためにglobalではなくlocalのconfigを使おう:
# 会社のリポジトリディレクトリ内で実行 — そのリポジトリにのみ適用される
git config user.email "[email protected]"
日々の操作を省力化するエイリアス
これらのエイリアスは毎日使っていて、かなりの手間が省ける:
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.undo "reset HEAD~1 --mixed"
設定後、git lgでコミット履歴がツリー形式で表示される——現在のブランチがmainからどの位置にあるかを確認するのに非常に便利だ。
最初から正しい.gitignoreでリポジトリを初期化する
git init
cat > .gitignore << EOF
node_modules/
.env
*.log
dist/
__pycache__/
.DS_Store
EOF
git add .gitignore
git commit -m "chore: init project structure"
詳細設定:個人と小規模チームの実践ワークフロー
ブランチ戦略:シンプルだけど十分
5人以下のチームでは、このモデルが最もうまく機能すると感じている——フルのGitFlowほど複雑ではないが、かといって緩すぎることもない:
- main — 本番ブランチ。Pull Request経由でのみコードを受け入れ、直接コミットは禁止
- develop — 統合ブランチ。mainにマージする前にテストを行う
- feature/機能名 — 各新機能の開発ブランチ
- hotfix/バグ名 — mainから切り出す緊急バグ修正ブランチ
個人のサイドプロジェクトなら?main+feature/*だけで十分。一人のときにdevelopは必要ない。
# developからfeatureブランチを作成
git checkout develop
git pull origin develop
git checkout -b feature/add-login-feature
# 作業して、小さな単位でコミットする...
git add src/login.py
git commit -m "feat: add login form with email validation"
git add tests/test_login.py
git commit -m "test: add unit tests for login validation"
# 完了したら、PRを作成するためにリモートへpush
git push origin feature/add-login-feature
コミット規約:チーム全体で統一すべきルール
Conventional Commitsを採用している——シンプルなフォーマットで履歴が読みやすく、changelogの自動生成にも対応できる:
# フォーマット: <type>(<scope>): <description>
git commit -m "feat: add user authentication via JWT"
git commit -m "fix(api): handle null response from payment gateway"
git commit -m "docs: update README with docker setup guide"
git commit -m "refactor: extract email validator to separate module"
git commit -m "chore: upgrade dependencies to latest stable"
よく使うtypeの一覧:
feat— 新機能fix— バグ修正docs— ドキュメントのみの変更chore— 補助的な作業:依存関係の更新、CI設定などrefactor— 機能追加もバグ修正もしないリファクタリングtest— テストの追加または修正
3ヶ月後に履歴を振り返ったとき、どのコミットが何をしたか一目でわかる——各ファイルを開いて推測する必要がない。これがコミット規約の価値をもっとも実感できる点だ。
小規模チームでのPull RequestとCode Review
2人チームでもPRを使うべきだ——お互いを疑っているからではない。単純な理由として、他人のコードをレビューしていると、書いた本人が見落としていたバグを発見することがよくある。長時間同じコードを見続けていると、見えなくなってしまうものがあるからだ。
チームで合意したPRのルール:
- PRには簡潔な説明を記載する:何をしたか、なぜしたか、手動テストの方法
- 各PRは小さく保つ——変更が300行以下が理想的で、レビューがずっと楽になる
mainにマージする前に最低1人のapproveが必要mainやdevelopへのforce pushは絶対禁止
最後の点は、最も辛い形で学んだ教訓だ。mainへのforce pushはチーム全員のコミット履歴を消し去る可能性がある。今では重要なブランチに対してGitHubのBranch Protectionを必ず有効にしている。コミットの改ざんや成りすましを防ぎたい場合は、GPGキーによるコミット署名も合わせて導入するとよい:
# GitHub: Settings → Branches → Add branch protection rule
# Branch name pattern: main
# ✅ Require a pull request before merging
# ✅ Require approvals: 1
# ✅ Do not allow bypassing the above settings
RebaseとMerge:使い分けのポイント
Rebase vs merge——どのチームでもおなじみの議論だ。自分の使い分け方はシンプル:
- rebaseを使うのは、developの最新コードでfeatureブランチを更新するとき——履歴が線形になり読みやすい
- mergeを使うのは、featureをdevelop/mainにマージするとき——コンテキストを保持し、機能全体のロールバックが容易になる
# 最新のdevelopでfeatureブランチを更新(rebase)
git checkout feature/add-login-feature
git fetch origin
git rebase origin/develop
# コンフリクトがあれば解決してから続行
git rebase --continue
# PRがapproveされたらfeatureをdevelopにマージ
git checkout develop
git merge --no-ff feature/add-login-feature -m "merge: add login feature (#42)"
--no-ff(no fast-forward)オプションは個別のマージコミットを作成し、マージ後に問題が発覚した場合に機能全体をrevertしやすくする。rebase中にマージコンフリクトが発生した場合の詳しい対処法は別記事で紹介している。
確認とモニタリング:ワークフローが正しく機能しているか追跡する
グラフ形式で履歴を確認する
git lg
# 出力例:
# * a3f2c1b (HEAD -> feature/login) feat: add form validation
# * 8d9e4f0 feat: add login form UI
# | * c4b1a2e (origin/develop) fix: resolve null pointer in API
# |/
# * 7f3d2c1 (develop) chore: update dependencies
これを見れば、featureブランチがdevelopより1コミット遅れているとすぐわかる——PRを作成する前にrebaseが必要だ。
定期的な統計と監査
リリース前には、いくつかのコマンドで確認作業を行う。バグを引き起こしたコミットを素早く絞り込みたい場合は、git bisect によるバイナリサーチも非常に効果的だ:
# 先週誰が何をコミットしたか確認
git shortlog -sn --since="1 week ago"
# 2バージョン間のdiffを確認
git diff v1.0.0..v1.1.0 --stat
# 特定のコードを追加/削除したコミットを探す
git log -S "function handleLogin" --oneline
# 最も変更頻度が高いファイルを確認
git log --name-only --pretty=format: | sort | uniq -c | sort -rn | head -10
各リリースにタグを付ける
多くのチームがこのステップをスキップする——そして夜11時に緊急ロールバックが必要になって、どのコミットに戻ればいいかわからなくなって初めて後悔する:
# 説明メッセージ付きのannotatedタグを作成
git tag -a v1.2.0 -m "Release v1.2.0 - add login feature, fix payment bug"
git push origin v1.2.0
# タグ一覧を表示
git tag -l
# 緊急時に旧タグへロールバック
git checkout v1.1.0
セルフホストのGitでforce pushからブランチを守る
GiteaやGitLabを自前でホストしている場合、サーバーサイドフックでforce pushをブロックできる:
# ファイル: /path/to/repo.git/hooks/pre-receive
#!/bin/bash
while read oldrev newrev refname; do
if [ "$refname" = "refs/heads/main" ]; then
# force pushかどうかチェック
if git rev-list $newrev..$oldrev | grep -q "."; then
echo "ERROR: Force push to main is not allowed!"
exit 1
fi
fi
done
chmod +x /path/to/repo.git/hooks/pre-receive
まとめ
良いワークフローは複雑である必要はない。個人または5人以下の小規模チームであれば、以下の4点を一貫して守るだけで十分だ:
- 明確なブランチ構造(main / develop / feature)
- 意味のある、規約に沿ったコミットメッセージ
- PRを通じたコードレビュー——たとえ2人でも
- 重要なブランチをforce pushから保護する
コードを一度失って初めて、面倒に思えていたこれらのルールの価値が身にしみた。次の小さなプロジェクトから——最初から正しいワークフローをセットアップすれば、後々の多くの頭痛を防げる。

