Git 从历史记录中删除合并提交,但保留与其关联的提交
Git remove merge commit from history, but retain the commits with which it has been connected
我有历史上最后 6 次提交的以下结构,都在同一个分支中:
A(merge commit)
|
|\
B \
| |
| C
| |
| D
| |
| /
|/
E
|
F
我想删除 A 合并提交,但我想在 B 提交之前将 C 和 D 提交保持在线性历史记录中。我提到所有提交都被推送到远程。我尝试 reset --soft HEAD~1
删除 A 合并提交,但使用该命令其他提交 C 和 D 也已被删除。我还想删除最后一个合并提交中的新更改,我想在 B 提交中传输该修改,因为它将添加到与 B 提交中相同的文件中。
我想要的最终历史:
B
|
|
C
|
|
D
|
|
E
如果您没有任何正在进行的工作,我会简单地做:
git switch mybranch # commit A
git reset --hard C
git cherry-pick B
这样,您将在新的 'mybranch' HEAD C
.
之上重新创建 B
之后将需要一个 git push --force
(如果之前已推送该分支),因此,如果您不是一个人在该分支上工作,请务必通知您的同事。
TL;DR
使用 git reset --soft
(正如您所做的那样,但使用不同的目标,HEAD^2
或提交的原始哈希 ID C
),然后使用 git commit
。您的 git commit
可能需要一两个额外的选项。有关详细信息,请参阅详细答案。
(还要注意,你需要 git push --force
,就像 中那样。我怀疑他在你提到你在提交 A
中也有修复之前写了那个答案.)
长
让我们更正一些 statements-of-fact 是......好吧,它们在 微妙的 方面是错误的。就您 所看到的 .
而言,它们是正确的
I try to reset --soft HEAD~1
to delete the A merge commit but with that command the other commits, C and D have been also deleted.
实际情况并非如此。提交尚未删除。他们只是变得很难找到。原因很简单:Git 实际上可以 向后.
让我 re-draw 你的水平提交顺序,我更喜欢 Whosebug 帖子的方式,左边是旧的提交,右边是新的提交。这给了我这张图:
...--F--E---B--A <-- somebranch (HEAD)
\ /
D--C
其中,根据重置的结果,我们看到 B
是 A
的第一个父级。 运行 git log
此时会:
- 显示提交
A
;然后
- 显示提交
B
因为 A
link 回到 B
;然后
- 显示提交
C
因为 A
link 回到 C
;然后
- 显示提交
D
因为 C
link 回到 D
;然后
- show commit
E
因为 B
和 D
link 回到 E
等等。显示 B
、C
和 D
的精确顺序取决于您为 git log
提供的任何 commit-sorting 选项:--topo-order
强制生成一个合理的图表order,例如,while --author-date
order 使用作者日期和时间戳。默认是使用提交者日期和时间戳,在 less-recent 提交之前看到最近的提交。
当我们进行重置时,我们得到了这个。由于我绘制图表的方式,我需要将 B
向上移动一条线,但 A
仍然 link 回到 B
和 C
两者:
B___ <-- somebranch (HEAD)
/ \
...--F--E A
\ /
D--C
也就是说,在 git reset --soft HEAD~1
之后,name somebranch
现在选择提交 B
而不是提交 A
。
因为Git 向后工作,我们不再看到 提交A
、C
和D
。 git log
操作以 commit B
开始,并显示它; B
然后 link 回到 E
,所以 git log
移动到 E
并显示它; E
link 回到 F
所以我们看到 F
,等等。我们从来没有机会将 forward 移动到 D
、C
或 A
:这根本不可能,因为 Git 有效 向后.
The final history I want to have:
E--D--C--B <-- somebranch (HEAD)
现在,事实上,提交 B
——B
代表一些丑陋的大哈希 ID——连接回提交 E
。情况总是如此:根本无法更改现有的提交。所以这段历史是不可能的。但是,我们可以创建一个 new 提交 B'
,它很像 B
,但又不同。
Also I have a new change in the last merge commit I want to delete and I want to transfer that modification in the B commit ...
当我们进行新的 B'
提交时 -B
-but-different,我们也可以这样做。
侧边栏:更多关于提交以及 Git 如何进行的信息
Git 中的每个提交都包含两个部分:
每个提交都有一个 每个文件的完整快照,Git 在您(或任何人)进行提交时知道 。这些快照存储文件,但与您的计算机存储它们的方式不同。相反,它们的名称和内容存储为内部 Git 对象,并且这些对象被压缩和 de-duplicated(并且一直冻结)。 de-duplication 意味着如果您有一系列提交 C1
、C2
、C3
,每个提交都有数千个文件,但只有 一个 文件实际上 更改 在这些提交中,数千个文件都是 共享的 。每个新提交只有一个 new 文件。即使那样,新数据也会以各种方式进行压缩和 Git-ified,这可能会将一个大文件变成一个很小的增量(最终——这发生在游戏的后期,在 Git,因为你会得到更好的增量那样)。
每个提交还存储一些元数据,或有关提交本身的信息。这包括作者和提交者信息:提交者和提交时间。它包含一条日志消息:如果您要提交,则可以自己编写。而且——对于 Git 自己的目的来说,所有这些都很重要——提交包括原始哈希 ID,那些像 225365fb5195e804274ab569ac3cc4919451dc7f
这样的丑陋的大字符串,对于每个提交的 parents .对于大多数提交,这只是一个较早的提交;对于像您的提交 A
这样的合并提交,这是两个提交哈希 ID 的列表(对于 B
和 C
,按此顺序)。
新提交中的元数据来自您的 user.name
和 user.email
设置——因为那是您的姓名和电子邮件地址所在的位置——以及 frm 信息 Git 可以立即找到,例如,存储在您计算机时钟中的当前日期和时间。 (如果时钟不对,提交的 date-and-time-stamps 也会出错。没什么大不了的,它们只是用来混淆人类。)新的 parent提交是...当前提交,作为pointed-to当前b运行ch名称。
因此,如果我们希望新提交 B'
指向现有提交 C
,我们需要提交 C
——而不是提交 B
,而不是提交 E
——成为当前提交。为了实现这一点,我们需要使 name somebranch
指向提交 C
.
有很多方法可以在 Git 中移动 b运行ch 名称,但我们在这里使用的是 git reset
。 git reset
命令又大又复杂,其中一个复杂之处在于它可以重置 Git 的 index。所以让我们提一下指数。
索引——Git也称为暂存区,指的是你如何使用它,有时也称为缓存,虽然现在主要是在 --cached
之类的标志中,如 git rm --cached
或 git diff --cached
——是 Git 获取 文件的地方 放入一个新的提交。换句话说,索引保存了新提交的 建议快照 。当您进行新提交时,该新提交将同时具有元数据和快照,而快照来自 Git 的索引。
当我们将索引描述为暂存区时,我们谈论的是我们如何更改工作树文件,然后使用git add
复制它们进入 集结区。这没有错,但这张图片并不完整:它表明停靠区一开始是空的,然后逐渐填满。但实际上,它 开始时满是文件 。只是它充满的文件与提交和工作树中的 相同 文件。
当你 运行 git status
并且它说,例如:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Makefile
这并不意味着 只有 Makefile
会进入下一个快照。事实上,每个文件 都将进入下一个快照。但是 Git 的索引 / staging-area 中的 Makefile
现在 不同于 Makefile
在 HEAD
提交 现在 .
如果我现在 运行 git diff --cached
(或 git diff --staged
,完全一样),我得到:
diff --git a/Makefile b/Makefile
index 9b1bde2e0e..5d0b1b5f31 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
+foo
# The default target of this Makefile is...
all::
我在 Makefile
和 运行 git add Makefile
的前面放了一些伪造的东西才能到达这里,这意味着我 Git 踢掉了现有的 HEAD
-从索引中提交 Makefile
的副本,并放入 Makefile
的现有 working-tree 副本。这就是行 foo
的来源。
如果我使用 git restore --staged Makefile
,正如 Git 此处建议的那样, 会将 HEAD:Makefile
复制到 :Makefile
。这里的 colon-prefix 语法特定于某些 Git 操作(例如 git show
)并允许您读取 Git 中的文件副本。我的 工作树 中 Makefile
的副本不在 Git 中 ,因此没有特殊的语法: 这只是一个普通的普通文件。但是对于一些 Git 命令,有一个特殊的语法,带有这个冒号的东西。例如,使用 git show HEAD:Makefile
查看 committed 副本,使用 git show :Makefile
查看 index 副本。
无论如何,我现在听从Git的建议:
$ git restore --staged Makefile
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Makefile
no changes added to commit (use "git add" and/or "git commit -a")
我运行将Makefile
的HEAD
副本复制到索引/staging-area中的git restore --staged
。所以现在这两个 相同 ,git status
并没有说他们是 staged for commit
。但是现在索引Makefile
和我的工作树Makefile
不一样了,所以现在git status
说这两个不一样
关于git reset
我在这里使用的git restore
命令是new-ish,已在Git 2.23中引入。 git reset
命令要老得多。这是一个大而复杂的命令,所以我们只会看看我们可以使用它的一部分方法。
当用作:
git reset --soft HEAD~1
例如,这种git reset
移动当前b运行ch名称。也就是我们这样取图:
B___
/ \
...--F--E A <-- somebranch (HEAD)
\ /
D--C
并移动 somebranch
使其指向 B
,像这样:
B___ <-- somebranch (HEAD)
/ \
...--F--E A
\ /
D--C
没有提交 更改。没有提交 可以 更改。
如果我们使用git reset --mixed
,我们会Git移动b运行ch名称和改变所有的Git 索引中的文件副本。如果我们要使用 git reset --hard
,我们会 Git 移动 b运行ch 名称,更改 Git 索引中的文件副本,和替换我们工作树中文件的副本。所以这个 kind of git reset
最多做三件事:
移动我们的HEAD
。使用我们给出的参数和 git rev-parse
/ gitrevisions 中的规则,找到一些提交。无论我们使用什么 b运行ch 名称——如果 git status
说 on branch somebranch
,那就是somebranch
—使该名称指向该提交的哈希 ID。
如果--soft
,停止!否则,继续...
替换 Git 索引中的所有文件。替换文件来自我们在步骤 1 中选择的提交。
如果--mixed
或没有选项,停止!否则(--hard
),继续...
按照步骤 2 中替换索引文件的方式替换工作树文件。
如果你已经完成了所有这些,你可以看到 git reset --mixed
和 git reset --hard
可以,如果我们选择 当前提交 作为new commit, just reset the index, or reset the index and replace the working-tree文件。如果我们不给 git reset
一个特定的提交哈希 ID 或名称或相关指令,如 HEAD~1
或 HEAD^2
,git reset
使用 HEAD
。所以 git reset --soft HEAD
或 git reset --soft
只是一种什么都不做的方法,但是 git reset HEAD
或 git reset
是一种清除 Git 索引的方法,使得它再次匹配 HEAD
。 (你不想这样做——我只是在这里注明,以便你可以对 git reset
的行为有一个正确的心理模型。)
关于git commit
当你 运行 git commit
, Git:
- 收集任何必要的元数据,包括日志消息;
- 添加适当的父提交哈希 ID:通常只是
HEAD
,但如果您要提交合并,HEAD
加上更多;
- 将Git索引中的任何内容打包为新快照;
- 将所有这些作为新提交写出,它获得一个新的、唯一的哈希 ID;和
- 将 new 哈希 ID 写入 b运行ch name.
最后一步是我们如何从:
...--F <-- somebranch (HEAD)
至:
...--F--E <-- somebranch (HEAD)
例如,很久以前。你做了 git checkout somebranch
或 git switch somebranch
。那:
- 选择提交
F
,因为 somebranch
指向提交 F
;
- 填写Git的索引来自提交;
- 从提交中填写你的工作树(现在在 Git 的索引中表示);和
- 将名称
HEAD
附加到名称 somebranch
,以便 Git 知道将来的提交应该写入 somebranch
.
然后你修改了一些文件和运行 git add
。这将任何更新的文件 复制到 Git 的索引中,准备提交。索引继续持有提议的下一次提交(或快照部分),git add
改变提议的快照,通过弹出一些current 索引文件并放入新的(更新的)文件。它实际上是执行所有 Git-ifying 文件的 git add
步骤,使它们准备好提交。
最后,你运行git commit
。这打包了所有文件的索引副本,以制作新的快照。它添加了正确的元数据。它进行了提交,获得了 Git 提交 E
的哈希 ID。 (这也将提交 E
放入 Git 的 all-the-commits-and-other-objects 数据库。)最后,它将 E
的哈希 ID 写入 name somebranch
,现在你有:
...--F--E <-- somebranch (HEAD)
与当前提交和 Git 的索引再次匹配。如果您 git add
-ed all 您更新的文件、提交、索引和您的工作树都匹配。如果你只 git add
-ed selected 文件,你仍然有一些与提交不匹配的工作树文件,你可以 git add
它们并制作另一个提交。
你现在在哪里
与此同时,我们现在处于这种状态:
B___
/ \
...--F--E A <-- somebranch (HEAD)
\ /
D--C
提交 B
在某种意义上是 不好的 。你不想提交 B
。它会保留很长一段时间——从你制作它起至少 30 天——即使在我们设置好之后你不能 see commit B
, 但没关系,Git 会 最终 在它闲置太久未使用时清除它。
这意味着提交 A
也很糟糕,因为提交 A
永久 link 回到提交 B
。 (A
link 也回到 C
,但是 C
没问题。)任何现有提交的任何部分都不能更改,因此放弃 B
,我们也不得不放弃A
。
所以:让我们使用 git reset
移动 somebranch
,以便 somebranch
找到提交 C
。我们可以在这里使用三个重置选项中的任何一个,但其中一个选项使事情变得简单:
如果我们使用git reset --soft
,索引保持不变。 Git 的索引当前与合并提交 A
中的快照匹配。 这是您说要保留的快照。
如果我们使用--mixed
或--hard
,Git将清空其索引并从提交C
填充它。这并不可怕——我们想要的文件仍在提交中 A
——但它显然没有那么有用。
所以让我们运行 git reset --soft <em>hash-of-C[=512=
。或者,因为 current 提交是提交 A
,我们可以使用 HEAD^2
。如果我们查看 the gitrevisions documentation,我们会发现 HEAD^2
表示 当前提交的第二个父级 。那将是提交 C
。 请注意,我们需要立即提交 A
才能在 Git 的索引中包含正确的内容,所以如果我们 not on commit A
此时,我们最好先检查一下。
最终结果是这样的:
B___
/ \
...--F--E A
\ /
D--C <-- somebranch (HEAD)
一旦我们有了这个,我们就可以 运行 git commit
。 Git 将使用 Git 索引中的任何内容——感谢 --soft
和我们之前在 A
的位置,这是来自提交 [=37= 的文件集]——进行新的提交。我们将调用新提交 B'
;让我们把它画进去:
B___
/ \
...--F--E A
\ /
D--C--B' <-- somebranch (HEAD)
无法看到提交 A
。没有 name (b运行ch name) 可以找到它。我们可以 运行 git log
并给它 A
的原始哈希 ID,然后 那 会找到提交 A
,但我们可以否则就会看到它。所以让我们更新我们的绘图,就好像没有提交 A
。由于 A
是找到 B
的唯一方法,因此我们也将 B
排除在外:
...--F--E--D--C--B' <-- somebranch (HEAD)
所以我们最后的命令序列是:
git checkout somebranch # if necessary
git log --decorate --oneline --graph # make sure everything is as expected
git reset --soft HEAD^2
git commit
关于 HEAD^2
的注释:当心 DOS/Windows CLI 吃掉 ^
个字符。您可能必须使用 HEAD^^2
、引号或其他东西来保护 ^
.
最后一次优化
当您 运行 git commit
时,Git 将需要一条日志消息。如果现有提交 B
中的日志消息是好的并且你想要 re-use 它,你可以告诉 Git 这样做。 git commit
命令有一个 -c
或 -C
选项。 运行:
git commit -C <hash-of-B>
将从提交 B
中获取提交消息并使用它。您不会被扔进编辑器来提出提交消息。
如果 B
中的提交消息可以 改进 ,您可能 想要 被扔到您的编辑器中。为此,请添加 --edit
,或将大写 -C
更改为小写 -c
:
git commit --edit -C <hash-of-B>
或:
git commit -c <hash-of-B>
请注意,在 git reset
之后,很难找到 B
的哈希值,因此您可能需要保存它。 Git 的 reflogs 有一个技巧来获取它,不过:somebranch@{1}
是重置前 somebranch
的旧值,因此:
git commit -c somebranch@{1}~1
会起作用。不过,我通常发现使用 git log
然后用鼠标剪切和粘贴原始哈希 ID 比输入复杂的 <em>name</em>@ 更容易{<em>number</em>}~<em>number</em>^<em>number</em>
个表达式.
我有历史上最后 6 次提交的以下结构,都在同一个分支中:
A(merge commit)
|
|\
B \
| |
| C
| |
| D
| |
| /
|/
E
|
F
我想删除 A 合并提交,但我想在 B 提交之前将 C 和 D 提交保持在线性历史记录中。我提到所有提交都被推送到远程。我尝试 reset --soft HEAD~1
删除 A 合并提交,但使用该命令其他提交 C 和 D 也已被删除。我还想删除最后一个合并提交中的新更改,我想在 B 提交中传输该修改,因为它将添加到与 B 提交中相同的文件中。
我想要的最终历史:
B
|
|
C
|
|
D
|
|
E
如果您没有任何正在进行的工作,我会简单地做:
git switch mybranch # commit A
git reset --hard C
git cherry-pick B
这样,您将在新的 'mybranch' HEAD C
.
之上重新创建 B
之后将需要一个 git push --force
(如果之前已推送该分支),因此,如果您不是一个人在该分支上工作,请务必通知您的同事。
TL;DR
使用 git reset --soft
(正如您所做的那样,但使用不同的目标,HEAD^2
或提交的原始哈希 ID C
),然后使用 git commit
。您的 git commit
可能需要一两个额外的选项。有关详细信息,请参阅详细答案。
(还要注意,你需要 git push --force
,就像 A
中也有修复之前写了那个答案.)
长
让我们更正一些 statements-of-fact 是......好吧,它们在 微妙的 方面是错误的。就您 所看到的 .
而言,它们是正确的I try to
reset --soft HEAD~1
to delete the A merge commit but with that command the other commits, C and D have been also deleted.
实际情况并非如此。提交尚未删除。他们只是变得很难找到。原因很简单:Git 实际上可以 向后.
让我 re-draw 你的水平提交顺序,我更喜欢 Whosebug 帖子的方式,左边是旧的提交,右边是新的提交。这给了我这张图:
...--F--E---B--A <-- somebranch (HEAD)
\ /
D--C
其中,根据重置的结果,我们看到 B
是 A
的第一个父级。 运行 git log
此时会:
- 显示提交
A
;然后 - 显示提交
B
因为A
link 回到B
;然后 - 显示提交
C
因为A
link 回到C
;然后 - 显示提交
D
因为C
link 回到D
;然后 - show commit
E
因为B
和D
link 回到E
等等。显示 B
、C
和 D
的精确顺序取决于您为 git log
提供的任何 commit-sorting 选项:--topo-order
强制生成一个合理的图表order,例如,while --author-date
order 使用作者日期和时间戳。默认是使用提交者日期和时间戳,在 less-recent 提交之前看到最近的提交。
当我们进行重置时,我们得到了这个。由于我绘制图表的方式,我需要将 B
向上移动一条线,但 A
仍然 link 回到 B
和 C
两者:
B___ <-- somebranch (HEAD)
/ \
...--F--E A
\ /
D--C
也就是说,在 git reset --soft HEAD~1
之后,name somebranch
现在选择提交 B
而不是提交 A
。
因为Git 向后工作,我们不再看到 提交A
、C
和D
。 git log
操作以 commit B
开始,并显示它; B
然后 link 回到 E
,所以 git log
移动到 E
并显示它; E
link 回到 F
所以我们看到 F
,等等。我们从来没有机会将 forward 移动到 D
、C
或 A
:这根本不可能,因为 Git 有效 向后.
The final history I want to have:
E--D--C--B <-- somebranch (HEAD)
现在,事实上,提交 B
——B
代表一些丑陋的大哈希 ID——连接回提交 E
。情况总是如此:根本无法更改现有的提交。所以这段历史是不可能的。但是,我们可以创建一个 new 提交 B'
,它很像 B
,但又不同。
Also I have a new change in the last merge commit I want to delete and I want to transfer that modification in the B commit ...
当我们进行新的 B'
提交时 -B
-but-different,我们也可以这样做。
侧边栏:更多关于提交以及 Git 如何进行的信息
Git 中的每个提交都包含两个部分:
每个提交都有一个 每个文件的完整快照,Git 在您(或任何人)进行提交时知道 。这些快照存储文件,但与您的计算机存储它们的方式不同。相反,它们的名称和内容存储为内部 Git 对象,并且这些对象被压缩和 de-duplicated(并且一直冻结)。 de-duplication 意味着如果您有一系列提交
C1
、C2
、C3
,每个提交都有数千个文件,但只有 一个 文件实际上 更改 在这些提交中,数千个文件都是 共享的 。每个新提交只有一个 new 文件。即使那样,新数据也会以各种方式进行压缩和 Git-ified,这可能会将一个大文件变成一个很小的增量(最终——这发生在游戏的后期,在 Git,因为你会得到更好的增量那样)。每个提交还存储一些元数据,或有关提交本身的信息。这包括作者和提交者信息:提交者和提交时间。它包含一条日志消息:如果您要提交,则可以自己编写。而且——对于 Git 自己的目的来说,所有这些都很重要——提交包括原始哈希 ID,那些像
225365fb5195e804274ab569ac3cc4919451dc7f
这样的丑陋的大字符串,对于每个提交的 parents .对于大多数提交,这只是一个较早的提交;对于像您的提交A
这样的合并提交,这是两个提交哈希 ID 的列表(对于B
和C
,按此顺序)。
新提交中的元数据来自您的 user.name
和 user.email
设置——因为那是您的姓名和电子邮件地址所在的位置——以及 frm 信息 Git 可以立即找到,例如,存储在您计算机时钟中的当前日期和时间。 (如果时钟不对,提交的 date-and-time-stamps 也会出错。没什么大不了的,它们只是用来混淆人类。)新的 parent提交是...当前提交,作为pointed-to当前b运行ch名称。
因此,如果我们希望新提交 B'
指向现有提交 C
,我们需要提交 C
——而不是提交 B
,而不是提交 E
——成为当前提交。为了实现这一点,我们需要使 name somebranch
指向提交 C
.
有很多方法可以在 Git 中移动 b运行ch 名称,但我们在这里使用的是 git reset
。 git reset
命令又大又复杂,其中一个复杂之处在于它可以重置 Git 的 index。所以让我们提一下指数。
索引——Git也称为暂存区,指的是你如何使用它,有时也称为缓存,虽然现在主要是在 --cached
之类的标志中,如 git rm --cached
或 git diff --cached
——是 Git 获取 文件的地方 放入一个新的提交。换句话说,索引保存了新提交的 建议快照 。当您进行新提交时,该新提交将同时具有元数据和快照,而快照来自 Git 的索引。
当我们将索引描述为暂存区时,我们谈论的是我们如何更改工作树文件,然后使用git add
复制它们进入 集结区。这没有错,但这张图片并不完整:它表明停靠区一开始是空的,然后逐渐填满。但实际上,它 开始时满是文件 。只是它充满的文件与提交和工作树中的 相同 文件。
当你 运行 git status
并且它说,例如:
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Makefile
这并不意味着 只有 Makefile
会进入下一个快照。事实上,每个文件 都将进入下一个快照。但是 Git 的索引 / staging-area 中的 Makefile
现在 不同于 Makefile
在 HEAD
提交 现在 .
如果我现在 运行 git diff --cached
(或 git diff --staged
,完全一样),我得到:
diff --git a/Makefile b/Makefile
index 9b1bde2e0e..5d0b1b5f31 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,4 @@
+foo
# The default target of this Makefile is...
all::
我在 Makefile
和 运行 git add Makefile
的前面放了一些伪造的东西才能到达这里,这意味着我 Git 踢掉了现有的 HEAD
-从索引中提交 Makefile
的副本,并放入 Makefile
的现有 working-tree 副本。这就是行 foo
的来源。
如果我使用 git restore --staged Makefile
,正如 Git 此处建议的那样, 会将 HEAD:Makefile
复制到 :Makefile
。这里的 colon-prefix 语法特定于某些 Git 操作(例如 git show
)并允许您读取 Git 中的文件副本。我的 工作树 中 Makefile
的副本不在 Git 中 ,因此没有特殊的语法: 这只是一个普通的普通文件。但是对于一些 Git 命令,有一个特殊的语法,带有这个冒号的东西。例如,使用 git show HEAD:Makefile
查看 committed 副本,使用 git show :Makefile
查看 index 副本。
无论如何,我现在听从Git的建议:
$ git restore --staged Makefile
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: Makefile
no changes added to commit (use "git add" and/or "git commit -a")
我运行将Makefile
的HEAD
副本复制到索引/staging-area中的git restore --staged
。所以现在这两个 相同 ,git status
并没有说他们是 staged for commit
。但是现在索引Makefile
和我的工作树Makefile
不一样了,所以现在git status
说这两个不一样
关于git reset
我在这里使用的git restore
命令是new-ish,已在Git 2.23中引入。 git reset
命令要老得多。这是一个大而复杂的命令,所以我们只会看看我们可以使用它的一部分方法。
当用作:
git reset --soft HEAD~1
例如,这种git reset
移动当前b运行ch名称。也就是我们这样取图:
B___
/ \
...--F--E A <-- somebranch (HEAD)
\ /
D--C
并移动 somebranch
使其指向 B
,像这样:
B___ <-- somebranch (HEAD)
/ \
...--F--E A
\ /
D--C
没有提交 更改。没有提交 可以 更改。
如果我们使用git reset --mixed
,我们会Git移动b运行ch名称和改变所有的Git 索引中的文件副本。如果我们要使用 git reset --hard
,我们会 Git 移动 b运行ch 名称,更改 Git 索引中的文件副本,和替换我们工作树中文件的副本。所以这个 kind of git reset
最多做三件事:
移动我们的
HEAD
。使用我们给出的参数和git rev-parse
/ gitrevisions 中的规则,找到一些提交。无论我们使用什么 b运行ch 名称——如果git status
说on branch somebranch
,那就是somebranch
—使该名称指向该提交的哈希 ID。如果
--soft
,停止!否则,继续...替换 Git 索引中的所有文件。替换文件来自我们在步骤 1 中选择的提交。
如果
--mixed
或没有选项,停止!否则(--hard
),继续...按照步骤 2 中替换索引文件的方式替换工作树文件。
如果你已经完成了所有这些,你可以看到 git reset --mixed
和 git reset --hard
可以,如果我们选择 当前提交 作为new commit, just reset the index, or reset the index and replace the working-tree文件。如果我们不给 git reset
一个特定的提交哈希 ID 或名称或相关指令,如 HEAD~1
或 HEAD^2
,git reset
使用 HEAD
。所以 git reset --soft HEAD
或 git reset --soft
只是一种什么都不做的方法,但是 git reset HEAD
或 git reset
是一种清除 Git 索引的方法,使得它再次匹配 HEAD
。 (你不想这样做——我只是在这里注明,以便你可以对 git reset
的行为有一个正确的心理模型。)
关于git commit
当你 运行 git commit
, Git:
- 收集任何必要的元数据,包括日志消息;
- 添加适当的父提交哈希 ID:通常只是
HEAD
,但如果您要提交合并,HEAD
加上更多; - 将Git索引中的任何内容打包为新快照;
- 将所有这些作为新提交写出,它获得一个新的、唯一的哈希 ID;和
- 将 new 哈希 ID 写入 b运行ch name.
最后一步是我们如何从:
...--F <-- somebranch (HEAD)
至:
...--F--E <-- somebranch (HEAD)
例如,很久以前。你做了 git checkout somebranch
或 git switch somebranch
。那:
- 选择提交
F
,因为somebranch
指向提交F
; - 填写Git的索引来自提交;
- 从提交中填写你的工作树(现在在 Git 的索引中表示);和
- 将名称
HEAD
附加到名称somebranch
,以便 Git 知道将来的提交应该写入somebranch
.
然后你修改了一些文件和运行 git add
。这将任何更新的文件 复制到 Git 的索引中,准备提交。索引继续持有提议的下一次提交(或快照部分),git add
改变提议的快照,通过弹出一些current 索引文件并放入新的(更新的)文件。它实际上是执行所有 Git-ifying 文件的 git add
步骤,使它们准备好提交。
最后,你运行git commit
。这打包了所有文件的索引副本,以制作新的快照。它添加了正确的元数据。它进行了提交,获得了 Git 提交 E
的哈希 ID。 (这也将提交 E
放入 Git 的 all-the-commits-and-other-objects 数据库。)最后,它将 E
的哈希 ID 写入 name somebranch
,现在你有:
...--F--E <-- somebranch (HEAD)
与当前提交和 Git 的索引再次匹配。如果您 git add
-ed all 您更新的文件、提交、索引和您的工作树都匹配。如果你只 git add
-ed selected 文件,你仍然有一些与提交不匹配的工作树文件,你可以 git add
它们并制作另一个提交。
你现在在哪里
与此同时,我们现在处于这种状态:
B___
/ \
...--F--E A <-- somebranch (HEAD)
\ /
D--C
提交 B
在某种意义上是 不好的 。你不想提交 B
。它会保留很长一段时间——从你制作它起至少 30 天——即使在我们设置好之后你不能 see commit B
, 但没关系,Git 会 最终 在它闲置太久未使用时清除它。
这意味着提交 A
也很糟糕,因为提交 A
永久 link 回到提交 B
。 (A
link 也回到 C
,但是 C
没问题。)任何现有提交的任何部分都不能更改,因此放弃 B
,我们也不得不放弃A
。
所以:让我们使用 git reset
移动 somebranch
,以便 somebranch
找到提交 C
。我们可以在这里使用三个重置选项中的任何一个,但其中一个选项使事情变得简单:
如果我们使用
git reset --soft
,索引保持不变。 Git 的索引当前与合并提交A
中的快照匹配。 这是您说要保留的快照。如果我们使用
--mixed
或--hard
,Git将清空其索引并从提交C
填充它。这并不可怕——我们想要的文件仍在提交中A
——但它显然没有那么有用。
所以让我们运行 git reset --soft <em>hash-of-C[=512=
。或者,因为 current 提交是提交 A
,我们可以使用 HEAD^2
。如果我们查看 the gitrevisions documentation,我们会发现 HEAD^2
表示 当前提交的第二个父级 。那将是提交 C
。 请注意,我们需要立即提交 A
才能在 Git 的索引中包含正确的内容,所以如果我们 not on commit A
此时,我们最好先检查一下。
最终结果是这样的:
B___
/ \
...--F--E A
\ /
D--C <-- somebranch (HEAD)
一旦我们有了这个,我们就可以 运行 git commit
。 Git 将使用 Git 索引中的任何内容——感谢 --soft
和我们之前在 A
的位置,这是来自提交 [=37= 的文件集]——进行新的提交。我们将调用新提交 B'
;让我们把它画进去:
B___
/ \
...--F--E A
\ /
D--C--B' <-- somebranch (HEAD)
无法看到提交 A
。没有 name (b运行ch name) 可以找到它。我们可以 运行 git log
并给它 A
的原始哈希 ID,然后 那 会找到提交 A
,但我们可以否则就会看到它。所以让我们更新我们的绘图,就好像没有提交 A
。由于 A
是找到 B
的唯一方法,因此我们也将 B
排除在外:
...--F--E--D--C--B' <-- somebranch (HEAD)
所以我们最后的命令序列是:
git checkout somebranch # if necessary
git log --decorate --oneline --graph # make sure everything is as expected
git reset --soft HEAD^2
git commit
关于 HEAD^2
的注释:当心 DOS/Windows CLI 吃掉 ^
个字符。您可能必须使用 HEAD^^2
、引号或其他东西来保护 ^
.
最后一次优化
当您 运行 git commit
时,Git 将需要一条日志消息。如果现有提交 B
中的日志消息是好的并且你想要 re-use 它,你可以告诉 Git 这样做。 git commit
命令有一个 -c
或 -C
选项。 运行:
git commit -C <hash-of-B>
将从提交 B
中获取提交消息并使用它。您不会被扔进编辑器来提出提交消息。
如果 B
中的提交消息可以 改进 ,您可能 想要 被扔到您的编辑器中。为此,请添加 --edit
,或将大写 -C
更改为小写 -c
:
git commit --edit -C <hash-of-B>
或:
git commit -c <hash-of-B>
请注意,在 git reset
之后,很难找到 B
的哈希值,因此您可能需要保存它。 Git 的 reflogs 有一个技巧来获取它,不过:somebranch@{1}
是重置前 somebranch
的旧值,因此:
git commit -c somebranch@{1}~1
会起作用。不过,我通常发现使用 git log
然后用鼠标剪切和粘贴原始哈希 ID 比输入复杂的 <em>name</em>@ 更容易{<em>number</em>}~<em>number</em>^<em>number</em>
个表达式.