如何修改特定的提交?

3164

我有以下提交历史:

  1. HEAD
  2. HEAD~
  3. HEAD~2
  4. HEAD~3

git commit --amend 修改了当前的 HEAD 提交记录。但是如何修改 HEAD~3 呢?


49
这里有一个备选答案:https://dev59.com/SGoy5IYBdhLWcg3wBJgx#18150592。你所接受的答案确实是一个准确回答你问题的答案,但如果在使用编辑之前你已经准备好了新的提交,那么这个答案会更加直接明了。它也可以适用于你想要合并/压缩多个提交与旧提交一起的情况。 - akostadinov
6
您可以查看《Git工具-重写历史》中的《拆分提交》一节以了解更多信息。 - hakre
1
可能是 如何修改现有的、未推送的提交? 的重复。 - tkruse
1
我认为最简单的方法就是添加一个新提交并将其与您想要更改的提交压缩。我是一个终端-IDE(JetBrains)-Git混合使用者。我通常在终端上执行git命令,如status、add、commit、push,但是用JetBrains Git集成工具来进行压缩。这样非常方便,而且速度很快。在这种情况下,只需要与相应的提交一起添加、提交、压缩(使用IDE),然后强制推送即可完成。 - Schmalitz
20个回答

4320

使用git rebase。例如,要修改提交bbc643cd,运行:

git rebase --interactive bbc643cd~

请注意命令末尾的波浪号~,因为您需要在bbc643cd之前的提交上重新应用提交(即bbc643cd~)。
在默认编辑器中,修改提到bbc643cd的行中的pickedit
保存文件并退出。git将解释并自动执行文件中的命令。您会发现自己处于刚刚创建提交bbc643cd的先前状态。
此时,bbc643cd是您的最后一次提交,您可以轻松地修改它。进行更改,然后使用以下命令提交它们:
git commit --all --amend --no-edit

然后,使用以下命令返回到先前的 HEAD 提交:

git rebase --continue

警告:请注意这将更改该提交的SHA-1以及所有子提交--换句话说,这将重写从该点之后的历史记录。如果您使用命令git push --force进行推送,可能会破坏repos


163
在这个流程中,另一个有趣的选项是,一旦您已经移动到要修改的提交,而不是修改文件并在顶部添加修订的提交(您正在编辑的提交),您可能想将该提交拆分为两个不同的提交(甚至更多)。在这种情况下,返回要编辑的提交,然后运行“git reset HEAD^”,这将把该提交的修改文件放入暂存区。现在可以随意选择和提交任何文件。这个流程在“git-rebase”手册页中有很好的解释。请参阅“分割提交”部分。http://bit.ly/d50w1M - Diego Pino
241
在Git 1.6.6及更高版本中,您可以在git rebase -i中使用reword操作代替edit(它会自动打开编辑器并继续执行其余的rebase步骤;这样就不需要使用git commit --amendgit rebase --continue,当您只需要更改提交消息而不是内容时)。 - Chris Johnsen
139
值得注意的是,如果您有未提交的更改,则在运行git rebase之前可能需要运行git stash命令,并在之后运行git stash pop来还原这些更改。 - user123444555621
8
在交互式变基中,是否有一种快捷命令可以编辑特定提交,而无需打开编辑器、查找提交、将其标记为编辑,然后返回到命令行? - sstur
34
请注意,使用更新版本的 Git 时,最好按照提示信息操作,而不是盲目地使用“git commit --all --amend --no-edit”。在执行“git rebase -i ...”后,我只需要正常地使用“git commit --amend”,然后执行“git rebase --continue”即可。 - Eric Chen
显示剩余26条评论

742
使用强大的交互式变基:
git rebase -i    # Show your commits in a text editor

找到你想要的提交,将 pick 改为 eedit),然后保存并关闭文件。Git 将会回退到该提交,让你可以选择:
- 使用 git commit --amend 进行修改,或者 - 使用 git reset @~ 放弃最后一次提交,但保留对文件的修改(即回到你编辑文件但尚未提交时的状态)。
后者对于执行更复杂的操作,如拆分成多个提交,非常有用。
然后,运行 git rebase --continue,Git 将在你修改的提交之上重播后续的更改。可能需要解决一些合并冲突。
注意: @HEAD 的简写,~ 表示指定提交的前一个提交。
在 Git 文档中阅读更多关于重写历史的内容。

不要害怕变基

专业提示™:不要害怕尝试使用“危险”的命令来重写历史* — Git 默认情况下会保留你的提交记录90天,你可以在reflog中找到它们:

