当我所做的只是压缩提交时,为什么 git-rebase 会给我带来合并冲突?
我们有一个包含 400 多个提交的 Git 存储库,其中前几十个提交经过了大量的反复试验。我们希望通过将许多提交压缩为单个提交来清理这些提交。当然,git-rebase 似乎是最佳选择。我的问题是它最终会产生合并冲突,而且这些冲突并不容易解决。我不明白为什么应该有任何冲突,因为我只是压缩提交(而不是删除或重新排列)。很可能,这表明我没有完全理解 git-rebase 是如何进行压缩的。
这是我正在使用的脚本的修改版本:
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
我将把“新消息”内容留给您想象。最初,我在没有“--strategy thoses”选项的情况下执行此操作(即使用默认策略,如果我正确理解文档,则该策略是递归的,但我不确定使用哪种递归策略),而且它也没有'不工作。另外,我应该指出,使用 repo_squash_helper.sh 中注释掉的代码,我保存了 sed 脚本所处理的原始文件,并对其运行 sed 脚本,以确保它正在执行我想要它执行的操作(是的)。同样,我什至不知道为什么会发生冲突,因此使用哪种策略似乎并不那么重要。任何建议或见解都会有所帮助,但大多数情况下我只是想让这种挤压发挥作用。
更新了与 Jefromi 讨论中的额外信息:
在使用我们的大型“真实”存储库之前,我在测试存储库上使用了类似的脚本。这是一个非常简单的存储库,测试运行顺利。
当它失败时我得到的消息是:
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
“两者都修改了”位对我来说听起来很奇怪,因为这只是选择的结果。还值得注意的是,如果我查看“冲突”,它可以归结为一行,其中一个版本以 [tab] 字符开头,另一个版本以四个空格开头。这听起来可能是我如何设置配置文件的问题,但其中没有任何此类内容。 (我确实注意到 core.ignorecase 设置为 true,但显然 git-clone 会自动执行此操作。考虑到原始源位于 Windows 计算机上,我对此并不完全感到惊讶。)
如果我手动修复 file_X.cpp,然后不久之后,它会因另一次冲突而失败,这次是一个版本认为应该存在的文件(CMakeLists.txt)与一个版本认为不应该存在的文件(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:
或者,如果我省略该策略并将最后一个命令替换为:
git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a
我不再遇到“无需提交”变基问题,但仍然存在其他冲突。
使用重现问题的玩具存储库进行更新:
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
PS:是的,我知道你们中的一些人在看到我使用 emacs 作为后备编辑器。
PPS:我们确实知道在变基后我们必须清除现有存储库的所有克隆。 (沿着“你不应该在发布后重新设置存储库”的思路。)
PPPS:任何人都可以告诉我如何为此添加赏金吗?无论我处于编辑模式还是查看模式,我都没有在屏幕上的任何位置看到该选项。
We have a Git repository with over 400 commits, the first couple dozen of which were a lot of trial-and-error. We want to clean up these commits by squashing many down into a single commit. Naturally, git-rebase seems the way to go. My problem is that it ends up with merge conflicts, and these conflicts are not easy to resolve. I don't understand why there should be any conflicts at all, since I'm just squashing commits (not deleting or rearranging). Very likely, this demonstrates that I'm not completely understanding how git-rebase does its squashes.
Here's a modified version of the scripts I'm using:
repo_squash.sh (this is the script that is actually run):
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 (this script is used only by 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: (this file is used only by 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
I'll leave the "new message" contents to your imagination. Initially, I did this without the "--strategy theirs" option (i.e., using the default strategy, which if I understand the documentation correctly is recursive, but I'm not sure which recursive strategy is used), and it also didn't work. Also, I should point out that, using the commented out code in repo_squash_helper.sh, I saved off the original file that the sed script works on and ran the sed script against it to make sure it was doing what I wanted it to do (it was). Again, I don't even know why there would be a conflict, so it wouldn't seem to matter so much which strategy is used. Any advice or insight would be helpful, but mostly I just want to get this squashing working.
Updated with extra information from discussion with Jefromi:
Before working on our massive "real" repository, I used similar scripts on a test repository. It was a very simple repository and the test worked cleanly.
The message I get when it fails is:
Finished one cherry-pick.
# Not currently on any branch.
nothing to commit (working directory clean)
Could not apply 66c45e2... Needed subdir
This is the first pick after the first squash commit. Running git status
yields a clean working directory. If I then do a git rebase --continue
, I get a very similar message after a few more commits. If I then do it again, I get another very similar message after a couple dozen commits. If I do it yet again, this time it goes through about a hundred commits, and yields this message:
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
If I then run git status
, I get:
# 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
The "both modified" bit sounds weird to me, since this was just the result of a pick. It's also worth noting that if I look at the "conflict", it boils down to a single line with one version beginning it with a [tab] character, and the other one with four spaces. This sounded like it might be an issue with how I've set up my config file, but there's nothing of the sort in it. (I did note that core.ignorecase is set to true, but evidently git-clone did that automatically. I'm not completely surprised by that considering that the original source was on a Windows machine.)
If I manually fix file_X.cpp, it then fails shortly afterward with another conflict, this time between a file (CMakeLists.txt) that one version thinks should exist and one version thinks shouldn't. If I fix this conflict by saying I do want this file (which I do), a few commits later I get another conflict (in this same file) where now there's some rather non-trivial changes. It's still only about 25% of the way through the conflicts.
I should also point out, since this might be very important, that this project started out in an svn repository. That initial history very likely was imported from that svn repository.
Update #2:
On a lark (influenced by Jefromi's comments), I decided to do the change my repo_squash.sh to be:
rm -rf repo_squash
git clone repo repo_squash
cd repo_squash/
git rebase --strategy theirs -i bd6a09a484b8230d0810e6689cf08a24f26f287a
And then, I just accepted the original entries, as is. I.e., the "rebase" shouldn't have changed a thing. It ended up with the same results describe previously.
Update #3:
Alternatively, if I omit the strategy and replace the last command with:
git rebase -i bd6a09a484b8230d0810e6689cf08a24f26f287a
I no longer get the "nothing to commit" rebase problems, but I'm still left with the other conflicts.
Update with toy repository that recreates problem:
test_squash.sh (this is the file you actually run):
#========================================================
# 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 (used by 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
P.S.: Yes, I know some of you cringe when you see me using emacs as a fall-back editor.
P.P.S.: We do know we'll have to blow away all of our clones of the existing repository after the rebase. (Along the lines of "thou shalt not rebase a repository after it's been published".)
P.P.P.S: Can anyone tell me how to add a bounty to this? I'm not seeing the option anywhere on this screen whether I'm in edit mode or view mode.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
如果您不介意创建一个新分支,这就是我处理问题的方式:
在主分支上:
If you don't mind creating a new branch, this is how I dealt with the problem:
Being on main:
好吧,我有足够的信心给出答案。也许需要编辑它,但我相信我知道你的问题是什么。
您的玩具存储库测试用例中有一个合并 - 更糟糕的是,它有一个存在冲突的合并。并且您正在合并过程中进行变基。如果没有
-p
(它不能完全与-i
一起使用),合并将被忽略。这意味着当 rebase 尝试挑选下一个提交时,您在冲突解决中所做的任何操作都不存在,因此其补丁可能不适用。 (我相信这显示为合并冲突,因为 gitcherry-pick 可以通过在原始提交、当前提交和共同祖先之间进行三向合并来应用补丁。)不幸的是,正如我们在评论中指出的,
-i
和-p
(保留合并)相处得不太好。我知道编辑/重新措辞有效,而重新排序则无效。不过,我相信它对南瓜效果很好。这没有记录,但它适用于我在下面描述的测试用例。如果你的情况非常复杂,那么你在做你想做的事情时可能会遇到很多麻烦,尽管这仍然是可能的。 (这个故事的寓意:在合并之前使用rebase -i
进行清理。)因此,假设我们有一个非常简单的情况,我们想要将 A、B 压缩在一起和 C:
现在,就像我说的,如果 X 中没有冲突,
git rebase -i -p
将按您的预期工作。如果存在冲突,事情就会变得有点棘手。它会进行很好的压缩,但是当它尝试重新创建合并时,冲突将再次发生。您必须再次解析它们,将它们添加到索引中,然后使用 git rebase --continue 继续。 (当然,您可以通过检查原始合并提交的版本来再次解决它们。)
如果您碰巧有
rerere
在您的存储库中启用(rerere.enabled
设置为 true),这会更容易- git 将能够重新使用您最初遇到冲突时的重新解决方案,而您所要做的就是检查以确保其正常工作,将文件添加到索引,然后继续。 (您甚至可以更进一步,打开rerere.autoupdate
,它会为您添加它们,因此合并甚至不会失败)。不过,我猜测您从未启用过 rerere,因此您必须自己解决冲突。** 或者,您可以尝试
rerere-train.sh
来自 git-contrib 的脚本,它尝试“从现有的合并提交中填充 [the] rerere 数据库” - 基本上,它检查所有合并提交,尝试合并它们,如果合并失败,它会获取结果并将其显示给 git-rerere。这可能很耗时,而且我从未真正使用过它,但它可能非常有帮助。All right, I'm confident enough to throw out an answer. Maybe will have to edit it, but I believe I know what your problem is.
Your toy repo test case has a merge in it - worse, it has a merge with conflicts. And you're rebasing across the merge. Without
-p
(which doesn't totally work with-i
), the merges are ignored. This means that whatever you did in your conflict resolution isn't there when the rebase tries to cherry-pick the next commit, so its patch may not apply. (I believe this is shown as a merge conflict becausegit cherry-pick
can apply the patch by doing a three-way merge between the original commit, the current commit, and the common ancestor.)Unfortunately, as we noted in the comments,
-i
and-p
(preserve merges) don't get along very well. I know that editing/rewording work, and that reordering doesn't. However, I believe that it works fine with squashes. This is not documented, but it worked for the test cases I describe below. If your case is way, way more complex, you may have a lot of trouble doing what you want, though it'll still be possible. (Moral of the story: clean up withrebase -i
before merging.)So, let's suppose we have a very simple case, where we want to squash together A, B, and C:
Now, like I said, if there were no conflicts in X,
git rebase -i -p
works as you'd expect.If there are conflicts, things get a little trickier. It'll do fine squashing, but then when it tries to recreate the merge, the conflicts will happen again. You'll have to resolve them again, add them to the index, then use
git rebase --continue
to move on. (Of course, you can resolve them again by checking out the version from the original merge commit.)If you happen to have
rerere
enabled in your repo (rerere.enabled
set to true), this will be way easier - git will be able to reuse the recorded resolution from when you originally had the conflicts, and all you have to do is inspect it to make sure it worked right, add the files to the index, and continue. (You can even go one step farther, turning onrerere.autoupdate
, and it'll add them for you, so the merge won't even fail). I'm guessing, however, that you didn't ever enable rerere, so you're going to have to do the conflict resolution yourself.** Or, you could try the
rerere-train.sh
script from git-contrib, which attempts to "Prime [the] rerere database from existing merge commits" - basically, it checks out all the merge commits, tries to merge them, and if the merge fails, it grabs the results and shows them togit-rerere
. This could be time-consuming, and I've never actually used it, but it might be very helpful.如果您想从一长段提交中创建一个提交(其中一些是合并提交),最简单的方法是将分支重置到第一次提交之前的点,同时保留所有更改,然后重新提交它们:
替换 < code>origin/master 替换为您分支的分支的名称。
add .
是必需的,因为新添加的文件在重置后显示为未跟踪。If you want to create exactly one commit out of a long branch of commits, some of which are merge commits, the easiest way is to reset your branch to the point before the first commit while keeping all your changes, then recommitting them:
Replace
origin/master
with the name of the branch from which you branched off.The
add .
is necessary because files that were newly added appear as untracked after the reset.我一直在寻找类似的要求,即放弃我的开发分支的中间提交,我发现这个过程对我有用。
在我的工作分支
viola 上,我们已将最新提交连接到分支的开始提交!
没有合并冲突问题!
在我的学习实践中我现阶段得出了这样的结论,是否有更好的方法达到目的。
I was looking for a similar requirement , i.e. discarding intermeiate commits of my development branch , I've found this procedure worked for me.
on my working branch
viola we have connected the latest commit to the start commit of the branch!
No merge conflict issues!
In my learning practice I have come to this conclusion at this stage , Is there a better approach for the purpose .
基于@hlidka上面的很好的答案,最大限度地减少了手动干预,我想添加一个版本来保留任何新的提交不在分支中的大师要挤压。
我相信这些很容易在该示例中的 git reset 步骤中丢失。
Building on @hlidka's great answer above which minimises manual intervention, I wanted to add a version that preserves any new commits on master that aren't in the branch to squash.
As I believe these could be easily lost in the
git reset
step in that example.请注意,在交互式变基中使用时,
-X
和策略选项将被忽略。请参阅提交db2b3b820e2b28da268cc88adff076b396392dfe(2013年7月,git 1.8.4+),
这意味着
-X
和策略现在可以与交互式变基以及普通变基一起使用,并且您的初始脚本现在可以更好地工作。Note that
-X
and strategy options were ignored when used in an interactive rebase.See commit db2b3b820e2b28da268cc88adff076b396392dfe (July 2013, git 1.8.4+),
That means
-X
and strategy now work with interactive rebase, as well as plain rebase, and your initial script could now work better.我遇到了一个更简单但类似的问题,我有
1)解决了本地分支上的合并冲突,
2)继续努力添加更多的小提交,
3)想要变基并遇到合并冲突。
对我来说, git rebase -p -i master 有效。它保留了最初的冲突解决承诺,并允许我将其他人压在上面。
希望对某人有帮助!
I was running into a simpler but similar issue, where I had
1) resolved a merge conflict on a local branch,
2) kept working adding lots more little commits,
3) wanted to rebase and hit merge conflicts.
For me,
git rebase -p -i master
worked. It kept the original conflict resolution commit and allowed me to squash the others on top.Hope that helps someone!