エージェントに更新作業を任せた翌朝、手元で git pull --rebase origin main を打つと、次の二行で止まりました。
error: cannot pull with rebase: You have unstaged changes.
error: please commit or stash them.個人開発でアプリの更新を回していると、このメッセージには何度も出会います。前日のセッションが途中で終わり、作業ツリーに未コミットの変更が残っているのです。
最初にお伝えしたいのは、このエラーが「壊れた」ではなく「守ってくれた」という合図だという点です。ただ、守りを外すために私が最初に覚えた近道には、小さな落とし穴がありました。手元の git 2.34.1 で再現して確かめた内容を、順番に書き残します。
まず「何が残っているか」を見る
取り込みの前に確かめることは一つだけです。
git status --short --untracked-files=no追跡済みファイルの変更だけを出します。 M notes.md のように出れば、それが止めている犯人です。未追跡ファイルは pull --rebase を止めないので、ここでは数えません。
見たあとの分かれ道は三つあります。
| 残っていたもの | 最初の一手 | 理由 |
|---|---|---|
| エージェントの作業の続きで、残したい変更 | 先にコミットする | 履歴に残るので、あとで取りこぼしません |
| 試しに触っただけの変更 | git restore で戻す | 取り込む対象を最新の状態だけにできます |
| 判断がつかない変更 | git stash で退避する | 消さずに、取り込みだけ先に進められます |
私は三つ目の「判断がつかない」ときに、もっとも手が止まります。そこで登場するのが --autostash です。
--autostash は便利で、少しだけ危うい
--autostash を付けると、git が退避から取り込み、復元までを一度にやってくれます。
git pull --rebase --autostash origin mainリモートと重ならないファイルだけが汚れていた場合は、次のように何事もなく終わりました。
Created autostash: cc3d9ae
Fast-forward
config.txt | 1 +
Applied autostash.問題は、リモートで変わったファイルと、手元で残っていた変更が同じファイルに重なったときです。同じ検証用リポジトリで、notes.md の同じ場所をリモートと手元の両方で変えて試しました。
Created autostash: cc3d9ae
Fast-forward
notes.md | 1 +
Applying autostash resulted in conflicts.
Your changes are safe in the stash.
You can run "git stash pop" or "git stash drop" at any time.この直後に echo $? を打つと、0 が返ってきました。
そして notes.md の中身には、衝突マーカーが入っています。
n
<<<<<<< Updated upstream
remote note
=======
agent leftover
>>>>>>> Stashed changes取り込みそのものは成功しているので、終了コードは 0 です。衝突したのは「退避した変更を戻す」段階だった、というわけです。変更が失われたわけではなく、stash@{0} にも残っています。ただ、スクリプトやエージェントに && でつなげてビルドや push を任せていると、衝突マーカー入りのファイルがそのまま先へ進んでしまいます。
予想と逆だったのはここでした。いま思えば、終了コードを疑う癖がなかったのです。「autostash を付ければ安全」と思い込んでいた私は、終了コードを信じて次の工程を走らせる書き方をしていたのです。
失敗の形を二つに分ける
ここまでを踏まえて、私は取り込みを二つの書き方に分けています。
一つ目は、汚れていたら止める書き方です。無人で回す手順には、こちらを標準にしています。
#!/bin/bash
# 取り込み前に「作業ツリーが汚れていないか」を先に見て、汚れていれば理由つきで止める
set -euo pipefail
branch="${1:-main}"
dirty="$(git status --porcelain --untracked-files=no)"
if [ -n "$dirty" ]; then
echo "未コミットの変更が残っています。取り込みを中止します:" >&2
echo "$dirty" >&2
exit 2
fi
git pull --rebase origin "$branch"
echo "sync ok: $(git rev-parse --short HEAD)"汚れた状態で実行すると、次のように終了コード 2 で止まります。
未コミットの変更が残っています。取り込みを中止します:
M notes.md
exit=2「何が残っているのか」がログに出るので、翌朝に原因を探し回らずに済みます。
二つ目は、人が手元で使う、autostash を許す書き方です。戻したあとの衝突を、終了コードとは別に自分で確かめます。
#!/bin/bash
# autostash を使う場合は、戻した後の衝突を自分で確かめる
set -euo pipefail
branch="${1:-main}"
git pull --rebase --autostash origin "$branch"
conflicts="$(git diff --name-only --diff-filter=U)"
if [ -n "$conflicts" ]; then
echo "退避した変更を戻す際に衝突しました(変更は stash に残っています):" >&2
echo "$conflicts" >&2
exit 3
fi
echo "sync ok: $(git rev-parse --short HEAD)"先ほどの衝突ケースで動かすと、notes.md を名指しして終了コード 3 で止まりました。git diff --diff-filter=U は未解決(unmerged)のファイルだけを拾うので、衝突がなければ空になり、そのまま通ります。
なぜ二つに分けるのか。無人の手順は「止まることが安全」で、人が見ている作業は「進めたうえで直せる」ほうが速いからです。無人の手順では汚れを許さずに止め、人が見ている手順では許したうえで戻し口を確かめます。この線引きが、いまの私の答えなのだと思います。
衝突したあとの片づけ
autostash で衝突してしまった場合の片づけも、検証用リポジトリで通しておきました。
git statusでUUと出ているファイルを開き、マーカーのどちらを残すか決めて保存しますgit add <ファイル>で解決済みにしますgit stash dropで、役目を終えた退避を捨てます
三番目を忘れると、git stash list に autostash が残り続けます。私も検証中、前の実験の退避が残ったままで次の実験を始めてしまい、一覧に見覚えのない行が並んで首をかしげました。取り込みの前後に git stash list を一度見る癖をつけておくと、この混乱は防げます。
直らないときの次の一手
ここまでやっても止まるときは、次の二点を疑います。
- リベース途中の状態が残っている:
git statusの先頭にrebase in progressと出ます。git rebase --continueかgit rebase --abortで、先にその途中を終わらせます - 別のブランチにいる:
git branch --show-currentを見ます。エージェントが作業用ブランチに切り替えたまま、元に戻していないことがあります
いずれも、メッセージが「unstaged changes」ではなく別の文言で出るので、読み分けは難しくありません。
今日の持ち帰り
まず git status --short --untracked-files=no を打って、止めているものの正体を見てください。そのうえで、無人で回す手順には上の一つ目のスクリプトを、手元の作業には二つ目を使い分けていただければ幸いです。
「終了コード 0」を、そのまま「きれいに終わった」とは読まないようにしています。この原則だけは、エージェントに手順を任せるようになってから、いっそう強く意識するようになりました。