1. ホーム
  2. git

[解決済み] コミットをつぶしているだけなのに、なぜ git-rebase はマージの衝突を起こすのですか?

2022-04-15 04:32:49

質問

私たちのGitリポジトリには400以上のコミットがあり、そのうち最初の数十回は多くの試行錯誤がありました。これらのコミットを整理して、多くのコミットをひとつのコミットにまとめたいと考えています。当然ながら、git-rebaseが最適な方法だと思われます。私の問題は、マージコンフリクトが発生してしまうことで、このコンフリクトを解決するのは簡単ではありません。私はただコミットを潰しているだけなので(削除や再配置をしているわけではありません)、なぜコンフリクトが発生するのかが理解できません。おそらく、これは私がgit-rebaseがどのようにsquashを行うかを完全に理解していないことを示しています。

以下は、私が使っているスクリプトの修正版です。


repo_squash.sh (実際に実行されるスクリプトです)。


rm -rf repo_squash
git clone repo repo_squash
cd repo_squash/
GIT_EDITOR=../repo_squash_helper.sh git rebase --strategy theirs -i bd6a09a484b8230d0810e6689cf08a24f26f287a


repo_squash_helper.sh (このスクリプトは repo_squash.sh によってのみ使用されます):


if grep -q "pick " $1
then
#  cp $1 ../repo_squash_history.txt
#  emacs -nw $1
  sed -f ../repo_squash_list.txt < $1 > $1.tmp
  mv $1.tmp $1
else
  if grep -q "initial import" $1
  then
    cp ../repo_squash_new_message1.txt $1
  elif grep -q "fixing bad import" $1
  then
    cp ../repo_squash_new_message2.txt $1
  else
    emacs -nw $1
  fi
fi


repo_squash_list.txt です。(このファイルは repo_squash_helper.sh によってのみ使用されます)


# Initial import
s/pick \(251a190\)/squash \1/g
# Leaving "Needed subdir" for now
# Fixing bad import
s/pick \(46c41d1\)/squash \1/g
s/pick \(5d7agf2\)/squash \1/g
s/pick \(3da63ed\)/squash \1/g


new message"の中身はご想像にお任せします。当初、"--strategy theirs"オプションなしで(つまり、デフォルトの戦略を使い、ドキュメントを正しく理解すれば再帰的ですが、どの再帰的戦略が使われるかはわかりません)やってみたところ、これもうまくいきませんでした。また、repo_squash_helper.sh のコメントアウトされたコードを使用して、sed スクリプトが動作する元のファイルを保存し、それに対して sed スクリプトを実行して、それが私の望むことを行っているかどうかを確認しました(それは行われました)。繰り返しになりますが、なぜそこに だから、どちらの戦略を使うかはそれほど重要ではないように思います。何かアドバイスや洞察があれば助かりますが、ほとんど私はこのスクワッシュを動作させたいだけです。

Jefromi氏との議論から得た追加情報を更新しました。

私たちの巨大な "real" リポジトリで作業する前に、私はテストリポジトリで同様のスクリプトを使用しました。それは非常にシンプルなリポジトリで、テストはきれいに動作しました。

失敗した時に出るメッセージは

Finished one cherry-pick.
# Not currently on any branch.
nothing to commit (working directory clean)
Could not apply 66c45e2... Needed subdir

これは、最初のスクショコミット後の最初のピックです。実行中 git status は、クリーンな作業ディレクトリを生成します。もし私が git rebase --continue さらに数回コミットすると、非常に似たようなメッセージが表示されます。もう一度同じことをすると、数十回のコミットの後、またよく似たメッセージが表示されます。さらにもう一度やると、今度は約百のコミットを経て、このメッセージが表示されます。

Automatic cherry-pick failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>', and
run 'git rebase --continue'
Could not apply f1de3bc... Incremental

を実行すると git status となります。

# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   repo/file_A.cpp
# modified:   repo/file_B.cpp
#
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified:      repo/file_X.cpp
#
# Changed but not updated:
#   (use "git add/rm <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
# deleted:    repo/file_Z.imp

これはピックした結果なので、"both modified"の部分が変に聞こえますね。また、quot;conflict" を見ると、1つの行が[tab]文字で始まるバージョンと、4つのスペースで始まるバージョンがあることに注目すべきです。これは、私の設定ファイルの設定方法に問題があるように思えましたが、そのようなことは何もありませんでした。(core.ignorecaseがtrueに設定されていることに気づきましたが、明らかにgit-cloneが自動的にそうしていたようです。元のソースがWindowsマシン上にあったことを考えると、このことに完全に驚いているわけではありません)。

file_X.cpp を手動で修正すると、その直後に別の衝突が発生して失敗します。今回は、あるバージョンでは存在すべきと考え、あるバージョンでは存在すべきでないと考えるファイル (CMakeLists.txt) の間で衝突が発生しました。もし私がこのファイルを必要だと言ってこの衝突を修正したら(私はそうします)、数回コミットした後に、今度は些細な変更ではない別の衝突(この同じファイル)が起こりました。まだコンフリクトの25%くらいしか起きていません。

また、これは非常に重要なことかもしれませんが、このプロジェクトはsvnリポジトリで始まったということも指摘しておかなければなりません。この最初の履歴は、おそらくその svn リポジトリからインポートされたものでしょう。

アップデートその2。

ふと思い立って(Jefromiさんのコメントに影響されて)、私のrepo_squash.shをこう変えることにしました。

rm -rf repo_squash
git clone repo repo_squash
cd repo_squash/
git rebase --strategy theirs -i bd6a09a484b8230d0810e6689cf08a24f26f287a

