哇,这个问题真难啊! :-)
使用您的脚本,我重现了这个问题。但是整个过程非常奇怪,所以我首先删除了rebase步骤,只剩下这个(稍作修改的)脚本:
#!/bin/sh
set -e
if [ -d testing_git ]; then
echo test dir testing_git already exists - halting
exit 1
fi
mkdir testing_git
cd testing_git
git init
touch main.txt
git add .
git commit -m "initial commit"
git checkout -b B
echo hello > test.txt
git add .
git commit -m "added test.txt"
git checkout master
echo hello > test.txt
git add .
git commit -m "added test.txt"
rm test.txt
git add .
git commit -m "remove test.txt"
运行后,检查提交记录,我得到了这个:
$ git log --graph --decorate | sed 's/@/ /'
* commit 249e4893ea7458f45fe5cdc496ddc0292a3f03ef (HEAD -> master)
| Author: Chris Torek <chris.torek gmail.com>
| Date: Thu May 5 20:28:02 2016 -0700
|
| remove test.txt
|
* commit a132dc9e3939b5338f7c784c58da9c83f4902c8d (B)
| Author: Chris Torek <chris.torek gmail.com>
| Date: Thu May 5 20:28:02 2016 -0700
|
| added test.txt
|
* commit 81c4d9be82094fdb4c88ed0a53bdbd5c3dfd7a5a
Author: Chris Torek <chris.torek gmail.com>
Date: Thu May 5 20:28:02 2016 -0700
initial commit
请注意,master
的父提交是分支B
的提交,总共只有三个提交,而不是四个。当脚本运行了四个git commit
命令时,这是怎么做到的呢?
现在,让我们在脚本中添加sleep 2
,将其放在git checkout master
之后,并重新运行它,看看会发生什么...
[edit]
$ sh testrebase.sh
[snip output]
$ cd testing_git && git log --oneline --decorate --graph --all
* cddbff1 (HEAD -> master) remove test.txt
* c4ac1b2 added test.txt
| * fefc150 (B) added test.txt
|/
* 8c07bb6 initial commit
哇,现在我们有了四个提交和一个正确的分支!
为什么第一个脚本会做三个提交,加上sleep 2
后变成四个提交呢?
答案在于提交的身份。每个提交都有(理论上)唯一的ID,这是提交内容的校验和。以下是第一次B
分支提交时的内容:
$ git cat-file -p B | sed 's/@/ /'
tree c3cd0188a6a1490204e25547986e49b0b445dec8
parent 81c4d9be82094fdb4c88ed0a53bdbd5c3dfd7a5a
author Chris Torek <chris.torek gmail.com> 1462505282 -0700
committer Chris Torek <chris.torek gmail.com> 1462505282 -0700
added test.txt
我们有一个tree,它的parent是master分支上的第一次提交,我们还有两个元组(作者和committer的名称、电子邮件和时间戳),一个空白行和日志信息。当我们在master分支上进行第二次提交时,Git从新文件生成了一个新的tree。这个tree与我们刚刚在分支B上提交的tree完全相同,因此它有了相同的唯一ID。然后,Git像往常一样创建了一个具有我的名称、电子邮件和时间戳以及日志信息的新提交对象。但是,由于该提交与我们刚刚在分支B上创建的提交完全相同,所以我们获得了与之前相同的ID,并使主分支指向该提交,换句话说,我们重新使用了该提交,只是在不同的分支上进行了提交(使得master分支指向与B相同的提交)。添加sleep 2更改了新提交的时间戳。现在,分支B和master中的两个提交不再完全相同。
$ git cat-file -p B | sed 's/@/ /' > bx
$ git cat-file -p master^ | sed 's/@/ /' > mx
$ diff bx mx
3,4c3,4
< author Chris Torek <chris.torek gmail.com> 1462505765 -0700
< committer Chris Torek <chris.torek gmail.com> 1462505765 -0700
---
> author Chris Torek <chris.torek gmail.com> 1462505767 -0700
> committer Chris Torek <chris.torek gmail.com> 1462505767 -0700
不同的时间戳 = 不同的提交 = 更合理的设置。
然而,实际执行变基操作后,文件还是被删除了!
事实证明,这是有意设计的。当你运行git rebase
时,设置代码不仅简单地列出每个提交进行挑选,而是使用git rev-list --right-only
查找需要删除的提交1。
由于添加test.txt
的提交在上游,Git直接将其删除:这里的假设是你将其发送给某个人并将其上传到上游,他们已经拿走了它,没有必要再次获取它。
让我们修改复现脚本再次——这次我们能够通过不同的更改--cherry-pick --right-only
来删除master
列表中的项。我们仍将添加相同的单行test.txt
,但也会修改该提交中的main.txt
:
# setup master
git checkout master
echo hello > test.txt
echo and also slight difference >> main.txt
git add .
git commit -m "added test.txt"
我们可以继续执行最后的git checkout B
和git rebase master
命令,这次,变基会按照我们最初的期望工作:
$ git log --oneline --decorate --graph --all
* c31b13a (HEAD -> B) added test.txt
* da2ca52 (master) remove test.txt
* 6972019 added test.txt
* 0f0d2e8 initial commit
$ ls
main.txt test.txt
我之前并没有意识到 rebase 会这样做;虽然另一个回答指出它被记录在文档中,但这并不是我预期的事情。这意味着说“rebase 只是重复的 cherry-pick”并不完全正确:它是带有特殊情况的重复 cherry-pick,可以丢弃提交。
1实际上,对于非交互式的 rebase ,它使用了以下关键行:
git format-patch -k --stdout --full-index --cherry-pick --right-only \
--src-prefix=a/ --dst-prefix=b/ --no-renames --no-cover-letter \
"$revisions" ${restrict_revision+^$restrict_revision} \
>"$GIT_DIR/rebased-patches"
在这种情况下,$revisions
会扩展为 master...B
。
--cherry-pick --right-only
选项对于git format-patch
来说是没有文档记录的;你必须知道在git rev-list
文档中查找它们。
交互式变基使用不同的技术,但仍会选择掉任何已经在上游的提交。如果你把rebase
改成rebase -i
,那么变基指令将只有一行noop
,而不是预期的单行pick
。
git init
,创建文件并提交,创建一个分支,再创建更多的文件等等,最后运行git rebase
来展示这个问题。) - torek