为什么git rebase会删除最近一次提交添加的文件,如果这个文件被rebase分支删除了?

8

我正在尝试弄清楚为什么git rebase会导致新创建的文件被删除,如果我要rebasing的分支已经将该文件删除。例如:

A1 - A2 - A3
 \
  B1

A2 = add a new file test.txt
A3 = delete test.txt
B1 = add the exact same file as A2

如果B1被签出并且我执行git rebase A3,test.txt仍然会被删除。我期望的结果是:
A1 - A2 - A3 - B1

这意味着test.txt仍然存在。为什么在rebase之后test.txt被删除了呢?

1
如果我正确理解了你的问题,那么这不应该发生。你有一个可以重现这个问题的脚本吗?(编写一个脚本,创建一个空目录,运行 git init,创建文件并提交,创建一个分支,再创建更多的文件等等,最后运行 git rebase 来展示这个问题。) - torek
当然,我会很快写一个。 - maxbart
好的,您可以复制并粘贴这个脚本 http://pastebin.com/mPcmGCT5 编辑: 更新了脚本以创建一个目录(创建testing_git)。 - maxbart
@maxbart 这可能有点冒险,但你能把pastebin上的脚本放到你的问题里吗? - AncientSwordRage
2个回答

10

哇,这个问题真难啊! :-)

使用您的脚本,我重现了这个问题。但是整个过程非常奇怪,所以我首先删除了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"

# setup B branch
git checkout -b B
echo hello > test.txt
git add .
git commit -m "added test.txt"

# setup master
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 Bgit 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


3
正如git rebase文档所说:

请注意,HEAD中引入与HEAD..<upstream>提交中相同文本更改的任何提交都将被省略 (即,已经在上游接受一个具有不同提交消息或时间戳的补丁将被跳过)。

在您的情况下,B1引入了与A2相同的更改。因此,在进行rebase时,B1将从rebase过程中省略,因为<upstream>已经有这个补丁。您可以添加-i选项来进行交互式rebase。这允许您查看rebase过程的待办列表中没有列出B1。虽然您可以通过在交互式rebase的待办列表中添加pick B1来手动选择该提交。

使用 rebase -i 压缩我的分支,然后将其变基到另一个分支上成功了。 - Ricardo Saracino

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接