我正在试图弄清楚为什么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?
答案 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
我们有作者和提交者的tree
,parent
,两个(姓名,电子邮件,时间戳)三元组,一个空白行和日志消息。父对象是主分支上的第一个提交,树是我们在添加test.txt
(及其内容)时创建的树。
然后,当我们在master
分支上进行第二次提交时,git从新文件中创建了一个新树。这个树与我们刚刚在分支B
上提交的那个树有点相同,所以它得到了相同的唯一ID(请记住,repo中只有该树的一个副本,所以这是正确的行为) 。然后它像往常一样用我的名字,电子邮件和时间戳创建了一个新的提交对象和日志消息。但是这个提交与我们刚刚在分支B
上提交的提交略有一致,所以我们得到了与之前相同的ID,并使分支master
指向该提交。
换句话说,我们重新使用了提交。我们只是在不同的分支上创建它(因此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
不同的时间戳=不同的提交=更明智的设置。
然而,实际上执行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 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只是重复樱桃选择”并不完全正确:它是重复的樱桃-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)
请注意,HEAD中的任何提交都会引入与HEAD中的提交相同的文本更改。&lt; upstream&gt;被省略(即,将跳过已经在上游接受不同提交消息或时间戳的补丁)。
在您的情况下B1
引入与A2
相同的更改。因此,当您进行rebase时,B1
在rebase过程中被省略,因为&lt; upstream&gt;已经有了补丁。您可以添加-i
选项来执行交互式rebase。这允许您看到,B1
未列在rebase进程的待办事项列表中。虽然,您可以通过在交互式rebase的待办事项列表中添加pick B1
来手动选择提交。