在Git中,提交是用来记录版本库的变更的,每次提交变更时,Git都会创建一个提交对象(Commit Object)来存储树对象和提交信息。初始提交或根提交产生的提交对象没有父提交,其他提交产生的提交对象有一个父提交,而合并提交由多个分支合并产生的提交对象有多个父提交,最终形成提交的有向无环图(DAG)。本文将全面介绍Git中与提交相关的操作命令,并给出相关示例代码。
创建提交
git commit将所有通过git add暂存的文件内容在存储库中创建一个持久快照,即创建一个版本。
$ git commit # 提交变更,使用文本编辑器输入提交消息
$ git commit -m|--message <msg> # 提交变更,附加提交消息
代码示例
$ git init example
$ cd example
$ touch index.html
$ git add --all
$ git commit -m "add index.html"
查看提交
查看提交历史
$ git log <commit> # 查看提交历史(SHA-1哈希值、作者、提交时间和提交消息),按时间逆序
$ git rev-list <commit> # 查看提交列表
查看提交信息
$ git show <object> # 查看提交对象
$ git cat-file -t <object> # 查看提交对象的类型
$ git cat-file -p <object> # 查看提交对象的内容
代码示例
$ git init example
$ cd example
# 创建3个提交
$ for i in {1..3}; do echo "version${i}" > index.html; git add --all; git commit -m "version${i}"; done
# 查看提交历史
$ git log HEAD
commit 0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
Author: leitiannet <347341200@qq.com>
Date: Sun Jul 21 11:05:45 2024 +0800
version3
commit 2f14d3386afcfa8c95800772b06d4daddf7bc3d2
Author: leitiannet <347341200@qq.com>
Date: Sun Jul 21 11:05:45 2024 +0800
version2
commit 31d0cc999680421c3de5a73ce743b9dc5dab2f66
Author: leitiannet <347341200@qq.com>
Date: Sun Jul 21 11:05:45 2024 +0800
version1
# 查看提交列表
$ git rev-list HEAD
0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
2f14d3386afcfa8c95800772b06d4daddf7bc3d2
31d0cc999680421c3de5a73ce743b9dc5dab2f66
# 查看提交对象
$ git show --pretty=fuller 0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
commit 0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
Author: leitiannet <347341200@qq.com>
AuthorDate: Sun Jul 21 11:05:45 2024 +0800
Commit: leitiannet <347341200@qq.com>
CommitDate: Sun Jul 21 11:05:45 2024 +0800
version3
diff --git a/index.html b/index.html
index df7af2c..777d3c2 100644
--- a/index.html
+++ b/index.html
@@ -1 +1 @@
-version2
+version3
# 查看提交对象的类型
$ git cat-file -t 0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
commit
# 查看提交对象的内容
$ git cat-file -p 0f419904722079d3ab4e1a7a2d692cfc5d78ccc1
tree 5f93d5a82ad52c796504fd79f26964b7df128c13
parent 2f14d3386afcfa8c95800772b06d4daddf7bc3d2
author leitiannet <347341200@qq.com> 1721531145 +0800
committer leitiannet <347341200@qq.com> 1721531145 +0800
version3
修改提交
修改最新提交
git commit --amend修改当前分支的最后一次提交(创建新的提交来替换旧的提交,旧的提交只能通过引用日志才能访问),可以根据需要添加或删除文件,通常用于修正刚刚做出错误的提交。注意:如果已经推送了最后一次提交就不要修正它。
# 修补式提交
$ git commit --amend --no-edit <file> # 修改最后一次提交内容,不编辑提交消息
$ git commit --amend -m "<msg>" # 修改最后一次提交消息(Amend Last Commit)
$ git commit --amend --date="<date>" # 修改最后一次提交时间(author date)
$ GIT_COMMITTER_DATE="<date>" git commit --amend # 修改最后一次提交时间(committer date)
说明:实际上git commit --amend使用当前暂存区的内容来创建一个新提交,相当于执行下面两条命令
$ git reset --soft HEAD^
$ git commit -e -F .git/COMMIT_EDITMSG
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 修改之前
$ git rev-list --oneline HEAD
5575daa C
bff4cdd B
2e8c39c A
# 修改操作:最新提交C
$ git commit --amend
# 修改之后
$ git rev-list --oneline HEAD
14157d4 amend C
bff4cdd B
2e8c39c A
修改特定提交
方式1:使用git rebase -i实现
交互式变基是在变基命令的基础上添加-i选项,在变基的时候进入一个交互界面,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。
$ git rebase -i <commit>~ # 首先传递要修改提交的父提交,然后修改提交前面的动作关键字
说明:提交以时间先后顺序排序的,旧的提交排在前面。这和git log命令输出结果中先显示最近的提交的顺序相反(除非使用git log --reverse 命令)。这一点很容易理解,变基会以它们被添加到分支上的顺序重新应用变更集,而查询日志操作显示的提交是从外部引用开始到可达的提交进行排序的。
pick 应用此提交
reword 应用此提交,但是在提交的时候允许用户修改提交消息
edit 应用此提交,但是在应用时停止,执行git commit --amend修正提交,执行git rebase --continue继续变基
squash 应用此提交,但是提交与前面的提交压缩为一个
fixup 类似squash动作,但是此提交的提交消息被丢弃
drop 丢弃此提交,也可以将相关行删除,或者将其注释掉
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline HEAD
2c87546 C
752ffa9 B
491c4fd A
# 变基操作:将需要修改的提交的pick改为reword
$ git rebase -i HEAD~2
# 变基之后:A提交没有变化,B和C两个提交被重写---一个历史提交的改变会引起连锁变化,导致所有后续提交发生变化
$ git rev-list --oneline HEAD
3a1ab4c C
6de2380 amend B
491c4fd A
方式2:使用filter-branch实现
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 过滤之前
$ git rev-list --oneline HEAD
1488a41 C
1b044d8 B
120aa17 A
# 过滤操作:提交范围为最新2个提交
$ git filter-branch --msg-filter 'sed -e "s/B/amend B/"' HEAD~2..HEAD
# 过滤之后:A提交没有变化,B和C提交被重写---一个历史提交的改变会引起连锁变化,导致所有后续提交发生变化
$ git rev-list --oneline HEAD
ccf57b2 C
5c1c4be amend B
120aa17 A
说明:脚本其实是对提交范围内每个提交都起作用,因此,编写脚本时需要充分考虑,做好测试。
修改多个提交
方式1:使用git rebase -i实现
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline HEAD
a332775 C
e4a224e B
fdce1dc A
# 变基操作:将需要修改的每一个提交的pick改为edit(在特定的提交停止)
$ git rebase -i HEAD~2
$ git commit --amend
$ git rebase --continue
$ git commit --amend
$ git rebase --continue
# 变基之后:A提交没有变化,B和C提交被重写
$ git rev-list --oneline HEAD
5322920 amend C
264350c amend B
fdce1dc A
方式2:使用git filter-branch实现
git filter-branch通过自定义命令来操作不同Git对象,从而重写分支上的多个提交记录,在某些场景下,该功能非常有用。
警告:git filter-branch存在大量隐患,可能会对预期的历史重写产生不明显的误差,而且存在安全和性能问题,因此不建议使用。git filter-repo或BFG Repo-Cleaner是git filter-branch的替代工具。除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,否则你不应当使用它!!!
git filter-branch在一个或多个分支上运行一系列过滤器,每个过滤器可以搭配一条自定义过滤器命令(可执行脚本)。这些过滤器不必全部执行,甚至可以只执行一个。但是它们按顺序依次执行,于是前面过滤器可以影响后面过滤器的行为。默认修改当前分支(HEAD),使用--all修改所有分支,使用rev-list修改指定范围内提交,例如HEAD~10..HEAD表示最新的10个提交。
git filter-branch是一个shell脚本,除了commit-filter之外,每个command都利用eval在shell上下文中运行。
--setup <command> 循环之前的一次性设置
--subdirectory-filter <directory> 只查看给定子目录的历史记录,子目录过滤器可以将版本库的一个子目录提取为一个新版本库,并将该子目录作为版本库的根目录
--env-filter <command> 修改环境变量,对特定的环境变量的修改会改变提交
--tree-filter <command> 修改一个目录中将要被树对象所记录的内容,可以向版本库中添加或删除文件
--index-filter <command> 类似tree-filter,但不需要将每个提交检出到特定目录(.git-rewrite),速度更快
--parent-filter <command> 修改提交的父节点
--msg-filter <command> 修改提交消息
--commit-filter <command> 执行提交操作,缺省执行git commit-tree命令创建替代提交
--tag-name-filter <command> 修改标签名称
执行git filter-branch时,Git会把之前的状态备份在.git/refs/original/refs/heads目录中(也只是备份开始执行filter-branch之前的那个HEAD的SHA-1值而已)。所以可以从这个文件中把SHA-1值找出来,然后再hard Reset回去。
$ git reset refs/original/refs/heads/master --hard
$ git reset ORIG_HEAD --hard
代码实例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 过滤之前
$ git rev-list --oneline HEAD
302eab2 C
1d0a555 B
ec7dd66 A
# 过滤操作:重写整个分支
$ git filter-branch --tree-filter 'rm -f B.txt' HEAD
# 过滤之后:A提交没有变化,B和C提交被重写
$ git rev-list --oneline HEAD
1095c7d C
f3e0a2f B
ec7dd66 A
移除提交
方式1:使用git revert实现
git revert创建一个新提交来抵消给定提交的影响(创建新提交进行反向操作),通常用于修正错误的历史提交,或不允许reset来修改历史记录的场景。
代码示例
# 反转提交或还原提交
$ git init example
$ cd example
# master分支
$ for i in {A..G}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 撤销之前
$ git rev-list --oneline master
f811bd1 G
5143908 F
e19dcfb E
34f3901 D
dd0a4e8 C
be73db6 B
254a5da A
# 撤销操作:提交D
$ git revert master~3
# 撤销之后
$ git rev-list --oneline master
7032ad6 Revert "D"
f811bd1 G
5143908 F
e19dcfb E
34f3901 D
dd0a4e8 C
be73db6 B
254a5da A
$ ls
A.txt B.txt C.txt E.txt F.txt G.txt
方式2:使用git rebase -i实现
代码示例
$ git init example
$ cd example
$ for i in {A..G}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline master
d785c72 G
ae968f4 F
0482a57 E
06802ce D
9c9926f C
35dacdf B
fe086ea A
# 变基操作:将需要移除的提交的pick改为drop
$ git rebase -i master~6
# 变基之后
$ git rev-list --oneline master
93d495d G
b404a66 F
82215eb E
9c9926f C
35dacdf B
fe086ea A
$ ls
A.txt B.txt C.txt E.txt F.txt G.txt
回退提交
$ git reset --soft HEAD^ # 重置到上一个提交,暂存区和工作区保持不变(影响最小)---可以重新提交,相当于修改提交
$ git reset --mixed HEAD^ # 重置到上一个提交,重置暂存区,但工作区保持不变(默认模式)---必须重新暂存才能提交
$ git reset --hard HEAD^ # 重置到上一个提交,重置暂存区和工作区(影响最大)---丢失修改
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 重置之前
$ git rev-list --oneline HEAD
379b763 C
d441be6 B
a283c0f A
# 重置操作:取消当前分支的多个提交
$ git reset --hard HEAD~2
# 重置之后
$ git rev-list --oneline HEAD
a283c0f A
压缩提交
方式1:使用git reset实现
代码示例
$ git init example
$ cd example
# 第一次提交
$ echo "v1">file-a.txt; git add --all; git commit -m "first commit"
# 第二次提交(中间状态)
$ echo "v2">file-a.txt; echo "v1">file-b.txt; git add --all; git commit -m "second commit"
# 第三次提交
$ echo "v3">file-a.txt; git add --all; git commit -m "three commit"
# 压缩之前
$ git rev-list --oneline master
02e30b4 three commit
b96ffa8 second commit
1e2432a first commit
# 压缩操作:移动HEAD移动到一个旧一点的提交上(想要保留的最近提交)
$ git reset --soft HEAD~2
$ git commit -m "compress second and three commit"
# 压缩之后
$ git rev-list --oneline master
5b3915d compress second and three commit
1e2432a first commit
$ ls
file-a.txt file-b.txt
$ cat file-a.txt
v3
$ cat file-b.txt
v1
方式2:使用git rebase -i实现
代码示例
$ git init example
$ cd example
# 第一次提交
$ echo "v1">file-a.txt; git add --all; git commit -m "first commit"
# 第二次提交(中间状态)
$ echo "v2">file-a.txt; echo "v1">file-b.txt; git add --all; git commit -m "second commit"
# 第三次提交
$ echo "v3">file-a.txt; git add --all; git commit -m "three commit"
# 变基之前
$ git rev-list --oneline master
af5144b three commit
cdb95a6 second commit
5eaea17 first commit
# 变基操作:将需要压缩的提交的pick改为squash
$ git rebase -i master~3
# 变基之后
$ git rev-list --oneline master
bf94688 compress second and three commit
5eaea17 first commit
$ ls
file-a.txt file-b.txt
$ cat file-a.txt
v3
$ cat file-b.txt
v1
拆分提交
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline HEAD
4e2362b C
ae99911 B
f4e9b34 A
# 变基操作:将需要拆分的提交的pick改为edit(在特定的提交停止),然后多次地暂存与提交
$ git rebase -i HEAD~2
$ git reset --hard HEAD^
$ touch B.txt
$ git add --all
$ git commit -m 'new B'
$ echo "B" > B.txt
$ git add --all
$ git commit -m 'update B'
$ git rebase --continue
# 变基之后
$ git rev-list --oneline HEAD
a6d46f4 C
94b54cb update B
4ad35c8 new B
f4e9b34 A
插入提交
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline HEAD
e3ac62d C
429c270 B
0d845f9 A
# 变基操作:将需要插入的提交的pick改为edit(在特定的提交停止)
$ git rebase -i HEAD~2
$ touch B1.txt
$ git add --all
$ git commit -m 'B1'
$ git rebase --continue
# 变基之后
$ git rev-list --oneline HEAD
fe79405 C
837a16d B1
429c270 B
0d845f9 A
拣选提交
git cherry-pick在当前分支上应用给定提交引入的变更(创建新的提交),通常用于将一个分支的特定提交引入到另一个分支中。
操作过程相当于将该提交导出为补丁文件,然后在当前HEAD上重放形成无论内容还是提交说明都一致的提交。
$ git cherry-pick <rev> # 应用一个提交
$ git cherry-pick <rev1>..<rev2> # 应用一批提交
说明:如果提交是高度耦合的,并且修改的行有重叠,需要解决冲突来完全应用给定提交的修改。
代码示例
$ git init --initial-branch dev example
$ cd example
# dev开发分支
$ for i in {A..H}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
$ git log --oneline -1 dev~6
8a2ba8e B
# rel_2.3发布分支
$ git checkout -b rel_2.3 8a2ba8e
$ for i in {V..Z}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 拣选之前
$ git rev-list --oneline dev
9f91c4f H
df827b1 G
9e9d874 F
a30d786 E
263f18f D
1e9c7e1 C
8a2ba8e B
824380b A
$ git rev-list --oneline rel_2.3
794d84c Z
ef6946e Y
213909f X
fb90aac W
c1aa64b V
8a2ba8e B
824380b A
# 拣选操作:应用提交F
$ git checkout rel_2.3
$ git cherry-pick dev~2
# 拣选之后
$ git rev-list --oneline dev
9f91c4f H
df827b1 G
9e9d874 F
a30d786 E
263f18f D
1e9c7e1 C
8a2ba8e B
824380b A
$ git rev-list --oneline rel_2.3
8c84452 F
794d84c Z
ef6946e Y
213909f X
fb90aac W
c1aa64b V
8a2ba8e B
824380b A
重排提交
方式1:使用git cherry-pick实现
git cherry-pick可以用于重建一系列提交,通过从一个分支选择一批提交,然后把它们以不同的顺序引入到一个新分支。
代码示例
$ git init example
$ cd example
# master分支
$ for i in {A..D}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
$ git log --oneline -1 master~2
10495de B
# my_dev分支
$ git checkout -b my_dev 10495de
$ for i in {V..Z}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 拣选之前
$ git rev-list --oneline master
2a698d9 D
194aa9b C
10495de B
43c0ebe A
$ git rev-list --oneline my_dev
b800201 Z
ee7422c Y
d53cfa5 X
dfdd8fc W
631ce64 V
10495de B
43c0ebe A
# 拣选操作:以Y、W、X、Z的顺序应用提交
$ git checkout master
$ git cherry-pick my_dev~
$ git cherry-pick my_dev~3
$ git cherry-pick my_dev~2
$ git cherry-pick my_dev
# 拣选之后
$ git rev-list --oneline master
345a116 Z
83b14ef X
a504fcc W
14b1d0b Y
2a698d9 D
194aa9b C
10495de B
43c0ebe A
$ git rev-list --oneline my_dev
b800201 Z
ee7422c Y
d53cfa5 X
dfdd8fc W
631ce64 V
10495de B
43c0ebe A
方式2:使用git rebase -i实现
代码示例
$ git init example
$ cd example
$ for i in {A..C}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline HEAD
ccfc402 C
913bcec B
a8e5798 A
# 变基操作:调整提交先后顺序
$ git rebase -i HEAD~2
# 变基之后
$ git rev-list --oneline HEAD
7f9fcff B
8524bc0 C
a8e5798 A
合并提交
Git支持同时合并两个、三个、四个或多个分支,所有要进行合并的分支必须在同一个版本库中。
说明:当一个分支中的修改与另一个分支中的修改不发生冲突的时候,Git会计算合并结果,并创建一个新提交来代表新的统一状态。但是当分支冲突时,Git并不解决冲突,这通常出现在对同一个文件的同一行进行修改的时候。相反,Git把这种争议性的修改在索引中标记为“未合并”(unmerged),留给开发人员来处理。当Git无法自动合并时,需要在所有冲突都解决后再做一次最终提交(git add+git commit)。
$ git merge [<srcbranch>] # 当前分支始终为目标分支,其他一个或多个分支合并到当前分支
$ git merge-base # 找到两个或两个以上分支之间的合并基础
代码示例
$ git init example
$ cd example
$ touch A.txt; git add --all; git commit -m "A"
$ git checkout -b dev
$ touch B.txt; git add --all; git commit -m "B"
$ git checkout master
$ git merge --no-ff -m "merge branch dev" dev
变基提交
git rebase改变一系列提交以什么为基础的,用于将某一分支的提交按照原有顺序依次应用到另一分支上(重放)。变基操作的实质是丢弃一些现有的提交(提交仍然保留,但没有分支跟踪),然后相应地新建一些内容一样但实际上不同的提交。
git rebase命令基本是一个自动化的cherry-pick命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的cherry-pick出它们。
$ git rebase <upstream> [<branch>] # 目标提交为<branch>..<upstream>,目标分支为<upstream>
$ git rebase --onto <newbase> <upstream> [<branch>] # 目标提交为<branch>..<upstream>,目标分支为<newbase>
说明1:如果指定<branch>,自动执行git switch <branch>切换分支,执行git rebase命令之后当前分支为<branch>。
方式1
$ git rebase master topic
方式2
$ git checkout topic
$ git rebase master
说明2:变基操作一次只迁移一个提交,每个提交都可能有冲突需要解决。
代码示例
$ git init example
$ cd example
# master分支
$ for i in {A..E}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
$ git log --oneline -1 master~3
607dc7d B
# topic分支
$ git checkout -b topic 607dc7d
$ for i in {W..Z}; do touch ${i}.txt; git add --all; git commit -m "${i}"; done
# 变基之前
$ git rev-list --oneline master
e50bbbc E
0762c8f D
26e1ded C
607dc7d B
63448d1 A
$ git rev-list --oneline topic
acf4f33 Z
f673a29 Y
d6f5d40 X
b4f54ea W
607dc7d B
63448d1 A
# 变基操作:将topic分支迁移到master分支的头
$ git rebase master topic
# 变基之后
# git rev-list --oneline master
e50bbbc E
0762c8f D
26e1ded C
607dc7d B
63448d1 A
# git rev-list --oneline topic
c80c188 Z
609868e Y
68a729b X
05f29ce W
e50bbbc E
0762c8f D
26e1ded C
607dc7d B
63448d1 A
请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
参考资料
《Pro Git》
《Git权威指南》
《Git版本控制管理(第2版)》
《Git高手之路》