$ git reset @~3   # go back 3 commits
$ git reflog
c4f708b HEAD@{0}: reset: moving to @~3
2c52489 HEAD@{1}: commit: more changes
4a5246d HEAD@{2}: commit: make important changes
e8571e4 HEAD@{3}: commit: make some changes
... earlier commits ...
$ git reset 2c52489
... and you're back where you started

* 小心选项,比如--hard--force,它们可能会丢弃数据。
* 另外,在你正在合作的任何分支上不要重写历史。



在许多系统上,默认情况下,git rebase -i会打开Vim。Vim的工作方式与大多数现代文本编辑器不同,所以请参考如何使用Vim进行rebase。如果你更喜欢使用其他编辑器,请使用git config --global core.editor your-favorite-text-editor进行更改。

10
“git reset @~” 是我在使用“git rebase…”命令选择提交后想要执行的操作,你是我的英雄。 - 18augst
3
想要编辑第一个提交的人:git rebase -i --root. - basickarl
1
太棒了!在我的JetBrains IDE中,我可以右键单击要编辑的提交,然后选择“从这里交互式地重新设置基础”,并获得一个漂亮的GUI窗口,让我选择要暂停编辑的提交。 - Toby 1 Kenobi
1
只需使用git rebase -i而不是git rebase -i @~9来查看您分支的所有提交。如果您使用git rebase -i @~9并且您的分支中的提交较少,那么rebase将使最后的9个提交成为您分支的一部分。 - undefined
@A1m 谢谢!我之前使用这个选项是为了确保rebase不会影响我不想修改的旧提交记录;然而,在测试过程中,我发现只有在先前的提交记录被修改过后,pick才会对提交记录进行操作。 - undefined
显示剩余4条评论

134

当我需要修复历史中更深的以前提交时,我经常使用带有--autosquash的交互式rebase。它本质上加速了ZelluX答案所示的过程,并且在您需要编辑多个提交时特别方便。

来自文档:

--autosquash

当提交日志消息以"squash!..."(或"fixup!...")开头,并且有一个标题以相同...开头的提交时,自动修改rebase -i的待办事项列表,使标记为squashing的提交紧随要修改的提交之后

假设您的历史记录如下所示:

$ git log --graph --oneline
* b42d293 Commit3
* e8adec4 Commit2
* faaf19f Commit1

如果您有更改需要修订到Commit2,则可以使用以下命令提交您的更改:

$ git commit -m "fixup! Commit2"

您可以选择使用提交 SHA 而不是提交消息,例如 "fixup! e8adec4" 或者仅使用提交消息的前缀。

然后在之前的提交上启动交互式变基。

$ git rebase e8adec4^ -i --autosquash

您的编辑器将会打开,已经正确排序了提交记录。
pick e8adec4 Commit2
fixup 54e1a99 fixup! Commit2
pick b42d293 Commit3

你需要做的就是保存并退出


