-
1. 起步
-
2. Git 基础
-
3. Git 分支
-
4. 服务器上的 Git
- 4.1 协议
- 4.2 在服务器上部署 Git
- 4.3 生成 SSH 公钥
- 4.4 架设服务器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管服务
- 4.10 小结
-
5. 分布式 Git
-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: Git 命令
7.6 Git 工具 - 重写历史
重写历史
在 Git 的使用过程中,你经常会想要修改你本地的提交历史。Git 的一个优点是它允许你在最后一刻做出决定。你可以通过暂存区在提交前决定哪些文件属于哪个提交;你可以使用 git stash 来放弃暂时不用的工作;你也可以重写已经发生的提交,让它们看起来像是以不同的方式发生的。这可能包括更改提交的顺序、修改提交信息或文件内容、合并或拆分提交,或者完全删除提交——所有这些都可以在与他人分享你的工作之前完成。
在本节中,你将学习如何完成这些任务,以便在与他人分享之前,使你的提交历史看起来是你想要的。
|
注意
|
在你满意你的工作之前,不要推送
Git 的一个基本原则是,由于大部分工作都在你的本地克隆中完成,所以你有很大的自由度来*本地*重写你的历史。但是,一旦你推送了你的工作,情况就完全不同了,你应该将推送过的作为最终版本,除非有充分的理由去修改。简而言之,在你对你的工作满意并准备好与世界分享之前,你应该避免推送你的工作。 |
修改最后一次提交
修改你最近一次提交可能是你最常进行的历史重写。你经常会对最后一次提交做两件基本的事情:简单地修改提交信息,或者通过添加、删除和修改文件来改变提交的实际内容。
如果你只想修改你最后一次提交的消息,这很简单
$ git commit --amend
上面的命令会将之前的提交消息加载到编辑器会话中,你可以在其中修改消息,保存更改并退出。当你保存并关闭编辑器时,编辑器会创建一个包含更新后提交消息的新提交,并使其成为你最新的提交。
另一方面,如果你想改变最后一次提交的*内容*,过程基本相同——首先进行你认为遗漏的更改,暂存这些更改,然后随后的 git commit --amend 会用你新的、改进的提交*替换*最后一次提交。
你需要小心使用此技术,因为修改会改变提交的 SHA-1 值。这就像一次非常小的 rebase——如果你已经推送了最后一次提交,请不要修改它。
|
提示
|
修改过的提交可能(或可能不需要)修改提交信息
当你修改提交时,你有机会同时修改提交消息和提交内容。如果你大幅修改了提交内容,你几乎肯定应该更新提交消息以反映修改后的内容。 另一方面,如果你的修改足够琐碎(例如,修复一个愚蠢的拼写错误或添加了一个你忘记暂存的文件),以至于之前的提交消息仍然适用,你可以简单地进行修改,暂存它们,然后使用以下命令完全避免不必要的编辑器会话:
|
修改多个提交消息
要修改历史中更靠前的提交,你必须使用更复杂的工具。Git 没有一个修改历史的工具,但你可以使用 rebase 工具将一系列提交 rebase 到它们最初基于的 HEAD,而不是将它们移动到另一个 HEAD。使用交互式 rebase 工具,你可以停在你想修改的每个提交之后,然后修改消息、添加文件,或者做任何你想做的事情。通过给 git rebase 添加 -i 选项,你可以交互式地运行 rebase。你必须通过告诉命令要 rebase 到哪个提交来指定你要重写多远的提交。
例如,如果你想修改最后三个提交的消息,或者该组中的任何一个提交的消息,你可以将你想要编辑的最后一个提交的父提交作为参数传递给 git rebase -i,即 HEAD~2^ 或 HEAD~3。记住 ~3 可能更容易,因为你想编辑最后三个提交,但要记住,你实际上是在指定四个提交之前的那个提交,即你想要编辑的最后一个提交的父提交。
$ git rebase -i HEAD~3
再次记住,这是一个 rebase 命令——范围 HEAD~3..HEAD 内的每个提交,如果其消息已更改,*及其所有后代*都将被重写。不要包含任何你已经推送到中央服务器的提交——这样做会通过提供同一更改的替代版本来混淆其他开发者。
运行此命令会在你的文本编辑器中得到一个提交列表,看起来像这样:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
重要的是要注意,这些提交的列出顺序与你通常使用 log 命令看到的顺序相反。如果你运行 log,你会看到类似这样的内容:
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d Add cat-file
310154e Update README formatting and add blame
f7f3f6d Change my name a bit
注意相反的顺序。交互式 rebase 提供了一个它将要运行的脚本。它将从你在命令行中指定的提交(HEAD~3)开始,并从上到下重放这些提交中的每个更改。它列出最旧的在最上面,而不是最新的,因为那是它将要重放的第一个。
你需要编辑脚本,使其在你想编辑的提交处停止。为此,将“pick”替换为“edit”,用于你希望脚本停止的每个提交。例如,如果你只想修改第三个提交的消息,你可以将文件修改成这样:
edit f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
当你保存并退出编辑器时,Git 会将你回溯到列表中最后一个提交,并在命令行上给你以下消息:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... Change my name a bit
You can amend the commit now, with
git commit --amend
Once you're satisfied with your changes, run
git rebase --continue
这些说明告诉你该怎么做。输入:
$ git commit --amend
修改提交消息,然后退出编辑器。然后,运行:
$ git rebase --continue
此命令将自动应用另外两个提交,然后你就完成了。如果你将 pick 改为 edit 多行,你可以对每个你更改为 edit 的提交重复这些步骤。每次,Git 都会停止,让你修改提交,并在你完成后继续。
重新排序提交
你也可以使用交互式 rebase 来重新排序或完全删除提交。如果你想删除“Add cat-file”提交并更改其他两个提交的引入顺序,你可以将 rebase 脚本从这个:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
改为这个:
pick 310154e Update README formatting and add blame
pick f7f3f6d Change my name a bit
当你保存并退出编辑器时,Git 会将你的分支回溯到这些提交的父提交,应用 310154e,然后是 f7f3f6d,然后停止。你有效地更改了这些提交的顺序,并完全删除了“Add cat-file”提交。
合并提交
使用交互式 rebase 工具,你也可以将一系列提交合并成一个提交。脚本在 rebase 消息中提供了有用的说明:
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
如果,而不是“pick”或“edit”,你指定“squash”,Git 会应用该更改及其直接前面的更改,并让你合并提交消息。所以,如果你想将这三个提交合并成一个提交,你可以将脚本修改成这样:
pick f7f3f6d Change my name a bit
squash 310154e Update README formatting and add blame
squash a5f4a0d Add cat-file
当你保存并退出编辑器时,Git 会应用所有三个更改,然后将你带回编辑器以合并三个提交消息:
# This is a combination of 3 commits.
# The first commit's message is:
Change my name a bit
# This is the 2nd commit message:
Update README formatting and add blame
# This is the 3rd commit message:
Add cat-file
当你保存它后,你就拥有了一个提交,它包含了之前所有三个提交的更改。
拆分提交
拆分提交是撤销一个提交,然后根据你想要的结果数量分多次暂存和提交。例如,假设你想拆分你三个提交中的中间一个。与其“Update README formatting and add blame”,不如将其拆分成两个提交:“Update README formatting”作为第一个,然后“Add blame”作为第二个。你可以在 rebase -i 脚本中通过将你想拆分的提交的指令更改为“edit”来做到这一点:
pick f7f3f6d Change my name a bit
edit 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file
然后,当脚本将你带到命令行时,你重置该提交,获取已重置的更改,并从中创建多个提交。当你保存并退出编辑器时,Git 会回溯到列表中第一个提交的父提交,应用第一个提交(f7f3f6d),应用第二个(310154e),并将你带到控制台。在那里,你可以使用 git reset HEAD^ 进行混合重置,这实际上撤销了该提交并使已修改的文件处于未暂存状态。现在你可以暂存并提交文件,直到你有了几个提交,并在完成后运行 git rebase --continue。
$ git reset HEAD^
$ git add README
$ git commit -m 'Update README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'Add blame'
$ git rebase --continue
Git 应用脚本中的最后一个提交(a5f4a0d),你的历史看起来像这样:
$ git log -4 --pretty=format:"%h %s"
1c002dd Add cat-file
9b29157 Add blame
35cfb2b Update README formatting
f7f3f6d Change my name a bit
这会改变你列表中三个最近提交的 SHA-1 值,所以请确保列表中没有你已经推送到共享仓库的已更改提交。请注意,列表中最后一个提交(f7f3f6d)保持不变。尽管此提交显示在脚本中,但因为它被标记为“pick”并在任何 rebase 更改之前应用,Git 会将该提交保持不变。
删除提交
如果你想摆脱一个提交,你可以使用 rebase -i 脚本来删除它。在提交列表中,在你想删除的提交前加上“drop”字样(或者直接从 rebase 脚本中删除该行):
pick 461cb2a This commit is OK
drop 5aecc10 This commit is broken
由于 Git 构建提交对象的方式,删除或修改一个提交会导致其后所有提交的重写。你的仓库历史越靠后,需要重新创建的提交就越多。如果你在序列的后面有许多依赖于你刚刚删除的提交,这可能会导致很多合并冲突。
如果你在进行 rebase 时遇到中途,并且决定这不是一个好主意,你可以随时停止。输入 git rebase --abort,你的仓库将恢复到 rebase 开始之前的状态。
如果你完成了一个 rebase 并决定它不是你想要的,你可以使用 git reflog 来恢复你分支的早期版本。有关 reflog 命令的更多信息,请参阅数据恢复。
|
注意
|
Drew DeVault 创建了一个实用的实践指南,其中包含学习如何使用 |
核选项:filter-branch
如果你需要以某种可脚本化的方式重写大量提交,还有一个历史重写选项——例如,全局更改你的电子邮件地址或从每个提交中删除一个文件。该命令是 filter-branch,它可以重写你的历史的绝大部分,所以除非你的项目还没有公开,并且其他人还没有基于你即将重写的提交进行工作,否则你不应该使用它。但是,它可能非常有用。你将学习一些常见用法,以便了解它的一些能力。
|
警告
|
|
从每个提交中删除文件
这种情况相当普遍。有人意外地通过一个不加思索的 git add . 提交了一个巨大的二进制文件,而你想将其从所有地方删除。也许你不小心提交了一个包含密码的文件,而你想让你的项目开源。filter-branch 是你可能想要用来清理你整个历史的工具。要从你的整个历史中删除一个名为 passwords.txt 的文件,你可以使用 filter-branch 的 --tree-filter 选项。
$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten
--tree-filter 选项会在每次检出项目后运行指定的命令,然后重新提交结果。在本例中,你将从每个快照中删除一个名为 passwords.txt 的文件,无论它是否存在。如果你想删除所有意外提交的编辑器备份文件,你可以运行类似 git filter-branch --tree-filter 'rm -f *~' HEAD 的命令。
你将能够看到 Git 重写树和提交,然后在最后移动分支指针。通常,最好在一个测试分支中执行此操作,然后在确定结果确实是你想要的之后,使用 hard-reset 你的 master 分支。要对所有分支运行 filter-branch,你可以将 --all 传递给命令。
将子目录设为新的根目录
假设你从另一个版本控制系统导入,并存在一些没有意义的子目录(trunk、tags 等)。如果你想让 trunk 子目录成为每个提交的新项目根目录,filter-branch 也可以帮助你做到这一点。
$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten
现在,你的新项目根目录是每次在 trunk 子目录中的内容。Git 还会自动删除未影响该子目录的提交。
全局更改电子邮件地址
另一个常见情况是,你忘记在开始工作前运行 git config 来设置你的姓名和电子邮件地址,或者你可能想开源一个公司项目,并将所有工作电子邮件地址更改为你的个人地址。在任何情况下,你都可以使用 filter-branch 批量更改多个提交中的电子邮件地址。你需要小心只更改你自己的电子邮件地址,所以使用 --commit-filter。
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
这会遍历并重写每个提交,使其包含你的新地址。由于提交包含其父提交的 SHA-1 值,此命令会更改你历史中的每个提交 SHA-1,而不仅仅是那些具有匹配电子邮件地址的提交。