git push 是否完全替换了远程仓库中的目标分支?我见过的所有撤消 git push 的解决方案都涉及在本地更改您的分支,然后重做您的 git push。你不能像撤消合并一样撤消 git push 吗?
2 回答
子问题“git push 是否完全替换了远程仓库中的目标分支?” 是有问题的。用“是”或“否”来回答充其量是误导。(与老律师的笑话“你停止殴打你的妻子吗”相比:如果你说“是”,则意味着你曾经殴打过她,如果你说“不”,则意味着你仍然会这样做![作为计算机程序员/逻辑学家,我想说正确的答案可能是“不” ,但没有那种暗示,但人们并不总是这样想。])
粗略地说,git push
所做的事情与所做的事情相同git fetch
,但方向相反。通过智能协议(如 ssh),有一个完整的协商序列,其中“你的一端”和“他们的一端”交换关于他们拥有哪些对象以及他们没有但想要哪些对象的信息。在 a 的情况下push
,您告诉另一端:“我有一个分支标签,B,那指的是最终提交 C9,我可以把它推给你吗?”他可能会说“等等 C9 的父母是什么”,而你说“C8”,他会说“我也没有那个,C8 的父母是什么”并且你说“C6 和 C7 的合并”,他说“哦,我得到了这两个 OK 给我 C8 和 C9”。所以你的结果然后制作了一个“包装”(特别是一个“薄包装” - 细节并不那么重要,但这就是您看到的消息,“计数对象”和“压缩对象”等等,是关于)保存这些提交以及它们引用的树和文件。
一旦您的发送将对象发送到他们的一端,他们的一端会将推送的分支标签移动到新的分支提示。如果新的分支提示标记了一个与远程分支完全没有共同历史的分支[见下面的注释],那么 - 实际上 - 答案将是“是的,它完全取代了目标分支”。也就是说,它发送一包全新的提交对象,分支标签B进入新的(不同的)提交树。
不过,更常见的情况是,您的一端在上面描述的提交 C9 上具有类似的分支提示标签,因此您只需交出几个新提交(C8 和 C9),最后,他们将分支标签从 C7 移出到 C9。在这种情况下,答案是“不,它不会替换目标分支,它只是扩充它并将标签向前移动。”
不过,还有一个更复杂但实际上非常典型的场景。也许“他们的结局”在提交 C7 之后有一个你没有的提交 C10 。同时,您有要发送的提交 C8 和 C9。这是一个“非快进”操作。通常--bare
会设置一个存储库来拒绝此类推送:他们会看到您要求添加 C8 和 C9,然后将分支标签从 C10 移动到 C9。这将留下一个“孤立”提交(C10),即没有标签的提交。如果你--force
推动(并且被允许),git 无论如何都会继续这样做。[这是之前的注释:推送一个不相关的分支也需要--force
,出于几乎相同的原因:git 检测到某些提交将变得未被引用,并让您说“我真的打算这样做”。]
与 All Things Git 一样,之前的提交“仍然存在”,只是不再被引用。当然,推送几乎总是去--bare
回购。--bare
由于没有充满 reflog 的目录,repo的事情变得有点棘手.git/logs
,所以现在要找到像 C10 这样丢失的提交或整个丢失的提交链要困难得多。(您,或本次讨论中的“他们”,需要git fsck --unreachable
找到无法访问的提交对象来恢复它们。)
现在,如果你做了一个你不应该做的推送——假设你打算推送提交 C8,而不是 C9——那么就有可能说服遥控器更改他的标签版本。假设您已更新master
,并且remote.origin.url
类似于ssh://barehost/dir/with/repo.git
. 假设您可以使用repossh barehost
登录主机:--bare
edithost$ ssh barehost
barehost$ cd /dir/with/repo.git
barehost$ git log --oneline --decorate -2 master
5e01371 (HEAD, master) commit-C9's one line description
309b36c commit-C8's one line desc.
barehost$
现在您可以重新标记master
以指向提交 C8:
barehost$ git branch -f master master^
噗,--bare
repo.git
副本现在有一个以提交 C8 结束的分支。
但是,如果您只是在其上创建自己的分支edithost
并强制推送它,则会获得相同的效果:
edithost$ git branch rewinder master^
edithost$ git push -f origin rewinder:master
这第二个git push
将连接到barehost
并且交换如下所示:
edithost (to barehost): branch label for you, master should go to 309b36c
barehost (to edithost): I got 309b36c but are you sure? force push?
edithost: yep, he's sure...
barehost: OK, I set master to 309b36c!
(实际上在最初的交换中强制标志是正确的,但我认为这样想象它更有趣/更难忘)。这真的很快;不需要“薄包”对象。
执行这些操作中的任何一个(<code>ssh 到barehost
并手动重置git branch
,或强制推送rewinder:master
标签对)的缺陷是相同的:如果其他人(我们称他为 Pat)进来并barehost
在这期间抓住了提交 C9你不小心推了它,而你“撤销”它的时间,那么帕特可能会感到困惑。他现在有提交 C9,据他所知,它应该在 C8 之后。根据接下来发生的情况,他可能会尝试使用它,或者他可能会将其推回barehost
并使其重新出现,或者其他什么。
如果你做了一个错误的提交出现在某个共享的地方,那么“摆脱它”的“安全”(为了某种安全的价值)要做的事情就是在那个提交之后进行一个新的提交,这会消除坏的影响一。每个人都希望出现更多新的提交。但是,除非您提前安排,否则没有人会期望提交(或整个提交树)会消失。
您可以完全忽略其余部分
这是一个完整的示例,根据要求带有一些 ASCII 艺术。我不确定它会好得多。实际上可能更糟。:-)
barehost
让我们从, 在 a 中的一些提交开始--bare
repo.git
。
A0
是初始提交,如下所示。(这个 git 树只包含一个文件,somefile
,带有无意义的文本。)
barehost
A0 -- A1 -- A2 <-- master
现在我将它克隆到edithost:
$ git clone ssh://[redacted]/repo.git
$ cd repo
我决定我不喜欢其中的任何内容,所以我将旧的 master 分支移开并创建一个新分支。(另外,我不想old_master
跟踪origin/master
。edithost
)
$ git branch -m master old_master
$ git config --remove-section branch.old_master
(现在追踪消失了)
$ git checkout --orphan master
Switched to a new branch 'master'
$ git rm somefile # or "git rm -rf ."
rm 'somefile'
$ echo '*.o' > .gitignore
$ git add .gitignore && git commit -m C0
[master (root-commit) f09f66f] C0
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
这是生成的分支图。(我HEAD
在所有情况下都省略了以避免混乱。)
A0 -- A1 -- A2 <-- origin/master, old_master
C0 <-- master
我确实希望这个新master
的来跟踪远程主机(最终),所以我也会在这里设置它:
$ git config branch.master.remote origin
$ git config branch.master.merge refs/heads/master
现在让我们继续向下并通过 提交C1
,C7
作为C7
一个新分支。内容并不重要,所以我将它们排除在外。然而,现在的画面是这样的:
A0 -- A1 -- A2 <-- origin/master, old_master
C0 -- ... -- C5 -- C6 <-- master
\
C7 <-- feep
现在:
$ git push origin feep:feep
Counting objects: 25, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (21/21), done.
Writing objects: 100% (25/25), 2.27 KiB | 0 bytes/s, done.
Total 25 (delta 6), reused 0 (delta 0)
To ssh://[redacted]/repo.git
* [new branch] feep -> feep
如果我尝试“git push origin master:master”,当然会出现错误,但无论如何我都可以使用 -f 推送:
$ git push -f origin master:master
Total 0 (delta 0), reused 0 (delta 0)
To ssh://[redacted]/repo.git
+ dce5ffc...ae235f9 master -> master (forced update)
注意这次推送说的是“强制更新”(和“+”);那是-f
旗帜。它也跳过了Counting objects
,只是说Total 0
。这是因为我们已经在推送分支“ feep
”的步骤中推送了C0
提交。C7
这次唯一发生的事情是从提交中删除标签“master”A2
并将其粘贴在提交上C6
(不是C7
,那只是在“feep”分支上)。但是现在图片看起来像这样
edithost
:
A0 -- A1 -- A2 <-- old_master
C0 -- ... -- C5 -- C6 <-- origin/master, master
\
C7 <-- origin/feep, feep
像这样barehost
:
A0 -- A1 -- A2 [no label]
C0 -- ... -- C5 -- C6 <-- master
\
C7 <-- feep
这里有一个关键点:此时,repository 是一样的;只有标签名称不同。我可以使用 label
old_master
onedithost
并将其转换为提交 ID:
$ git rev-parse old_master
dce5ffcfcee7c5c66275189c75ea68219e8f26f5
并转到barehost
并查看提交A0
、A1
和A2
。在一些临时仓库上自己尝试一下(您的提交 ID 会有所不同):git log --oneline dce5ffcfcee7c5c66275189c75ea68219e8f26f5
无论如何,回到edithost
. 让我们做一个新的克隆:
$ cd ..
$ git clone ssh://[redacted]/repo.git altrepo
Cloning into 'altrepo'...
remote: Counting objects: 25, done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 25 (delta 6), reused 0 (delta 0)
Receiving objects: 100% (25/25), done.
Resolving deltas: 100% (6/6), done.
Checking connectivity... done
$ cd altrepo
在此处运行git log --all
显示C0
通过C7
. 让我们在分支C10
上添加一个提交,然后推送它:C6
master
$ git branch
* master
$ git log --oneline -1
ae235f9 C6
$ echo 'Some important documentation blah blah blah' > doc.txt
$ git add doc.txt && git commit -m C10
[master a22a608] C10
1 file changed, 1 insertion(+)
create mode 100644 doc.txt
$ git push origin master:master
Counting objects: 4, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 368 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ssh://[redacted]/repo.git
ae235f9..a22a608 master -> master
$
现在barehost
有这个:
A0 -- A1 -- A2 [no label]
C0 -- ... -- C5 -- C6 -- C10 <-- master
\
C7 <-- feep
同时,在repo
(not altrepo
) 我们还C10
没有。如果我们再次强制推送,我们将在这里故意视而不见来说明“丢失”提交。
让我们回到那里并合并feep
为master
一个非快进,在 commit 下C8
,我们想要推回;并添加另一个实验性提交,我们将在下面“错误地”推送:
$ cd ../repo
$ git checkout master
$ git merge --no-ff feep -m C8
Merge made by the 'recursive' strategy.
silly.c | 19 ++++++++++++++++++-
1 file changed, 18 insertions(+), 1 deletion(-)
$ echo experiment > experimental.file
$ git add experimental.file && git commit -m C9
repo
这个相当杂乱的图表显示了on中的提交树的状态edithost
。请注意,就 git 而言,
origin/master
还没有移动。(在../altrepo
git 中知道 commit C10
,但在 git 中./repo
,它不知道。)
A0 -- A1 -- A2 <-- old_master
C0 -- ... -- C5 -- C6 _ <-- origin/master
\ \_ C8 -- C9 <-- master
\ /
C7 <-- origin/feep, feep
让我们尝试在没有 -f 的情况下推动它:
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 3 commits.
# (use "git push" to publish your local commits)
#
nothing to commit, working directory clean
$ git push origin master:master
To ssh://[redacted]/repo.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'ssh://[redacted]/repo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
而不是git pull
,让我们使用git fetch
,因为fetch
是正确的反义词push
:
$ git fetch
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ssh://[redacted]/repo
ae235f9..a22a608 master -> origin/master
$ git status
# On branch master
# Your branch and 'origin/master' have diverged,
# and have 3 and 1 different commit each, respectively.
# (use "git pull" to merge the remote branch into yours)
#
nothing to commit, working directory clean
现在我们可以看到 commit C10
。这是git log
格式:
$ git log --oneline --decorate --graph --all
* 525b6fa (HEAD, master) C9
* 1933a04 C8
|\
| * c4c3302 (origin/feep, feep) C7
|/
| * a22a608 (origin/master, origin/HEAD) C10
|/
* ae235f9 C6
* e026711 C5
* f16f7cc C4
* 00d0a69 C3
* 45acc03 C2
* 07a1145 C1
* f09f66f C0
* dce5ffc (old_master) A2
* ad84d73 A1
* cee030f initial commit
这是一个 ASCII-art 版本,可能更清晰,也可能不清晰,但至少是我一直使用的风格:
A0 -- A1 -- A2 <-- old_master
C10 <-- origin/master
/
C0 -- ... -- C5 -- C6 _
\ \_ C8 -- C9 <-- master
\ /
C7 <-- origin/feep, feep
我实际上不必C10
在此处的任何地方保存提交的 ID,因为我在../altrepo
. 让我们push -f
:
$ git push -f origin master:master
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 529 bytes | 0 bytes/s, done.
Total 4 (delta 0), reused 0 (delta 0)
To ssh://[redacted]/repo.git
+ a22a608...525b6fa master -> master (forced update)
提交被复制到--bare
repo,它现在有:
A0 -- A1 -- A2 [no label]
C10 [no label]
/
C0 -- ... -- C5 -- C6 _
\ \_ C8 -- C9 <-- master
\ /
C7 <-- feep
(如果您在裸主机上,很难看到提交A0
、A1
、A2
和
C10
,但是如果您获取它们的 SHA-1 值并将它们交给git
log
,您可以看到它们实际上在那里。)
现在让我们意识到:哎呀,错误,并不是要推C9
送到
barehost
. barehost
从以下位置倒带很容易edithost
:
$ git branch rewinder master^
$ git push -f origin rewinder:master
Total 0 (delta 0), reused 0 (delta 0)
To ssh://[redacted]/repo.git
+ 525b6fa...1933a04 rewinder -> master (forced update)
并且在 上barehost
,git log --oneline
表明它似乎已经消失了:
barehost$ git log --oneline --graph
* 1933a04 C8
|\
| * c4c3302 C7
|/
* ae235f9 C6
* e026711 C5
* f16f7cc C4
* 00d0a69 C3
* 45acc03 C2
* 07a1145 C1
* f09f66f C0
但实际上,我们现在拥有的是这样的:
A0 -- A1 -- A2 [no label]
C10 [no label]
/
C0 -- ... -- C5 -- C6 _
\ \_ C8 <-- master
\ / \_ C9 [no label]
C7 <-- feep
让我们使用我们对所有 SHA-1 的知识来“复活”(标记)所有丢失的提交和分支,在barehost
:
barehost$ barehost$ git branch resurrected_master dce5ffc
barehost$ git branch recovered_c10 a22a608
barehost$ git branch recovered_c9 525b6fa
barehost$ git log --oneline --decorate --graph --all
* 525b6fa (recovered_c9) C9
* 1933a04 (HEAD, master) C8
|\
| * c4c3302 (feep) C7
|/
| * a22a608 (recovered_c10) C10
|/
* ae235f9 C6
* e026711 C5
* f16f7cc C4
* 00d0a69 C3
* 45acc03 C2
* 07a1145 C1
* f09f66f C0
* dce5ffc (resurrected_master) A2
* ad84d73 A1
* cee030f initial commit
现在回到edithost,在repo
:
$ git fetch
From ssh://[redacted]/repo
* [new branch] recovered_c10 -> origin/recovered_c10
* [new branch] recovered_c9 -> origin/recovered_c9
* [new branch] resurrected_master -> origin/resurrected_master
$ git log --oneline --decorate --graph --all
* 525b6fa (HEAD, origin/recovered_c9, master) C9
* 1933a04 (origin/master, origin/HEAD, rewinder) C8
|\
| * c4c3302 (origin/feep, feep) C7
|/
| * a22a608 (origin/recovered_c10) C10
|/
* ae235f9 C6
* e026711 C5
* f16f7cc C4
* 00d0a69 C3
* 45acc03 C2
* 07a1145 C1
* f09f66f C0
* dce5ffc (origin/resurrected_master, old_master) A2
* ad84d73 A1
* cee030f initial commit
它们都还在那里,现在它们也被标记(作为来源/名称)edithost
。(我们可能应该删除
rewinder
,edithost
因为我们可能已经完成了。)
要撤消它,您有两个选项提交“撤消”代码或替换撤消代码。
要替换它,您将变基并在 force 标志打开的情况下推动。
提交选项会向其他人显示你改变了主意,它更安全,因为如果人们已经拉取和分叉,那么这样他们至少可以参考表单的来源