そして、元のエントリーをそのまま受け入れました。つまり、"rebase" は何も変えていないはずなのです。結局、以前記述したのと同じ結果になりました。

更新その3。

あるいは、strategyを省略して、最後のコマンドを置き換えると。

git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a

私はもう "nothing to commit" リベースの問題は発生しませんが、他のコンフリクトはまだ残っています。

問題を再現したトイリポジトリで更新します。

test_squash.sh(これは実際に実行するファイルです)。

#========================================================
# Initialize directories
#========================================================
rm -rf test_squash/ test_squash_clone/
mkdir -p test_squash
mkdir -p test_squash_clone
#========================================================

#========================================================
# Create repository with history
#========================================================
cd test_squash/
git init
echo "README">README
git add README
git commit -m"Initial commit: can't easily access for rebasing"
echo "Line 1">test_file.txt
git add test_file.txt
git commit -m"Created single line file"
echo "Line 2">>test_file.txt 
git add test_file.txt 
git commit -m"Meant for it to be two lines"
git checkout -b dev
echo Meaningful code>new_file.txt
git add new_file.txt 
git commit -m"Meaningful commit"
git checkout master
echo Conflicting meaningful code>new_file.txt
git add new_file.txt 
git commit -m"Conflicting meaningful commit"
# This will conflict
git merge dev
# Fixes conflict
echo Merged meaningful code>new_file.txt
git add new_file.txt
git commit -m"Merged dev with master"
cd ..

#========================================================
# Save off a clone of the repository prior to squashing
#========================================================
git clone test_squash test_squash_clone
#========================================================

#========================================================
# Do the squash
#========================================================
cd test_squash
GIT_EDITOR=../test_squash_helper.sh git rebase -i HEAD@{7}
#========================================================

#========================================================
# Show the results
#========================================================
git log
git gc
git reflog
#========================================================

test_squash_helper.sh (test_sqash.shが使用します):

# If the file has the phrase "pick " in it, assume it's the log file
if grep -q "pick " $1
then
  sed -e "s/pick \(.*\) \(Meant for it to be two lines\)/squash \1 \2/g" < $1 > $1.tmp
  mv $1.tmp $1
# Else, assume it's the commit message file
else
# Use our pre-canned message
  echo "Created two line file" > $1
fi

追伸:そう、私がemacsをフォールバック・エディターとして使っているのを見ると、ぞっとする人がいるのは知っています。

P.P.S.: リベースの後、既存のリポジトリのクローンをすべて破棄しなければならないことは承知しています。("thou shalt not rebase a repository after it's published" にならって。)

P.P.P.S: 誰かこれに懸賞金をつける方法を教えてください。編集モードでも表示モードでも、この画面のどこにもオプションが表示されないのです。

解決方法

よし、答えを出す自信はある。もしかしたら編集しなければならないかもしれませんが、あなたの問題が何であるかは分かっていると思います。

あなたのトイリポのテストケースにはマージがあり、さらに悪いことに、コンフリクトを伴うマージがあります。そして、そのマージをまたいでリベースしています。それがなければ -p (とは全く相性が悪いのですが)。 -i ) の場合、マージは無視されます。これは、競合の解決で何をしたにせよ は存在しない リベースが次のコミットをチェリーピックしようとしたときに、そのパッチが適用されない可能性があります。(これはマージコンフリクトとして表示されると思います。 git cherry-pick は、元のコミット、現在のコミット、共通の祖先の間で3方向マージを行うことでパッチを適用することができます)。

残念ながら、コメントで指摘したように -i-p (プリザーブド・マージ)はあまり仲が良くない。編集・書き直しがうまくいって、並べ替えがうまくいかないことは知っています。しかし、私は 信じる スクワッシュで問題なく動作すること。これは文書化されていませんが、以下に説明するテストケースではうまくいきました。もしあなたのケースがもっとずっと複雑なものであれば、あなたの望むことをするのは大変かもしれませんが、それでも可能でしょう。(この話の教訓は rebase -i 前に をマージします)。

では、A,B,Cを一緒につぶすという、とてもシンプルなケースを考えてみましょう。

- o - A - B - C - X - D - E - F (master)
   \             /
    Z -----------

今、言ったように、Xにコンフリクトがなかったとしたら。 git rebase -i -p は期待通りに動作します。

コンフリクトが発生すると、少し厄介なことになります。うまくつぶすことができても、マージを再作成しようとすると、またコンフリクトが発生します。もう一度解決してインデックスに追加し、それから git rebase --continue を使用して次に進みます。(もちろん、元のマージコミットのバージョンをチェックアウトすることで、再度解決することができます)。

もし rerere を有効にして、レポで ( rerere.enabled を true に設定)、これは非常に簡単です - git が を使用します。 コード化 の解決策は、もともとコンフリクトがあったときのもので、あとはそれが正しく動作しているかどうかを検査し、ファイルをインデックスに追加して、続行するだけでいいのです。(さらにもう一歩進んで rerere.autoupdate そうすれば、マージに失敗することもないでしょう。) しかし、あなたはrerereを有効にしていないようなので、競合の解決は自分でやらなければならないでしょう。

<サブ * または rerere-train.sh スクリプトは git-contrib にあり、既存のマージコミットから "Prime [中略] rerere database" を試みます。基本的には、すべてのマージコミットをチェックしてマージを試み、マージに失敗したら結果を取得して git-rerere . これは時間がかかるかもしれないし、実際に使ったこともないのですが、とても便利かもしれません。