【Git submodule】チームで運用する際の不安と対策(pre-commit編)
Git submoduleは複数のコードから同じリポジトリを指すことができて便利ですが、通常のGitより操作することが多くなり、ちょっと難しくなります。
操作が多くなるということはヒューマンエラーの可能性も高くなります。ちょうどチーム開発でsubmoduleを使うことになったので、運用していくにあたって気付いた問題と対策をまとめようと思います。
その1:Git submoduleについて
別のリポジトリのコミットへの参照をGitに含める機能です。内容や操作方法については別の記事を参考にしてください。
その2:親子を並行して開発したい
並行して開発する場合、それぞれで扱わずにsubmoduleの状態で操作することが多いと思います。すると、親Gitの方では差分として検出され、add+commitを要求されます。
# 例(子Gitでcommit前)
modified: src/database/migrations (modified content)
# 例(子Gitでcommit後)
modified: src/database/migrations (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
子Gitでcommitしていれば、push前であっても親Gitでadd+commit+pushすることが可能です。
するとどうなるかというと...
他の人から見えない
問題点
- 子がpushする前のコミットを親が参照している場合でも親Gitでcommitできてしまう
対処
こういう時はpre-commitで対応しましょう。 git log origin/{branch name}..HEAD
だと、ブランチの先頭以外にいる時にうまく動かないので、 git branch -r --contains $commit
を使います。繋がっているリモートブランチがあればブランチ名が返ってくるので、空行であればpush前とします。
#!/usr/bin/env bash
# 区切り文字を改行のみに変更
IFS=$'\n'
submodules=$(git submodule)
currentPath=$(pwd)
exitCode=0
for line in $submodules
do
commit=$(echo $line | awk '{ print $1 }' | sed -e 's/^[-+ ]//g')
submodulePath=$(echo $line | awk '{ print $2 }')
# git submoduleの結果がマイナスから始まる場合はcloneできていないのでチェック不可として落とす
if echo $line | awk '{ print $1 }' | grep -E '^-'; then
echo "git submodule update --init --recursive を実行してください"
exitCode=$(($exitCode | 1))
else
cd $submodulePath
# 未pushのcommitを参照することをブロック
remoteBranches=$(git branch -r --contains $commit)
if [ "$remoteBranches" = "" ]; then
echo "$submodulePath の変更がリモートにプッシュされていません"
exitCode=$(($exitCode | 1))
fi
cd $currentPath
fi
done
exit $exitCode
検証
🎐 ❯ git status
On branch workspace/submodule
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: src/database/migrations (new commits)
no changes added to commit (use "git add" and/or "git commit -a")
🎐 ❯ git add .
🎐 ❯ git commit -m "test"
src/database/migrations の変更がリモートにプッシュされていません
その3:参照できるブランチを固定したい
通常の開発フローだと、トピックブランチを切って開発することが多いと思います。親も子も新規ブランチを作成→開発→pushしてPRを出して......と進むと1つ問題が発生します。トピックブランチを指したsubmoduleが生まれてしまいます
子側でマージされれば小問題ですみますが、もしマージされずに放置されてしまった場合......
問題点
- 親:デフォルトブランチ、子:トピックブランチという状態になる
- 子Gitが指しているコミットはトピックブランチのものなので、そのまま新規ブランチを切ると、トピックブランチから別のトピックブランチが発生する
- 子がマージされていない場合、子をデフォルトブランチに戻すとデグレが発生する
対処
参照できるブランチを固定しましょう。これも2と同じでpre-commitが手軽ですね。 git branch --contains $commit
を使うとブランチ名を取ることができます。
取り敢えずデフォルトのmainを決め打ちしてますが、今後進めていくにあたって気づきがあれば追記します。
git-flowだとreleaseはreleaseを参照したいとかになりそうな予感がありますな
#!/usr/bin/env bash
# 区切り文字を改行のみに変更
IFS=$'\n'
submodules=$(git submodule)
currentPath=$(pwd)
exitCode=0
for line in $submodules
do
commit=$(echo $line | awk '{ print $1 }' | sed -e 's/^[-+ ]//g')
submodulePath=$(echo $line | awk '{ print $2 }')
# git submoduleの結果がマイナスから始まる場合はcloneできていないのでチェック不可として落とす
if echo $line | awk '{ print $1 }' | grep -E '^-'; then
echo "git submodule update --init --recursive を実行してください"
exitCode=$(($exitCode | 1))
else
cd $submodulePath
# ---------------------- 追記ここから -----------------------------
# main以外を参照することをブロック
currentExitCode=$(git branch --contains $commit | sed -e 's|[\* ]*||' | grep -E ^main$ > /dev/null; echo $?)
exitCode=$(($exitCode | $currentExitCode))
if [ $((currentExitCode)) -ne 0 ]; then
echo "$submodulePath がmainブランチに存在しないコミットを参照しています(${commit:0:7})"
fi
# ---------------------- 追記ここまで -----------------------------
# 未pushのcommitを参照することをブロック
remoteBranches=$(git branch -r --contains $commit)
if [ "$remoteBranches" = "" ]; then
echo "$submodulePath の変更がリモートにプッシュされていません"
exitCode=$(($exitCode | 1))
fi
cd $currentPath
fi
done
exit $exitCode
検証
🎐 ❯ git submodule
f6e3657a8fa1246de4ed03fdefe273fb11523d3d submodule-test-child (heads/feat/new-2)
🎐 ❯ git commit -m "feat: new-2"
submodule-test-child がmainブランチに存在しないコミットを参照しています(f6e3657)
その4:デグレを防ぎたい
ここまでで十分かというと、まだあります。
後から発生したPRが先にマージされるなんてこと、あると思います。GitHubのPR上ではしっかりコンフリクトになるので、気をつけていれば何も起こりません。
ですがコンフリクト修正の結果が巻き戻っていたとしても、レビュアーは気付けないと思います。
???「8027e7とce564eってどっちが新しいんだ?」
問題
- 意図せず昔のコミットに戻ってしまう
対処
こちらの記事ではGHAを使って対処しています。
なので、今回はpre-commitでやってみます。
🎐 ❯ git diff --cached --submodule=log
Submodule submodule-test-child ce564ea..8027e79 (rewind):
< Merge pull request #1 from sun-yryr/hogehoge
--submodule=log
オプションを使うと、短い形で変更を表示してくれるので、こちらを使います。rewind(= 巻き戻し)があるのでこれをgrepしてもいいんですが、丁度コミットハッシュが2つあるので git log
を使ってみましょう。
git log 新しいコミット..古いコミット
をすると空行になることを利用します。
🎐 ❯ git log ce564ea..8027e79
🎐 ❯ git log 8027e79..ce564ea
commit ce564ea23089d971408ba5f184cd41a3ab4a38af
Merge: 8027e79 5e79825
Author: ゆるゆる <develop@sun-yryr.com>
#!/usr/bin/env bash
# 区切り文字を改行のみに変更
IFS=$'\n'
submodules=$(git submodule)
currentPath=$(pwd)
exitCode=0
for line in $submodules
do
commit=$(echo $line | awk '{ print $1 }' | sed -e 's/^[-+ ]//g')
submodulePath=$(echo $line | awk '{ print $2 }')
# ---------------------- 追記ここから -----------------------------
submoduleCommitDiff=$(\
git diff --cached --unified=0 --no-color --submodule=log -- $submodulePath \
| head -n 1 \
| sed -r 's/^.*(.{8}\.\..{8}).*$/\1/g' \
| tr -d ' '\
)
# ---------------------- 追記ここまで -----------------------------
# git submoduleの結果がマイナスから始まる場合はcloneできていないのでチェック不可として落とす
if echo $line | awk '{ print $1 }' | grep -E '^-'; then
echo "git submodule update --init --recursive を実行してください"
exitCode=$(($exitCode | 1))
else
cd $submodulePath
# main以外を参照することをブロック
currentExitCode=$(git branch --contains $commit | sed -e 's|[\* ]*||' | grep -E ^main$ > /dev/null; echo $?)
exitCode=$(($exitCode | $currentExitCode))
if [ $((currentExitCode)) -ne 0 ]; then
echo "$submodulePath がmainブランチに存在しないコミットを参照しています(${commit:0:7})"
fi
# 未pushのcommitを参照することをブロック
remoteBranches=$(git branch -r --contains $commit)
if [ "$remoteBranches" = "" ]; then
echo "$submodulePath の変更がリモートにプッシュされていません"
exitCode=$(($exitCode | 1))
fi
# ---------------------- 追記ここから -----------------------------
# 過去に戻るのをブロック
if [ "$submoduleCommitDiff" != "" ]; then
# git logが存在しない = 時系列が逆 = 時間逆行
log=$(git log $submoduleCommitDiff)
if [ "$log" = "" ]; then
echo "$submodulePath が過去に戻っています"
exitCode=$(($exitCode | 1))
fi
fi
# ---------------------- 追記ここまで -----------------------------
cd $currentPath
fi
done
exit $exitCode
検証
🎐 ❯ git commit -m "test"
submodule-test-child が過去に戻っています
まとめ
全部pre-commitでやってるので、誰かが --no-verify
したらおしまいなんですよね〜 基本pre-commit、追加でGHAもやるのが一番いいと思います。
ここまでチェックを自動化した上でも、リリースとかまだまだ考えないといけないことがありそうなので、正直使わないでいけるのであれば使わない選択肢を取りたいですね。
参考になる↓
Discussion