32
你也可以使用 git commit --fixup=@~ 替代 git commit -m "fixup! Commit2"。当你的提交信息很长,在键入整个信息时会很麻烦时,这一点尤为有用。 - Zaz
3
我为我的.gitconfig写了一个别名,以简化这个流程:fixup = "!fn() { git commit --fixup ${1} && GIT_EDITOR=true git rebase --autosquash -i ${1}^; }; fn -> git fixup <commitId> 将所有暂存的更改添加到给定的提交中。 - thrau
1
谢谢 @thrau!但是缺少一个闭合的" - Roald
1
Thraus别名似乎无法使用短的提交哈希。这个可以工作:fixup =“!fn(){git commit -m \”fixup!$ {1} \“&& GIT_EDITOR = true git rebase --autosquash -i $ {1} ^;}; fn” - Jens Roland
1
现在可以使用HEADHEAD^HEAD~7等:fixup = "!fn() { _FIXUP_COMMIT=\git rev-parse ${1}` && git commit -m "fixup! ${_FIXUP_COMMIT}" && GIT_EDITOR=true git rebase --autosquash -i ${_FIXUP_COMMIT}^; }; fn"` - Dori

63
根据文档修改早期或多个提交信息的方法
git rebase -i HEAD~3 

以上显示了当前分支上最近的3个提交记录,如果您想获取更多,请将3更改为其他数字。列表将类似于以下内容:

pick e499d89 Delete CNAME
pick 0c39034 Better README
pick f7fde4a Change the commit message but push the same commit.

在您想要更改的每个提交消息之前,将pick替换为reword。假设您要更改列表中的第二个提交,则文件将如下所示:

pick e499d89 Delete CNAME
reword 0c39034 Better README
pick f7fde4a Change the commit message but push the same commit.

保存并关闭提交列表文件,这将弹出一个新的编辑器供您更改提交消息,更改提交消息并保存。

最后,强制推送修改后的提交。

git push --force

我收到了以下错误信息:错误:编辑器“vi”出现问题。 请使用“-m”或“-F”选项提供消息。 - Erick Maynard
2
“Reword”选项是一个好工具,但“git push --force”是危险的。如果我们想要更改提交消息的提交尚未提交,则不需要--force。--force选项会重写远程库中的历史记录,并需要更多权限。如果您想修改仅位于计算机上的提交,则不需要--force;如果提交已经被推送,则除非绝对必要,否则不应更改它。 - sissi_luaty

61

运行:

$ git rebase --interactive commit_hash^

每个^表示您要编辑多少个提交,如果只有一个(您指定的提交哈希值),则只需添加一个^

使用Vim,将单词pick更改为reword以更改您想要更改的提交,保存并退出(:wq)。 然后Git会提示您每个标记为重新编辑的提交,以便您可以更改提交消息。

每个提交消息都必须保存并退出(:wq)以进入下一个提交消息

如果您想退出而不应用更改,请按:q!

编辑:要在vim中导航,您可以使用j向上,k向下,h向左和l向右(所有这些都在NORMAL模式下,在NORMAL模式下按ESC键)。 要编辑文本,请按i,以便您进入INSERT模式,在该模式下插入文本。 按ESC返回NORMAL模式 :)

更新:这是来自Github的一个很棒的链接,列出了使用Git撤消(几乎)所有操作


4
对我来说完美运行。值得一提的是 git push --force 吗? - u01jmg3
@BetuUuUu 当然,如果您的提交已推送到远程并且您在本地修改了提交消息,那么您肯定想要强制推送到远程,不是吗? - Sudip Bhandari
@SudipBhandari 这就是我得到的感觉。我没有强制,现在我有一个额外的分支,将所有提交镜像回我更改消息的那个提交,这非常丑陋。 - ruffin
交互式变基在开始时可能会看起来有些棘手。我写了一篇带有图片的文章,详细地介绍了它的每个步骤:https://blog.tratif.com/2018/04/19/the-power-of-git-interactive-rebase/ - Tomasz Kaczmarzyk
2
@Greenhouse 如果您修改并强制推送,那么其他团队成员很可能会遇到合并冲突。因此,您应该非常谨慎地处理它。但是,如果您修改了尚未被其他人获取的内容,那么这应该是可以的(他们不会注意到它)。因此,我认为--force应该是最后的手段,并始终与其他成员协商仓库的状态。 - Tomasz Kaczmarzyk
显示剩余2条评论

42

完全非交互式命令(1)

我想分享一个我正在使用的别名。它基于非交互式交互式变基。将其添加到您的git中,运行以下命令(下面给出解释):

git config --global alias.amend-to '!f() { SHA=`git rev-parse "$1"`; git commit --fixup "$SHA" && GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^"; }; f'

或者,还有一种可以处理未暂存文件(通过储藏和取消储藏)的版本:

git config --global alias.amend-to '!f() { SHA=`git rev-parse "$1"`; git stash -k && git commit --fixup "$SHA" && GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^" && git stash pop; }; f'

这个命令最大的优点就是它不需要使用vim


(1)当然,前提是在rebase过程中没有冲突。

用法

git amend-to <REV> # e.g.
git amend-to HEAD~1
git amend-to aaaa1111

在我看来,amend-to 这个名称似乎很合适。将其与 --amend 进行比较:

git add . && git commit --amend --no-edit
# vs
git add . && git amend-to <REV>

说明

  • git config --global alias.<NAME> '!<COMMAND>' - 创建名为<NAME>的全局Git别名,它将执行非Git命令<COMMAND>
  • f() { <BODY> }; f - 一个“匿名”的Bash函数。
  • SHA=`git rev-parse "$1"`; - 将参数转换为Git修订,并将结果分配给变量SHA
  • git commit --fixup "$SHA" - 用于SHA的修复提交。更多信息请参见git-commit文档
  • GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^"
    • git rebase --interactive "$SHA^"部分已经在其他答案中涵盖。
    • --autosquash是与git commit --fixup一起使用的选项,请参阅git-rebase文档以了解更多信息
    • GIT_SEQUENCE_EDITOR=true使整个过程非交互式。我从这篇博客文章中学到了这个技巧。

1
还可以让 amend-to 处理未暂存的文件:git config --global alias.amend-to '!f() { SHA=git rev-parse "$1"; git stash -k && git commit --fixup "$SHA" && GIT_SEQUENCE_EDITOR=true git rebase --interactive --autosquash "$SHA^" && git stash pop; }; f' - Dethariel
3
这种方法的一个担忧是它可能会应用不相关的修复。 - Ciro Santilli OurBigBook.com
问题的重点不是要改变提交记录消息吗?因为这个答案没有涉及到这一点,或者至少没有直接回答。 - wytten
只是提醒一下,如果"$SHA1"参数是第一次提交,我认为rebase命令不起作用。 - Big McLargeHuge
能够处理未暂存版本的版本 - 淘金,谢谢!改变游戏规则 :D - Gelldur
显示剩余5条评论

26
如果出于某些原因您不喜欢交互式编辑器,您可以使用git rebase --onto。假设您想修改Commit1。首先,请从Commit1之前的分支中创建一个新分支:
git checkout -b amending [commit before Commit1]

第二步,使用 cherry-pick 命令获取 Commit1

git cherry-pick Commit1

现在,请修改您的更改,创建Commit1'

git add ...
git commit --amend -m "new message for Commit1"

最后,将所有其他更改都存储后,将其余的提交移植到您新提交的 master 顶部:

git rebase --onto amending Commit1 master

阅读:"将位于amending分支和master分支之间(不含Commit1)的所有提交,rebase到当前分支上。也就是说,剪切掉旧的Commit1,只保留Commit2和Commit3。你也可以使用cherry-pick方式,但这种方式更简单。

记得清理你的分支!

git branch -d amending

4
你可以使用 git checkout -b amending Commit1~1 命令获取之前的提交记录。 - Arin Taylor
前两个步骤是否等同于 git checkout -b amending Commit1 - Haoshu
这是一个非常适合对交互式变基感到害怕的人的好答案。我唯一不满意的是从早期提交开始并挑选要修改的实际提交是不必要的。你可以根据给定的提交分支,然后像所示那样进行修改,跳过挑选步骤。事实上,挑选将只会快进你的分支一个提交,就像你直接从此提交分支一样。 - Red

19

git stash + rebase 自动化

当我需要多次修改旧提交以进行 Gerrit 评审时,我一直在执行以下操作:

git-amend-old() (
  # Stash, apply to past commit, and rebase the current branch on to of the result.
  current_branch="$(git rev-parse --abbrev-ref HEAD)"
  apply_to="$1"
  git stash
  git checkout "$apply_to"
  git stash apply
  git add -u
  git commit --amend --no-edit
  new_sha="$(git log --format="%H" -n 1)"
  git checkout "$current_branch"
  git rebase --onto "$new_sha" "$apply_to"
)

GitHub 上游

用法:

  • 修改源文件,如果已经在仓库中则不需要 git add
  • git-amend-old $old_sha

我喜欢这个方法胜过 --autosquash,因为它不会压缩其他无关的修补。


2
非常好的解决方法,这应该成为git amend的默认选项,使用当前存储来应用更改到特定提交,非常聪明! - caiohamamura
2
非常聪明易懂!谢谢。 - because_im_batman

13

最佳选择是使用"交互式变基命令"

git rebase 命令非常强大,它允许您编辑提交消息、合并提交、重新排序等。

每次重新设置提交时,无论内容是否更改,每个提交都将创建一个新的SHA!您应该谨慎使用此命令,因为它可能会对与其他开发人员协作的工作产生重大影响。他们可能会在您重新设置某些提交时开始使用您的提交。在您强制推送提交之后,他们将不同步,您可能会发现处于混乱状态。所以要小心!

建议在重新设置之前创建一个备份分支,这样当您发现事情失控时,可以返回到以前的状态。

现在如何使用此命令?

git rebase -i <base> 

-i 代表 "交互式"。请注意,您可以在非交互模式下执行变基。例如:

#interactivly rebase the n commits from the current position, n is a given number(2,3 ...etc)
git rebase -i HEAD~n 

HEAD 表示您当前的位置(可以是分支名称或提交 SHA)。~n 表示“往前 n”,因此 HEAD~n 将是您当前所在的提交之前的“n”个提交的列表。

git rebase 有不同的命令,例如:

  • ppick:保持提交信息不变。
  • rreword:保留提交的内容但更改提交消息。
  • ssquash:将此提交的更改合并到上一个提交中(列表中的前一个提交)。
  • ... 等等。

    注意:最好让 Git 与您的代码编辑器配合使用,以使事情变得更简单。例如,如果您使用 Visual Code,您可以像这样添加:git config --global core.editor "code --wait"。或者您可以搜索谷歌以了解如何将您喜欢的代码编辑器与 GIT 关联起来。

git rebase 的示例

我想要更改我最近的两个提交,所以我按照以下步骤进行:

  1. 显示当前的提交:
  2. #This to show all the commits on one line
    $git log --oneline
    4f3d0c8 (HEAD -> documentation) docs: Add project description and included files"
    4d95e08 docs: Add created date and project title"
    eaf7978 (origin/master , origin/HEAD, master) Inital commit
    46a5819 Create README.md
    
  3. 现在我使用git rebase来修改最近两个提交的信息: $ git rebase -i HEAD~2 它会打开代码编辑器并显示如下内容:

  4. pick 4d95e08 docs: Add created date and project title
    pick 4f3d0c8 docs: Add project description and included files
    
    # Rebase eaf7978..4f3d0c8 onto eaf7978 (2 commands)
    #
    # Commands:
    # p, pick <commit> = use commit
    # r, reword <commit> = use commit, but edit the commit message
    ...
    

    由于我想要修改这2次提交的提交信息。因此,我会在pick的位置上输入rreword。然后保存文件并关闭标签页。 请注意,rebase是一个多步骤的过程,下一步是更新消息。还要注意,提交按照时间倒序显示,因此最后一个提交在最上面一行,第一个提交在最下面一行等等。

  5. 更新消息: 更新第一个消息:

    docs: Add created date and project title to the documentation "README.md"
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    ...
    

    保存并关闭 编辑第二个消息

    docs: Add project description and included files to the documentation "README.md"
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    ...
    

    保存并关闭。

  6. 你将在rebase结束时收到这样的消息:Successfully rebased and updated refs/heads/documentation,这意味着你成功了。你可以显示更改:

  7. 5dff827 (HEAD -> documentation) docs: Add project description and included files to the documentation "README.md"
    4585c68 docs: Add created date and project title to the documentation "README.md"
    eaf7978 (origin/master, origin/HEAD, master) Inital commit
    46a5819 Create README.md
    

    我希望这能帮助新用户 :).


12

自动交互式变基编辑后撤销提交,准备重做

我发现自己经常需要修复过去的提交,因此我写了一个脚本来处理它。

以下是工作流程:

  1. ...

git commit-edit <commit-hash>

这将使您进入要编辑的提交。

  • 按您原本希望的方式修复和暂存提交。

    (您可能需要使用git stash save命令来保存未提交的任何文件)

  • 使用--amend重新提交,例如:

    git commit --amend
    
  • 完成变基:

    git rebase --continue
    

    为了使上述内容生效,请将以下脚本放入名为 git-commit-edit 的可执行文件中,放在您的 $PATH 中的某个位置:

    ``` #!/bin/sh exec < /dev/tty && $(dirname "$0")/git-commit --edit "$@" ```
  • #!/bin/bash
    
    set -euo pipefail
    
    script_name=${0##*/}
    
    warn () { printf '%s: %s\n' "$script_name" "$*" >&2; }
    die () { warn "$@"; exit 1; }
    
    [[ $# -ge 2 ]] && die "Expected single commit to edit. Defaults to HEAD~"
    
    # Default to editing the parent of the most recent commit
    # The most recent commit can be edited with `git commit --amend`
    commit=$(git rev-parse --short "${1:-HEAD~}")
    message=$(git log -1 --format='%h %s' "$commit")
    
    if [[ $OSTYPE =~ ^darwin ]]; then
      sed_inplace=(sed -Ei "")
    else
      sed_inplace=(sed -Ei)
    fi
    
    export GIT_SEQUENCE_EDITOR="${sed_inplace[*]} "' "s/^pick ('"$commit"' .*)/edit \\1/"'
    git rebase --quiet --interactive --autostash --autosquash "$commit"~
    git reset --quiet @~ "$(git rev-parse --show-toplevel)"  # Reset the cache of the toplevel directory to the previous commit
    git commit --quiet --amend --no-edit --allow-empty  #  Commit an empty commit so that that cache diffs are un-reversed
    
    echo
    echo "Editing commit: $message" >&2
    echo
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接