为什么git rebase删除了最新提交中添加的文件(如果它被rebase分支删除)?

时间:2016-05-06 02:23:55

标签: git rebase git-rebase

我正在试图弄清楚为什么git rebase会导致新创建的文件被删除,如果我正在重新定义的分支将其删除。例如:

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?

2 个答案:

答案 0 :(得分:5)

哇,这是一个艰难的! : - )

使用您的脚本,我重现了这个问题。尽管如此,有一些非常奇怪的事情,所以首先,我删除了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

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

然后,当我们在master分支上进行第二次提交时,git从新文件中创建了一个新树。这个树与我们刚刚在分支B上提交的那个树有点相同,所以它得到了相同的唯一ID(请记住,repo中只有该树的一个副本,所以这是正确的行为) 。然后它像往常一样用我的名字,电子邮件和时间戳创建了一个新的提交对象和日志消息。但是这个提交与我们刚刚在分支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

不同的时间戳=不同的提交=更明智的设置。

然而,实际上执行rebase无论如何都丢弃了文件!

事实证明这是设计的。当您运行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只是重复樱桃选择”并不完全正确:它是重复的樱桃-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的{​​{1}}选项未记录;我们必须知道要查看git format-patch文档。

交互式rebase使用不同的技术但仍然选择已经在上游的任何提交。如果您将git rev-list更改为rebase,则会显示,因为rebase指令包含一行rebase -i而不是预期的单noop行。

答案 1 :(得分:2)

正如git rebase documentation所说:

  

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

在您的情况下B1引入与A2相同的更改。因此,当您进行rebase时,B1在rebase过程中被省略,因为&lt; upstream&gt;已经有了补丁。您可以添加-i选项来执行交互式rebase。这允许您看到,B1未列在rebase进程的待办事项列表中。虽然,您可以通过在交互式rebase的待办事项列表中添加pick B1来手动选择提交。