将一个分支与另一个首先提交的分支合并
Merge a branch with another branch which first committed
我有以下问题。我在一个分支(我们称之为 A)上工作,在那里我实现了一个新功能。我只提交了更改,但没有推送它们。现在我后来意识到我在错误的分支上。所以我换到了右边的分支(B)。如何将更改从分支 A 转移到分支 B?
所以在 B 中,到目前为止的所有内容都保留下来,而 A 中的所有新内容都存放在 B 中。
如果:
- 您做喜欢一些提交,但是
- 关于相同的提交,还有一些您不喜欢的东西
那么通常解决这个问题的正确方法是使用 git rebase
。关于 git rebase
总是有一个警告,我稍后会描述,但由于您还没有 发送 这些提交给一些 other Git 存储库 - 您想要以某种方式更改的提交完全属于您,仅存在于您自己的 Git 存储库中 - 此警告不适用于您的情况。
不过,在您的特定情况下,您根本不需要使用变基。您将改为使用 git cherry-pick
,然后使用 git reset
或 git branch -f
。或者,您甚至可能不需要执行 cherry-pick.
关于提交(和 Git 的一般知识)
Git 实际上就是 提交 。它与文件无关,尽管提交会 hold 文件。它也与分支无关,尽管分支名称可以帮助我们(和 Git)找到 提交。不过,最后,重要的只是提交。这意味着您需要了解有关提交的所有信息。
在Git中:
每个提交都是编号,有一个独特的,但又大又丑的random-looking,哈希ID 或 object ID。这些实际上根本不是随机的:数字是加密哈希函数的输出。每个 Git 使用相同的计算,因此宇宙中每个地方的每个 Git 都会同意某个特定的提交得到 那个数字 。没有其他提交可以有 那个数字,不管它是什么:那个数字现在被那个特定的提交用完了。由于数字必须是普遍唯一的,因此它们必须很大(因此很难看,人类无法使用)。
Git 将这些提交和其他支持提交的内部 object 存储在一个大数据库中 - key-value store - 其中哈希 ID 是键和提交(或其他 object)是值。你给 Git 密钥,例如,通过从 git log
输出剪切和粘贴,Git 可以找到提交并因此使用它。这通常不是我们实际使用 Git 的方式,但重要的是要知道:Git 需要密钥,即哈希 ID。
每个提交存储两件事:
每次提交都会存储一个每个文件的完整快照,截至您提交时。它们以特殊的 read-only、Git-only、压缩和 de-duplicated 格式存储,而不是您计算机上的普通文件。根据您的 OS,Git 可能能够存储您的计算机实际上无法使用或提取的文件(例如,Windows 上名为 aux.h
的文件),这有时是个问题。 (你必须制作这些文件在OS上可以命名它们,当然,比如Linux。不过,所有这一切的目的只是为了表明这些文件 不是 常规文件。)
每个提交还存储一些 元数据,或有关提交本身的信息:例如,谁创建的,何时创建的。元数据包括 git log
显示的日志消息。对于 Git 至关重要的是,每个提交的元数据都包含一个列表——通常只有一个条目长——包含 先前的提交哈希 ID.
由于 Git 使用的散列技巧,没有提交——没有任何类型的内部 object——一旦被存储就永远无法更改。 (这也是文件存储的工作方式,也是 Git de-duplicates 文件的方式,并且可以存储您的计算机无法存储的文件。它们都只是那个大数据库中的数据。)
同样,提交的元数据存储一些先前提交的哈希 ID。大多数提交在此列表中只有一个条目,并且该条目是此提交的 parent。这意味着 child 提交记住他们的 parents' 的名字,但是 parents 不记得他们的 children:parents 被及时冻结从他们制作的那一刻起,他们 children 的最终存在就无法添加到他们的记录中。但是当child人出生时,parent人就存在了,所以child可以保存它的parent提交号。
这一切意味着提交形式向后看链,其中最新提交指向next-to-latest,并且该提交指向另一个跃点,依此类推。也就是说,如果我们绘制一个小的提交链,其 last 提交具有散列 H
,我们得到:
... <-F <-G <-H
哈希为H
的提交保存所有文件的快照,加上元数据; H
的元数据让 Git 找到提交 G
,因为 H
指向它的 parent G
。 Commit G
依次保存所有文件和元数据的快照,并且G
的元数据指向 F
。这一直重复到第一次提交,这是第一次提交 - 不能向后指向。它有一个空的 parent 列表。
git log
程序因此只需要知道一个提交哈希ID,即H
。从那里,git log
可以显示 H
,然后向后移动一跳到 G
并显示 G
。从那里,它可以向后移动另一跳到 F
,依此类推。当您厌倦阅读 git log
输出并退出程序时,或者当它一路返回到第一次提交时,该操作将停止。
分支名称帮助我们找到提交
这里的问题是我们仍然需要以某种方式记住链中最后一个提交 H
的哈希 ID。我们可以把它记在白板上、纸上或其他东西上——但我们有一台 计算机 。为什么不让 computer 为我们保存哈希 ID?这就是 分支名称 的意义所在。
每个分支名称,在Git中,只保存一个哈希ID。无论分支名称中的哈希 ID 是什么,我们都说该名称 指向 该提交,并且该提交是该分支的 尖端提交 .所以:
...--F--G--H <-- main
这里我们有分支名称 main
指向提交 H
。我们不再需要记住哈希 ID H
:我们只需输入 main
即可。 Git 将使用名称 main
查找 H
,然后使用 H
查找 G
,然后使用 G
查找 F
,等等。
一旦我们这样做了,我们就有了一个简单的方法来添加新的提交:我们只需做一个新的提交,比如I
,这样它就指向后面到 H
,然后 将 I
的哈希 ID 写入名称 main
,如下所示:
...--F--G--H--I <-- main
或者,如果我们不想更改我们的名字 main
,我们可以创建一个 新名字,例如 develop
或 br1
:
...--F--G--H <-- br1, main
现在我们有多个 name,我们需要知道我们使用哪一个来查找提交 H
,所以我们将绘制特殊名称 HEAD
,附加到分支名称之一,以表明:
...--F--G--H <-- br1, main (HEAD)
这里我们通过名称 main
使用提交 H
。如果我们 运行:
git switch br1
我们得到:
...--F--G--H <-- br1 (HEAD), main
没有其他变化——Git 注意到我们正在“从 H
移动到 H
”,可以说是——所以 Git 需要一些 short-cuts 并且不会为这种情况做任何其他工作。但现在我们是 on branch br1
,正如 git status
所说。现在,当我们进行新提交时 I
,我们将得到:
I <-- br1 (HEAD)
/
...--F--G--H <-- main
名称 main
留在原地,而名称 br1
移至指向新提交 I
。
您描述的情况
I was working on a branch (let's call it A) where I implemented a new function. I have only committed the changes, but I did not push them. Now I realized later that I am on the wrong branch. So I changed to the right branch (B). How can I transfer the changes from branch A to branch B?
让我们画这个:
...--G--H <-- br-A (HEAD), main
\
I--J <-- br-B
你是 on branch br-A
并且做了一个新的提交,我们称之为 K
:
K <-- br-A (HEAD)
/
...--G--H <-- main
\
I--J <-- br-B
关于提交 K
,您 做 有一些事情喜欢:例如,它的快照与提交 H
中的快照不同,无论您如何更改制作。它的日志消息也说明了您希望日志消息说明的内容。
但是有一件事你不喜欢提交K
:它发生在提交H
之后,当你想要它在提交 J
.
之后出现
您不能更改提交
我们在靠近顶部的位置注意到,一旦提交,就无法更改。您现有的提交 K
是一成不变的:没有人,没有任何东西,甚至 Git 本身也不能更改关于提交 K
的 任何东西。它在 H
之后,它有快照和日志消息,并且永远如此。
但是...如果我们可以复制 K
到一个新的改进的提交呢?我们称此 new-and-improved 提交 K'
,表明它是 K
的 副本 ,但有一些不同之处。
应该有什么不同?好吧,一方面,我们希望它在 J
之后出现。然后我们希望它对 K
对 H
所做的 更改 与 J
相同。也就是说,如果我们问 H
-vs-K
快照有什么不同,然后问 J
-vs-K'
快照有什么不同制作,我们希望获得 相同的更改。
有一个相当低级别的 Git 命令可以像这样精确地复制一个提交,称为 git cherry-pick
。这实际上就是我们最终要使用的。
不过,这里还是要说一下git rebase
。如果我们有十几个或一百个要复制的提交,cherry-pick 对每个进行复制可能会很乏味; git rebase
也会自动执行重复的 cherry-picking。所以 rebase 是 usual 使用的命令。
rebase 的工作原理如下:
- 首先,我们Git列出了它需要复制的所有提交。在这种情况下,只需提交
K
.
- 然后,我们 Git 签出 (切换到)我们所在的提交希望副本 go。在这种情况下,提交
J
.
- 接下来,我们Git从它创建的列表中一次复制每个提交。
- 然后我们 Git 获取 分支名称 找到要复制的 last 提交,然后移动它指向 last-copied 提交的名称。
所有这一切的最终结果,在这种情况下,是:
K ???
/
...--G--H <-- main
\
I--J <-- br-B
\
K' <-- br-A (HEAD)
请注意提交 K
仍然存在。只是再也没有人能找到它了。名称 br-A
现在找到 copy,提交 K'
.
Cherry-picking
这不是我们想要的,所以我们不使用 git rebase
,而是使用 git cherry-pick
。我们先运行:
git switch br-B
得到:
K <-- br-A
/
...--G--H <-- main
\
I--J <-- br-B (HEAD)
现在我们将 运行:
git cherry-pick br-A
这个用名字br-A
找到commitK
,然后复制到我们现在所在的地方。也就是说,我们得到了一个新的提交,它进行了 与提交 K
相同的更改 ,并且具有 相同的日志消息 。这个提交在我们现在所在的分支上进行,所以 br-B
被更新为指向副本:
K <-- br-A
/
...--G--H <-- main
\
I--J--K' <-- br-B (HEAD)
我们现在应该检查和测试新的提交,以确保我们真的喜欢结果(因为如果我们不喜欢,您可以在这里做很多事情)。但假设一切顺利,现在我们想 discard 在 br-A
.
末尾提交 K
我们实际上无法删除 提交K
。但是分支名称只是保存了我们想说的“在分支上”的最后一次提交的哈希 ID,我们可以更改存储在分支名称中的哈希 ID.
这里事情变得有点复杂,因为 Git 有两种不同的方法来做到这一点。使用哪一个取决于我们是否检查了那个特定的分支。
git reset
如果我们现在运行:
git switch br-A
得到:
K <-- br-A (HEAD)
/
...--G--H <-- main
\
I--J--K' <-- br-B
我们可以使用 git reset --hard
将提交 K
从当前分支的末尾删除。我们只需找到 previous 提交的哈希 ID,即哈希 ID H
。我们可以使用 git log
,然后是 cut-and-paste 哈希 ID,或者我们可以使用 Git 内置的一些特殊语法:
git reset --hard HEAD~
语法 HEAD~
的意思是:找到由 HEAD
命名的提交,然后返回到它的(首先也是唯一在这种情况下)parent。在此特定绘图中定位提交 H
。
重置命令然后将分支名称移动到指向此提交,并且——因为 --hard
——更新我们的工作树和 Git 的 index aka 暂存区匹配:
K ???
/
...--G--H <-- br-A (HEAD), main
\
I--J--K' <-- br-B
Commit K
不再有办法找到它,所以除非你告诉他们,否则没人会知道它在那里。
请注意,鉴于此特定绘图,我们也可以完成 git reset --hard main
。 HEAD~1
样式语法甚至在其他情况下也有效。
git branch -f
如果我们不先检查 br-A
,我们可以使用git branch -f
强制它后退一步。这与 git reset
具有相同的效果,但是因为我们没有按名称检查分支,所以我们不必担心我们的工作树和 Git 的 index/staging-area:
git branch -f br-A br-A~
在这里,我们使用名称 br-A
的波浪号后缀让 Git 后退一个 first-parent 跃点。效果是完全一样的,但是只有在还没有检出分支br-A
.
的情况下才能这样做
特例
假设我们上面的图纸不太正确。也就是说,假设分支 br-A
和 br-B
在我们提交 K
之前指向 不同的提交 ,它们都指向 相同的提交。例如,我们可能有:
...--G--H <-- main
\
I--J <-- br-A (HEAD), br-B
如果我们处于这种情况然后提交 K
,我们将得到:
...--G--H <-- main
\
I--J <-- br-B
\
K <-- br-A (HEAD)
请注意,在这种情况下,没有什么我们不喜欢提交K
:它有正确的快照和 它有正确的元数据。 唯一的问题是名称br-A
指向K
,br-B
指向J
。我们希望 br-B
指向 K
并且 br-A
指向 J
.
我们可以通过以下方式得到我们想要的:
- 移动两个分支名称,或
- 交换分支名称
我们可以用 git reset
和 git branch -f
的组合来做第一个。我们只需要注意不要丢失提交 K
的哈希 ID。
我们可以运行git log
剪切粘贴K
的hash ID,这样就不会丢了,然后运行:
git reset --hard HEAD~
得到:
...--G--H <-- main
\
I--J <-- br-A (HEAD), br-B
\
K ???
那么我们可以运行:
git branch -f br-B <hash-of-K>
粘贴正确的散列,得到:
...--G--H <-- main
\
I--J <-- br-A (HEAD)
\
K <-- br-B
例如。或者,与其采用那种稍微冒险的方法(如果我们不小心剪切了一些其他文本并丢失了哈希 ID 会怎样?),我们可以更新br-B
第一个,其中:
git branch -f br-B br-A
或:
git checkout br-B; git merge --ff-only br-A
(里面引入了--ff-only
合并的概念,这里不做解释)得到:
...--G--H <-- main
\
I--J
\
K <-- br-A, br-B
其中之一是当前分支。然后我们可以修复 br-A
使其向后移动一跳。
最后,我们可以使用“重命名两个分支”技巧。这需要临时取第三个名字:
git branch -m temp # rename br-A to temp
git branch -m br-B br-A # rename br-B to br-A
git branch -m br-B # rename temp to br-B
在所有这些情况下,无需复制任何提交,因为K
已经是正确的形式。我们只需要将 names 稍微打乱一下。
关键通常是画图
如果您对这些事情不确定,画图。
您可以让 Git 或其他程序为您绘制图形:请参阅 Pretty Git branch graphs。请注意,绘制和阅读图表需要一些练习,但这是一项重要技能,在 Git.
绘制图表后,您可以判断是否需要 新的和改进的提交——您可以使用 git cherry-pick
获得,也许 git rebase
—and/or你需要分支名称re-point.
这也让您深入了解我提到的警告。 当您将提交复制到 new-and-improved 时,任何已经拥有 old-and-lousy 的 Git 存储库 1 也需要更新。 因此,如果您使用 git push
来 发送 old-and-lousy 提交到其他 Git 存储库,请确保他们——无论谁”他们”——也愿意更新。如果你不能让它们切换,进行new-and-improved提交只会造成大量重复提交,因为他们会继续把旧的和糟糕的提交即使你一直把它们拿出来,也要回来。因此,如果您 发布了 一些提交,请确保他们——无论他们是谁——在你进行变基或其他任何事情之前同意切换到改进的提交。
1如果某些东西是 new-and-improved,这告诉您关于旧版本的什么信息?或许这里“烂”太过强烈,但至少让人回味无穷。
我有以下问题。我在一个分支(我们称之为 A)上工作,在那里我实现了一个新功能。我只提交了更改,但没有推送它们。现在我后来意识到我在错误的分支上。所以我换到了右边的分支(B)。如何将更改从分支 A 转移到分支 B?
所以在 B 中,到目前为止的所有内容都保留下来,而 A 中的所有新内容都存放在 B 中。
如果:
- 您做喜欢一些提交,但是
- 关于相同的提交,还有一些您不喜欢的东西
那么通常解决这个问题的正确方法是使用 git rebase
。关于 git rebase
总是有一个警告,我稍后会描述,但由于您还没有 发送 这些提交给一些 other Git 存储库 - 您想要以某种方式更改的提交完全属于您,仅存在于您自己的 Git 存储库中 - 此警告不适用于您的情况。
不过,在您的特定情况下,您根本不需要使用变基。您将改为使用 git cherry-pick
,然后使用 git reset
或 git branch -f
。或者,您甚至可能不需要执行 cherry-pick.
关于提交(和 Git 的一般知识)
Git 实际上就是 提交 。它与文件无关,尽管提交会 hold 文件。它也与分支无关,尽管分支名称可以帮助我们(和 Git)找到 提交。不过,最后,重要的只是提交。这意味着您需要了解有关提交的所有信息。
在Git中:
每个提交都是编号,有一个独特的,但又大又丑的random-looking,哈希ID 或 object ID。这些实际上根本不是随机的:数字是加密哈希函数的输出。每个 Git 使用相同的计算,因此宇宙中每个地方的每个 Git 都会同意某个特定的提交得到 那个数字 。没有其他提交可以有 那个数字,不管它是什么:那个数字现在被那个特定的提交用完了。由于数字必须是普遍唯一的,因此它们必须很大(因此很难看,人类无法使用)。
Git 将这些提交和其他支持提交的内部 object 存储在一个大数据库中 - key-value store - 其中哈希 ID 是键和提交(或其他 object)是值。你给 Git 密钥,例如,通过从
git log
输出剪切和粘贴,Git 可以找到提交并因此使用它。这通常不是我们实际使用 Git 的方式,但重要的是要知道:Git 需要密钥,即哈希 ID。每个提交存储两件事:
每次提交都会存储一个每个文件的完整快照,截至您提交时。它们以特殊的 read-only、Git-only、压缩和 de-duplicated 格式存储,而不是您计算机上的普通文件。根据您的 OS,Git 可能能够存储您的计算机实际上无法使用或提取的文件(例如,Windows 上名为
aux.h
的文件),这有时是个问题。 (你必须制作这些文件在OS上可以命名它们,当然,比如Linux。不过,所有这一切的目的只是为了表明这些文件 不是 常规文件。)每个提交还存储一些 元数据,或有关提交本身的信息:例如,谁创建的,何时创建的。元数据包括
git log
显示的日志消息。对于 Git 至关重要的是,每个提交的元数据都包含一个列表——通常只有一个条目长——包含 先前的提交哈希 ID.
由于 Git 使用的散列技巧,没有提交——没有任何类型的内部 object——一旦被存储就永远无法更改。 (这也是文件存储的工作方式,也是 Git de-duplicates 文件的方式,并且可以存储您的计算机无法存储的文件。它们都只是那个大数据库中的数据。)
同样,提交的元数据存储一些先前提交的哈希 ID。大多数提交在此列表中只有一个条目,并且该条目是此提交的 parent。这意味着 child 提交记住他们的 parents' 的名字,但是 parents 不记得他们的 children:parents 被及时冻结从他们制作的那一刻起,他们 children 的最终存在就无法添加到他们的记录中。但是当child人出生时,parent人就存在了,所以child可以保存它的parent提交号。
这一切意味着提交形式向后看链,其中最新提交指向next-to-latest,并且该提交指向另一个跃点,依此类推。也就是说,如果我们绘制一个小的提交链,其 last 提交具有散列 H
,我们得到:
... <-F <-G <-H
哈希为H
的提交保存所有文件的快照,加上元数据; H
的元数据让 Git 找到提交 G
,因为 H
指向它的 parent G
。 Commit G
依次保存所有文件和元数据的快照,并且G
的元数据指向 F
。这一直重复到第一次提交,这是第一次提交 - 不能向后指向。它有一个空的 parent 列表。
git log
程序因此只需要知道一个提交哈希ID,即H
。从那里,git log
可以显示 H
,然后向后移动一跳到 G
并显示 G
。从那里,它可以向后移动另一跳到 F
,依此类推。当您厌倦阅读 git log
输出并退出程序时,或者当它一路返回到第一次提交时,该操作将停止。
分支名称帮助我们找到提交
这里的问题是我们仍然需要以某种方式记住链中最后一个提交 H
的哈希 ID。我们可以把它记在白板上、纸上或其他东西上——但我们有一台 计算机 。为什么不让 computer 为我们保存哈希 ID?这就是 分支名称 的意义所在。
每个分支名称,在Git中,只保存一个哈希ID。无论分支名称中的哈希 ID 是什么,我们都说该名称 指向 该提交,并且该提交是该分支的 尖端提交 .所以:
...--F--G--H <-- main
这里我们有分支名称 main
指向提交 H
。我们不再需要记住哈希 ID H
:我们只需输入 main
即可。 Git 将使用名称 main
查找 H
,然后使用 H
查找 G
,然后使用 G
查找 F
,等等。
一旦我们这样做了,我们就有了一个简单的方法来添加新的提交:我们只需做一个新的提交,比如I
,这样它就指向后面到 H
,然后 将 I
的哈希 ID 写入名称 main
,如下所示:
...--F--G--H--I <-- main
或者,如果我们不想更改我们的名字 main
,我们可以创建一个 新名字,例如 develop
或 br1
:
...--F--G--H <-- br1, main
现在我们有多个 name,我们需要知道我们使用哪一个来查找提交 H
,所以我们将绘制特殊名称 HEAD
,附加到分支名称之一,以表明:
...--F--G--H <-- br1, main (HEAD)
这里我们通过名称 main
使用提交 H
。如果我们 运行:
git switch br1
我们得到:
...--F--G--H <-- br1 (HEAD), main
没有其他变化——Git 注意到我们正在“从 H
移动到 H
”,可以说是——所以 Git 需要一些 short-cuts 并且不会为这种情况做任何其他工作。但现在我们是 on branch br1
,正如 git status
所说。现在,当我们进行新提交时 I
,我们将得到:
I <-- br1 (HEAD)
/
...--F--G--H <-- main
名称 main
留在原地,而名称 br1
移至指向新提交 I
。
您描述的情况
I was working on a branch (let's call it A) where I implemented a new function. I have only committed the changes, but I did not push them. Now I realized later that I am on the wrong branch. So I changed to the right branch (B). How can I transfer the changes from branch A to branch B?
让我们画这个:
...--G--H <-- br-A (HEAD), main
\
I--J <-- br-B
你是 on branch br-A
并且做了一个新的提交,我们称之为 K
:
K <-- br-A (HEAD)
/
...--G--H <-- main
\
I--J <-- br-B
关于提交 K
,您 做 有一些事情喜欢:例如,它的快照与提交 H
中的快照不同,无论您如何更改制作。它的日志消息也说明了您希望日志消息说明的内容。
但是有一件事你不喜欢提交K
:它发生在提交H
之后,当你想要它在提交 J
.
您不能更改提交
我们在靠近顶部的位置注意到,一旦提交,就无法更改。您现有的提交 K
是一成不变的:没有人,没有任何东西,甚至 Git 本身也不能更改关于提交 K
的 任何东西。它在 H
之后,它有快照和日志消息,并且永远如此。
但是...如果我们可以复制 K
到一个新的改进的提交呢?我们称此 new-and-improved 提交 K'
,表明它是 K
的 副本 ,但有一些不同之处。
应该有什么不同?好吧,一方面,我们希望它在 J
之后出现。然后我们希望它对 K
对 H
所做的 更改 与 J
相同。也就是说,如果我们问 H
-vs-K
快照有什么不同,然后问 J
-vs-K'
快照有什么不同制作,我们希望获得 相同的更改。
有一个相当低级别的 Git 命令可以像这样精确地复制一个提交,称为 git cherry-pick
。这实际上就是我们最终要使用的。
不过,这里还是要说一下git rebase
。如果我们有十几个或一百个要复制的提交,cherry-pick 对每个进行复制可能会很乏味; git rebase
也会自动执行重复的 cherry-picking。所以 rebase 是 usual 使用的命令。
rebase 的工作原理如下:
- 首先,我们Git列出了它需要复制的所有提交。在这种情况下,只需提交
K
. - 然后,我们 Git 签出 (切换到)我们所在的提交希望副本 go。在这种情况下,提交
J
. - 接下来,我们Git从它创建的列表中一次复制每个提交。
- 然后我们 Git 获取 分支名称 找到要复制的 last 提交,然后移动它指向 last-copied 提交的名称。
所有这一切的最终结果,在这种情况下,是:
K ???
/
...--G--H <-- main
\
I--J <-- br-B
\
K' <-- br-A (HEAD)
请注意提交 K
仍然存在。只是再也没有人能找到它了。名称 br-A
现在找到 copy,提交 K'
.
Cherry-picking
这不是我们想要的,所以我们不使用 git rebase
,而是使用 git cherry-pick
。我们先运行:
git switch br-B
得到:
K <-- br-A
/
...--G--H <-- main
\
I--J <-- br-B (HEAD)
现在我们将 运行:
git cherry-pick br-A
这个用名字br-A
找到commitK
,然后复制到我们现在所在的地方。也就是说,我们得到了一个新的提交,它进行了 与提交 K
相同的更改 ,并且具有 相同的日志消息 。这个提交在我们现在所在的分支上进行,所以 br-B
被更新为指向副本:
K <-- br-A
/
...--G--H <-- main
\
I--J--K' <-- br-B (HEAD)
我们现在应该检查和测试新的提交,以确保我们真的喜欢结果(因为如果我们不喜欢,您可以在这里做很多事情)。但假设一切顺利,现在我们想 discard 在 br-A
.
K
我们实际上无法删除 提交K
。但是分支名称只是保存了我们想说的“在分支上”的最后一次提交的哈希 ID,我们可以更改存储在分支名称中的哈希 ID.
这里事情变得有点复杂,因为 Git 有两种不同的方法来做到这一点。使用哪一个取决于我们是否检查了那个特定的分支。
git reset
如果我们现在运行:
git switch br-A
得到:
K <-- br-A (HEAD)
/
...--G--H <-- main
\
I--J--K' <-- br-B
我们可以使用 git reset --hard
将提交 K
从当前分支的末尾删除。我们只需找到 previous 提交的哈希 ID,即哈希 ID H
。我们可以使用 git log
,然后是 cut-and-paste 哈希 ID,或者我们可以使用 Git 内置的一些特殊语法:
git reset --hard HEAD~
语法 HEAD~
的意思是:找到由 HEAD
命名的提交,然后返回到它的(首先也是唯一在这种情况下)parent。在此特定绘图中定位提交 H
。
重置命令然后将分支名称移动到指向此提交,并且——因为 --hard
——更新我们的工作树和 Git 的 index aka 暂存区匹配:
K ???
/
...--G--H <-- br-A (HEAD), main
\
I--J--K' <-- br-B
Commit K
不再有办法找到它,所以除非你告诉他们,否则没人会知道它在那里。
请注意,鉴于此特定绘图,我们也可以完成 git reset --hard main
。 HEAD~1
样式语法甚至在其他情况下也有效。
git branch -f
如果我们不先检查 br-A
,我们可以使用git branch -f
强制它后退一步。这与 git reset
具有相同的效果,但是因为我们没有按名称检查分支,所以我们不必担心我们的工作树和 Git 的 index/staging-area:
git branch -f br-A br-A~
在这里,我们使用名称 br-A
的波浪号后缀让 Git 后退一个 first-parent 跃点。效果是完全一样的,但是只有在还没有检出分支br-A
.
特例
假设我们上面的图纸不太正确。也就是说,假设分支 br-A
和 br-B
在我们提交 K
之前指向 不同的提交 ,它们都指向 相同的提交。例如,我们可能有:
...--G--H <-- main
\
I--J <-- br-A (HEAD), br-B
如果我们处于这种情况然后提交 K
,我们将得到:
...--G--H <-- main
\
I--J <-- br-B
\
K <-- br-A (HEAD)
请注意,在这种情况下,没有什么我们不喜欢提交K
:它有正确的快照和 它有正确的元数据。 唯一的问题是名称br-A
指向K
,br-B
指向J
。我们希望 br-B
指向 K
并且 br-A
指向 J
.
我们可以通过以下方式得到我们想要的:
- 移动两个分支名称,或
- 交换分支名称
我们可以用 git reset
和 git branch -f
的组合来做第一个。我们只需要注意不要丢失提交 K
的哈希 ID。
我们可以运行git log
剪切粘贴K
的hash ID,这样就不会丢了,然后运行:
git reset --hard HEAD~
得到:
...--G--H <-- main
\
I--J <-- br-A (HEAD), br-B
\
K ???
那么我们可以运行:
git branch -f br-B <hash-of-K>
粘贴正确的散列,得到:
...--G--H <-- main
\
I--J <-- br-A (HEAD)
\
K <-- br-B
例如。或者,与其采用那种稍微冒险的方法(如果我们不小心剪切了一些其他文本并丢失了哈希 ID 会怎样?),我们可以更新br-B
第一个,其中:
git branch -f br-B br-A
或:
git checkout br-B; git merge --ff-only br-A
(里面引入了--ff-only
合并的概念,这里不做解释)得到:
...--G--H <-- main
\
I--J
\
K <-- br-A, br-B
其中之一是当前分支。然后我们可以修复 br-A
使其向后移动一跳。
最后,我们可以使用“重命名两个分支”技巧。这需要临时取第三个名字:
git branch -m temp # rename br-A to temp
git branch -m br-B br-A # rename br-B to br-A
git branch -m br-B # rename temp to br-B
在所有这些情况下,无需复制任何提交,因为K
已经是正确的形式。我们只需要将 names 稍微打乱一下。
关键通常是画图
如果您对这些事情不确定,画图。
您可以让 Git 或其他程序为您绘制图形:请参阅 Pretty Git branch graphs。请注意,绘制和阅读图表需要一些练习,但这是一项重要技能,在 Git.
绘制图表后,您可以判断是否需要 新的和改进的提交——您可以使用 git cherry-pick
获得,也许 git rebase
—and/or你需要分支名称re-point.
这也让您深入了解我提到的警告。 当您将提交复制到 new-and-improved 时,任何已经拥有 old-and-lousy 的 Git 存储库 1 也需要更新。 因此,如果您使用 git push
来 发送 old-and-lousy 提交到其他 Git 存储库,请确保他们——无论谁”他们”——也愿意更新。如果你不能让它们切换,进行new-and-improved提交只会造成大量重复提交,因为他们会继续把旧的和糟糕的提交即使你一直把它们拿出来,也要回来。因此,如果您 发布了 一些提交,请确保他们——无论他们是谁——在你进行变基或其他任何事情之前同意切换到改进的提交。
1如果某些东西是 new-and-improved,这告诉您关于旧版本的什么信息?或许这里“烂”太过强烈,但至少让人回味无穷。