如果 rebase 分支删除了最近提交中添加的文件,为什么 git rebase 会删除该文件?

Why does git rebase delete a file added in the most recent commit if it was deleted by the rebase branch?

我想弄清楚为什么 git 变基会导致新创建的文件在我要变基的分支被删除时被删除。例如:

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 仍然存在。为什么变基后 test.txt 被删除了?

正如git rebase documentation所说:

Note that any commits in HEAD which introduce the same textual changes as a commit in HEAD..<upstream> are omitted (i.e., a patch already accepted upstream with a different commit message or timestamp will be skipped).

在您的案例中 B1 引入与 A2 相同的更改。因此,当您进行变基时,变基过程中会省略 B1,因为 已经有了该补丁。您可以添加 -i 选项来进行交互式变基。这让你看到,B1 没有列在变基过程的待办事项列表中。虽然,您可以通过在交互式 rebase 的待办事项列表中添加 pick B1 来手动选择该提交。

哇,这太难了! :-)

使用你的脚本,我重现了这个问题。虽然这一切都有些奇怪,所以首先,我删掉了变基步骤,留下这个(稍微修改过的)脚本:

#!/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

哇哦,现在我们有四个提交和一个正确的分支!

为什么第一个脚本进行了 3 次提交,添加 sleep 2 将其更改为进行 4 次提交?

答案在于提交的身份。每个提交都有一个(据说!)唯一 ID,它是提交内容的校验和。这是第一次在 B-branch 提交中的内容:

$ 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

我们有 treeparent、作者和提交者的两个(姓名、电子邮件、时间戳)三元组、一个空行和日志消息。父级是 master 分支上的第一个提交,树是我们添加 test.txt(及其内容)时创建的树。

然后,当我们在 master 分支上进行 second 提交时,git 从新文件创建了一个新树。这棵树与我们刚刚在分支 B 上提交的树完全相同,因此它具有相同的唯一 ID(请记住,回购中只有该树的一个副本,因此这是正确的行为) .然后它像往常一样用我的名字、电子邮件和时间戳以及日志消息创建了一个新的提交 object。但是这次提交与我们刚刚在分支 B 上所做的提交完全相同,因此我们获得了与之前相同的 ID,并使分支 master 指向该提交。

换句话说,我们重新使用了提交。我们只是在不同的分支上创建它(因此 master 指向与 B 相同的提交)。

添加 sleep 2 更改了新提交的 时间戳 。现在这两个提交(在 Bmaster 中)不再是逐位相同的:

$ 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 完全删除它:这里的假设是你将它发送给上游的某人,他们已经接受了它,并且有不用再拿了。

让我们再次修改复制器脚本——这次我们可以去掉sleep 2,加快速度——这样就可以把master 不同,不会通过 --cherry-pick --right-only 从列表中删除。我们仍将使用相同的单行添加 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 is just repeated cherry-pick" 不太正确:它重复了 cherry-选择,具有丢弃提交的特殊情况。


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

git format-patch--cherry-pick --right-only 选项未记录;必须知道在 git rev-list 文档中查找它们。

Interactive rebase 使用不同的技术,但仍然选择掉上游中已经存在的任何提交。如果您将 rebase 更改为 rebase -i,则会出现这种情况,因为变基指令由一个 noop 行组成,而不是预期的单个 pick 行。