# 比较全面的 Git 命令手册,几乎涵盖日常所有的使用场景(中)
# 工具
# 单次提交
# 日志引用
显示某次提交的具体信息,其中show
后为某次提交的部分校验和。
git show 9fceb0
查看某个分支的最后一次提交对象。
git show master
查看某个分支指向的提交对象的校验和。
git rev-parse master
可使用@{n}
来引用git reflog
中输出的记录,其中HEAD@{n}
若为提交记录,则输出提交记录信息,若非提交记录,则输出所在的分支的最近一次提交记录。注意reflog
引用日志只存在本地仓库,记录仓库内的操作信息,新克隆的仓库引用日志为空。
git show HEAD@{2}
# 祖先引用
查看某个提交的父提交信息。
git show HEAD^
某次提交为合并提交,则其存在多个父提交,^n
表示当前提交的第n
父提交。若某合并提交有两个父提交,其中第一父提交为合并时所在分支的提交,第二父提交为所合并分支的提交。
git show HEAD^2
根据指定的次数获取对应的第一父提交,如下为第一父提交的第一父提交,与HEAD^^
等价。
git show HEAD~2
# 提交区间
C —— D <-- dev
/
A —— B —— E —— F <-- master
# 双点
筛选出在一个分支中而不在另一个分支中的提交。如下命令选出在dev
分支而不在master
分支中的提交,即C
和D
提交。
git log master..dev
也可查看即将推送到远端的某些提交,如下查看当前分支推送到远端master
分支的提交有哪些。
git log origin/master..HEAD
# 排除
也可加上^
字符或者--not
来指明不希望包含某个分支的提交,如下三个命令等价。
git log A..B
git log ^A B
git log B --not A
查看A
或B
包含的但是C
不包含的提交。
git log A B ^C
git log A B --not C
# 三点
筛选被两个中的一个包含但又不包括两者同时包含的提交。如下查看master
或dev
中包含但是不包括两者共有提交,即E
、F
、C
和D
。
git log master...dev
常用参数--left-right
显示每个提交是处于哪一侧的分支。
git log --left-right master...dev
< F
< E
> D
> C
# 交互式暂存
运行命令git add -i
进入Git
交互式终端模式,其中-i
为--interactive
简写。
staged unstaged path
1: unchanged +1/-1 TODO
2: +1/-1 nothing index.html
3: unchanged +1/-1 readme.md
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now>
其中staged
为已暂存列表,unstaged
为未暂存列表,Commands
为操作命令,What now
后键入数字序号或命令首字母操作。
status
:同git status
一致,信息更简略update
:暂存文件,键入2
或u
后输入文件对应的数字暂存文件(多个文件用,
隔开),每个文件前面的*
意味着选中的文件将会被暂存,>>
提示符后不输入任何东西并回车表示执行此次命令revert
:取消暂存add untracked
:跟踪文件patch
:部分暂存文件,类似git add -p
diff
:暂存区和最近一次提交的差异,类似git diff --cached
quit
:退出交互式终端help
:命令帮助
# 部分暂存
执行如下命令,部分暂存更改,其中-p
为--patch
简写。
git add -p
其中每一个修改的文件称为一个区块,也可分隔成多个较小的区块,区块命令如下。
y
:暂存此区块n
:不暂存此区块q
:退出,不暂存包括此区块在内的剩余的区块a
:暂存此区块与此文件后面所有的区块d
:不暂存此区块与此文件后面所有的区块g
:选择并跳转至一个区块/
:搜索正则表达示匹配的区块j
:跳转至下一个未处理的区块J
:跳转至下一个区块k
:跳转至上一个未处理的区块K
:跳转至上一个区块s
:将当前的区块分割成多个较小的区块e
:手动编辑当前的区块?
:输出帮助
# 储藏
# 概述
储藏即将还不想提交的但是已修改的内容保存至堆栈中,后续可在某个分支上恢复出堆栈中的内容。不仅可以恢复到原分支,也可以恢复到其他任意指定的分支上。作用范围包括工作区和暂存区中的修改,即未提交的修改都会保存至堆栈中。
# 保存
将未提交(工作区和暂存区)的修改保存至堆栈中,不包括未跟踪的文件。
git stash
将未提交的修改和未跟踪的文件都保存至堆栈中,其中-u
为--include-untracked
简写,也可执行git stash --all
。
git stash -u
将工作区的修改保存至堆栈,不包括未跟踪的文件,其中-k
为--keep-index
简写。
git stash -k
保存至堆栈中并备注,其中message
为备注信息。
git stash save 'message'
保存部分修改至堆栈中,其中-p
为--patch
简写。
git stash -p
# 查看
查看堆栈中的内容。
git stash list
运行如下命令,查看保存至堆栈中的某次修改的改动(每个修改的文件增改行统计和共计)。git stash show
查看栈顶即最近一次保存至堆栈的修改的改动。
git stash show stash@{3}
查看某次修改的改动的详细差异。
git stash show stash@{3} -p
# 应用
运行如下命令,应用某次改动到当前分支。git stash apply
应用栈顶的改动。
git stash apply stash@{3}
已重新应用了文件的改动,但是之前暂存的修改未被重新暂存。--index
选项重新应用暂存的修改。
git stash apply --index
# 移除
移除堆栈上某次改动,git stash drop
移除栈顶的改动。
git stash drop stash@{3}
应用堆栈上某次改动并移除。git stash drop
应用并移除栈顶的改动。注意若执行git stash pop
出现冲突,实际已经应用了改动,但是改动依然在堆栈列表内,手动移除即可。
git stash pop stash@{3}
清空堆栈列表。
git stash clear
# 分支
运行如下命令,将堆栈中某次改动生成一个新分支,检出储藏时所在的提交,然后应用改动,应用成功后移除改动。git stash branch
将栈顶改动生成一个新分支。
git stash branch dev stash@{3}
# 清理
# 命令
;git clean
用来从工作目录删除未被跟踪的文件。
主要选项如下,可指定多项并简写。
-f
:--force
简写,删除时必须指定-n
:--dry-run
简写,用于显示将被删除的文件或文件夹-d
:删除文件夹时必须指定-x
:包括.gitignore
忽略的文件或文件夹-X
:仅为.gitignore
忽略的文件或文件夹-i
:交互式清理
查看将被删除的文件列表 。
git clean -n
查看将被删除的文件和文件夹。
git clean -d -n
删除未被跟踪的文件。
git clean -f
删除未被跟踪的文件和文件夹。
git clean -f -d
删除未被跟踪的文件和被.gitignore
忽略的文件。
git clean -f -x
仅删除被.gitignore
忽略的文件。
git clean -f -X
删除未被跟踪的文件和文件夹、.gitignore
忽略的文件和文件夹,也可简写为git clean -fdx
。
git clean -f -d -x
# 交互式清理
运行git clean -i -d
进入交互模式。
Would remove the following item:
dist/ readme.md index.html
*** Commands ***
1: clean 2: filter by pattern 3: select by numbers
4: ask each 5: quit 6: help
What now>
;Would remove the following item
后为即将清理的文件和文件夹列表。
clean
:清理列表内文件和文件夹filter by pattern
:排除清理列表部分文件或文件夹,全部排除清理列表为空自动退出交互模式select by numbers
:选择清理列表部分文件或文件夹,均未选择清理列表为空自动退出交互模式ask each
:询问方式删除列表文件或文件夹quit
:退出交互式模式help
:命令帮助
# 搜索
从工作目录中查找一个字符串或者正则表达式。
查找工作目录包含字符串A
的行。
git grep A
查找工作目录包含字符串A
的行,并展示行号。
git grep -n A
查找包含字符串A
的文件。
git grep --name-only A
统计文件中出现字符串A
的行数,-c
为--count
简写。
git grep -c A
某一行满足多个条件,如下命令满足某一行包括A
或B
,其中--or
可省略。
git grep -e A --or -e B
某一行包括A
并且包括B
。
git grep -e A --and -e B
某一行包括A
和B
或者A
和C
。
git grep -e A --and \( -e B -e C \)
# 交互式变基
用于变基时对提交历史记录进行复杂的修改,可用于调整提交顺序、改变提交中的提交信息或修改文件、压缩提交或拆分提交、也可用于移除提交等。
# 提交区间
D d34... <-- master HEAD
|
C c23...
|
B b12...
|
A <-- HEAD~3
运行如下命令显示交互式变基界面,其中-i
选项后为提交记录区间,HEAD~3
到HEAD
范围的提交记录,左开又闭,即为B
、C
和D
。区间终点可省略,默认为HEAD
指向的提交记录。注意Git
从上到下依次应用每一个提交的修改,越早的提交在越上面。
git rebase -i HEAD~3 HEAD
pick b12... B
pick c23... C
pick d34... D
# Rebase ... onto ... (3 commands)
# 选项参数
部分选项参数如下,注意删除某一行提交即移除某个提交,全部删除变基将会终止。
pick
:保留某个提交reword
:修改某个提交的提交信息edit
:修改某个提交squash
:将某个提交与上一个提交合并,可修改提交信息fixup
:将某个提交与上一个提交合并drop
:移除某个提交
# 移除提交
将pick
修改为drop
,保存并退出。如下将移除C
的提交信息。
pick b12... B
drop c23... C
pick d34... D
# 提交顺序
调整编辑器内提交记录顺序,保存并退出。如下将提交顺序由B
、C
、D
调整为D
、B
、C
。
pick d34... D
pick b12... B
pick c23... C
# 修改提交信息
将pick
修改为reword
,保存并退出。如下将修改C
、D
的提交信息。
pick b12... B
reword c23... C
reword d34... D
保存并退出后进入C
的提交信息编辑界面,然后再进入D
的提交信息编辑界面。运行git log --oneline
查看提交历史。
d89... D'
c36... C'
b12... B
# 压缩提交
将多个提交压缩为一个提交,如下将C
、D
压缩到B
,并修改提交信息。
pick b12... B
squash c23... C
squash d34... D
保存并退出将修改提交信息。
# This is a combination of 3 commits.
# This is the 1st commit message:
B
# This is the commit message #2:
C
# This is the commit message #3:
D
也可执行如下命令,跳过修改提交信息。
pick b12... B
fixup c23... C
fixup d34... D
# 拆分提交
拆分一个提交为多个提交,如下将拆分提交C
。
pick b12... B
edit c23... C
pick d34... D
保存并退出后,HEAD
指向提交C
,运行git reset HEAD^
实际是撤销C
的提交但是保留了修改且均为未暂存。然后再多次提交,最后执行git rebase --continue
继续变基。
git reset HEAD^
git add readme.md
git commit -m 'C1'
git add index.html
git commit -m 'C2'
git rebase --continue
运行git log --oneline
查看提交历史。
d96... D
c35... C2
c46... C1
b12... B
# 擦除
使用脚本的方式改写大量提交,可全局修改邮箱地址或从每一个提交中移除一个文件。
;filter-branch
选项参数如下。
--tree-filter
:在检出项目的每一个提交后运行指定的命令然后重新提交结果--prune-empty
:若修改后的提交为空则扔掉不要-f
:--force
简写,即忽略备份强制覆盖。第二次进行擦除时会出错,即Git
上次已做了备份,若再次运行的话会先处理掉上次的备份--all
:擦除所有分支--index-filter
:与--tree-filter
类似,--tree-filter
将每个提交检出到临时目录,运行filter
命令,并根据临时目录中的内容构建新的提交。而--index-filter
则将每个提交复制到索引中,运行filter
命令,并根据索引中的内容构建新的提交。即从索引构建提交比从目录构建提交要快
擦除dev
分支整个提交历史中的dist/index.html
文件。误操作可运行git reflog
查看历史提交校验和,再版本回退恢复。
git filter-branch -f --prune-empty --index-filter 'git rm -f --cached --ignore-unmatch dist/index.html' dev
批量修改当前分支提交历史中的作者和邮箱地址,如下将提交记录中的邮箱A@git.com
修改为B@git.com
,作者修改为B
。
git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "A@git.com" ];
then
GIT_AUTHOR_NAME="B";
GIT_AUTHOR_EMAIL="B@git.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi'
# 高阶命令
# 文件集合
;HEAD
是当前分支引用的指针,总是指向该分支上的最后一次提交。
;Index
是预期的下一次提交,可引用为暂存区域。
;Working Directory
即工作目录。
# 提交
;git init
创建一个Git
仓库,其中的HEAD
引用指向未创建的分支(master
还不存在)。分支即指向提交的指针,初始化的仓库没有提交记录,默认也就不存在分支。
? <-- master <-- HEAD
工作目录新建文件readme.md
,暂为v1
版本。
———— HEAD ———————— Index ———————— Working Directory
? ? readme.md (v1)
;git add
获取工作目录中的内容,将其复制到Index
中。
———— HEAD ———————— Index ———————— Working Directory
? readme.md (v1) readme.md (v1)
;git commit
将Index
中的内容保存为快照,然后创建指向快照的提交对象,更新master
指向此次提交对象。
v1 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v1) readme.md (v1)
修改工作目录中文件,定为v2
版本,运行git status
,将会看到Changes not staged for commit
。
v1 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v1) readme.md (v2)
暂存v2
,运行git status
,将会看到Changes to be committed
。
v1 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v2) readme.md (v2)
提交此次修改,master
指向v2
版本。
v1 —— v2 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v2) readme.md (v2) readme.md (v2)
# 重置
重置即git reset
版本回退,修改readme.md
并提交,提交历史如下。
v1 —— v2 —— v3 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v3) readme.md (v3) readme.md (v3)
第一步移动HEAD
,即移动master
指向v2
,HEAD
再指向master
。此过程可运行git reset --soft HEAD^
实现,其实质是撤销了v3
的提交,再次运行git commit
可完成git commit --amend
所做的事。
v3
/
v1 —— v2 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v2) readme.md (v3) readme.md (v3)
第二步更新Index
,即更新暂存区域。此过程可运行git reset --mixed HEAD^
实现,其中--mixed
可省略,实质是撤销v3
的提交,同时取消暂存所有修改。
v3
/
v1 —— v2 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v2) readme.md (v2) readme.md (v3)
第三步更新工作目录,即让工作目录与Index
一致。此过程可运行git reset --hard HEAD^
实现,强制将Index
中的v2
覆盖工作目录。
v3
/
v1 —— v2 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v2) readme.md (v2) readme.md (v2)
# 撤销暂存
修改readme.md
并暂存,提交历史如下。
v1 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v2) readme.md (v2)
运行git reset readme.md
(为git reset --mixed HEAD readme.md
的简写形式),实质只是将readme.md
从HEAD
复制到Index
。
v1 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v1) readme.md (v2)
也可以不从HEAD
复制到Index
,而是复制具体某次提交的文件对应版本,运行git reset 5f5292 readme.md
,其中5f5292
为某次提交的校验和。
v1 (5f5292) —— v2 —— v3 <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v3) readme.md (v1) readme.md (v3)
# 压缩
若一个项目最近有三次提交,第一次提交A
新增readme.md
,第二次提交B
修改readme.md
并新增index.txt
,第三次提交C
再次修改readme.md
。由于B
、C
两次提交都是修改同一功能,因此需要压缩。
C readme.md (v3) index.txt (v1) <-- master <-- HEAD
/
B readme.md (v2) index.txt (v1)
/
A readme.md (v1)
———— HEAD ———————— Index ———————— Working Directory
readme.md (v3) readme.md (v3) readme.md (v3)
index.txt (v1) index.txt (v1) index.txt (v1)
运行git reset --soft HEAD^^
将HEAD
移动到提交A
上。
C readme.md (v3) index.txt (v1)
/
B readme.md (v2) index.txt (v1)
/
A readme.md (v1) <-- master <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v3) readme.md (v3)
index.txt (v1) index.txt (v1)
运行git commit
将B
、C
的修改压缩为一次新的提交D
。
C readme.md (v3) index.txt (v1)
|
B readme.md (v2) index.txt (v1)
| D readme.md (v3) index.txt (v1) <-- master <-- HEAD
| /
A readme.md (v1)
———— HEAD ———————— Index ———————— Working Directory
readme.md (v3) readme.md (v3) readme.md (v3)
index.txt (v1) index.txt (v1) index.txt (v1)
# 分支
分支的提交历史如下,当前HEAD
指向master
分支。
B readme.md (v2) <-- master <-- HEAD
/
A readme.md (v1) <-- dev
———— HEAD ———————— Index ———————— Working Directory
readme.md (v2) readme.md (v2) readme.md (v2)
;git checkout dev
移动HEAD
指向dev
分支,不同于git reset --hard HEAD
,仅仅移动HEAD
自身,且checkout
会检查是否有未提交的修改,防止修改丢失。
B readme.md (v2) <-- master
/
A readme.md (v1) <-- dev <-- HEAD
———— HEAD ———————— Index ———————— Working Directory
readme.md (v1) readme.md (v1) readme.md (v1)
# 高级合并
# 选项参数
--continue
:某些情况下合并产生冲突,Git
会暂停下来等待解决冲突。一种方式是git add
将冲突文件标记为已解决,再次提交即可。另一种方式是标记后执行git merge --continue
继续合并,若没有冲突产生,Git
会自动创建一个合并提交--abort
:尝试恢复到合并前的状态。当工作目录中有未提交的修改,git merge --abort
某些情况下无法重现合并前的状态。因此建议合并前保持干净的工作目录,可将部分修改通过git stash
储藏,解决冲突后再释放出来-Xignore-all-space
:合并过来的分支和当前分支某一文件除了空格以外没有任何区别的时候,忽略合并过来的分支的那个文件。即若A
合并B
的修改,A
修改为hello wor ld
,B 修改为hello wo rld
,两次修改等效,且忽略合并过来的修改B
-Xignore-space-change
:忽略空格量的变化。若某行在非末尾的位置有空格而另外一个没有,按照冲突处理。即若A
合并B
的修改,A
修改为hello*world
(暂用*
代替空格),B
修改为hello**world
,则两次修改等效,且忽略合并过来的修改B
。A
修改为helloworld
,B
修改为hello world
,则两次修改冲突
# 冲突状态
查看未合并的文件,运行如下命令。其中包括两者共同祖先的版本1
、当前版本2
(HEAD
)、合并过来的版本3
(MERGE_HEAD
)。
git ls-files -u
100644 ac5336... 1 readme.md
100644 36c569... 2 readme.md
100644 e85456... 3 readme.md
查看未合并版本的具体内容,运行如下命令,其中36c569
为当前版本2
的部分校验和,也可运行一个特别的语法git cat-file -p :2:readme.md
。
git cat-file -p 36c569
冲突文件修改后(不暂存),可运行如下命令查看修改差异。其中--base
为查看修改后的版本与两者共同祖先的版本的差异,--theirs
为查看修改后的版本与合并过来的版本的差异,--ours
为查看修改后的版本和当前版本的差异。
git diff [--base|--ours|--theirs]
# 检出冲突
;master
分支修改了readme.md
文件,dev
分支也修改了readme.md
文件,当前HEAD
指向master
分支,若合并dev
分支的readme.md
修改,将会产生大致如下的冲突。
<<<<<<< HEAD
puts 'hi world'
=======
puts 'hello git'
>>>>>>> dev
此时并不知道保留哪一处修改,缺少更多的参照信息,运行如下命令可查看ours
、base
、theirs
三个版本的差异。可通过配置git config --global merge.conflictstyle diff3
来作为以后合并冲突的默认格式。
git checkout --conflict=diff3 readme.md
cat readme.md
<<<<<<< ours
hi world
||||||| base
hello world
=======
hello git
>>>>>>> theirs
运行如下命令,快速保留某一方的修改。其中--ours
表示保留当前的修改,丢弃掉引入的修改。--theirs
表示保留引入的修改,丢弃掉当前的修改。
git checkout [--ours|--theirs] readme.md
# 合并日志
;master
分支和dev
分支提交历史如下,当前HEAD
指向master
。其中B
、D
提交修改了readme.md
文件,提交C
新增了index.txt
,提交E
新增了file.txt
。
C index.txt (v1) <-- master <-- HEAD
|
B readme.md (v3)
| E file.txt (v1) <-- dev
| /
| D readme.md (v2)
| /
A readme.md (v1)
;git merge dev
合并产生冲突后,可运行如下命令,查看此次合并中包含的每一个分支的所有独立提交。
git log --oneline --left-right HEAD...MERGE_HEAD
< f127062 B
< af9d363 C
> e3eb226 D
> c3ffff1 E
添加--merge
选项,只显示任何一边接触了合并冲突文件的提交。也可再添加-p
选项查看所有冲突文件的区别。
git log --oneline --left-right --merge
< f127062 B
> e3eb226 D
# 策略合并
运行如下命令。若有某些可以合并的修改,Git
会直接合并,某些有冲突的修改,Git
根据选项参数选择特定的修改。-Xours
选项即产生冲突优先使用当前HEAD
修改,-Xtheirs
选项即优先使用dev
分支修改,其余可合并的修改直接合并。
git merge [-Xours|-Xtheirs] dev
# 重用合并记录
重用合并记录(reuse recorded resolution
)即让Git
记住解决一个块冲突的方法,下一次看到相同的冲突时自动解决它。
配置如下选项启用rerere
功能,也可不配置,在.git
目录下新建rr-cache
文件夹即可。
git config --local rerere.enabled true
# 记录冲突
各分支的readme.md
修改内容如下。
C readme.md (hello git) <-- master <-- HEAD
/
A readme.md (hello world) —— B readme.md (hi world) <-- dev
;master
分支下合并dev
分支readme.md
的修改。其中Recorded preimage
表示Git
已经开始跟踪此次合并了。
git merge dev
Auto-merging readme.md
CONFLICT (content): Merge conflict in readme.md
Recorded preimage for 'readme.md'
Automatic merge failed; ...
查看readme.md
文件。
cat readme.md
<<<<<<< HEAD
hello git
=======
hi world
>>>>>>> dev
查看.git/rr-cache/0ff6a9/preimage
下记录的合并冲突前的版本,其中0ff6a9
为此次冲突的校验和。
<<<<<<<
hello git
=======
hi world
>>>>>>>
处理readme.md
,将其标记为已解决并提交。其中Recorded resolution
表示记录了此次冲突的解决方法。
git add readme.md
git commit -m 'D'
Recorded resolution for 'readme.md'.
[master ...] D
冲突解决后提交历史如下。
B readme.md (hi world) <-- dev
/ \
A readme.md (hello world) D readme.md (hi git) <-- master <-- HEAD
\ /
C readme.md (hello git)
查看.git/rr-cache/0ff6a9/postimage
下记录的合并冲突后的版本。本质上当Git
看到一个readme.md
文件的一个块冲突中有hi world
在一边、hello git
在另一边,它会将其解决为hi git
。
hi git
撤销合并提交D
,然后再次合并dev
的修改。其中Resolved ... using previous resolution
表示使用了之前的合并记录。
git reset --hard HEAD^
git merge dev
Auto-merging readme.md
CONFLICT (content): Merge conflict in readme.md
Resolved 'readme.md' using previous resolution.
Automatic merge failed; ...
查看使用了合并记录后的readme.md
。
cat readme.md
hi git
执行git merge --abort
撤销本次合并,回到合并前的状态。再来看看将dev
的修改变基到master
的情况。
git switch dev
git rebase master
...
Resolved 'readme.md' using previous resolution.
....
执行git add
将文件标记为已解决,执行git rebase --continue
继续变基,此次变基记录记为B'
,master
成为dev
分支的直接上游。
B' <-- dev <-- HEAD
|
C <-- master
| B
| /
A
# 恢复冲突
;Git
自动解决冲突,但是可能你已经忘记冲突时readme.md
的状态了,运行如下命令,恢复至冲突时的readme.me
状态。
git checkout --conflict=merge readme.md
cat readme.md
<<<<<<< ours
hello git
=======
hi world
>>>>>>> theirs
# 应用场景
分支提交历史如下。
E —— F <-- dev
/
A —— B —— C <-- master
某种情况下要合并master
分支的修改,来测试dev
分支的修改是否影响了master
分支的部分功能。
E —— F —— G <-- dev
/ /
A —— B ———————— C <-- master
可能多次从master
分支合并至dev
分支进行测试,最终master
分支合并了dev
分支的修改。查看master
分支提交历史,可能看见很多的合并记录,历史树看起来并不直观。
E —— F —— G —— H —— K —— L <-- dev
/ / / \
A —— B ———————— C ———————— J —— M —— N <-- master
其实dev
分支每次合并master
分支完成测试后,可以丢弃掉那次合并记录,因为rerere
已经记录了冲突的解决方法,不必担心以后再次合并,最终dev
分支完成开发合并至master
,提交历史树如下。
E —— F —— H —— L <-- dev
/ \
A —— B —— C —— J —— M —— N <-- master
# 还原提交
提交包括常规提交、合并提交,常规提交只有一个父提交对象,而合并提交有两个或者多个的父提交对象。git reset --hard
可取消某次提交的修改,但是对于已经推送至远程仓库的提交,会造成很大的问题。另一种解决方式就是还原提交,即生成一个新的提交,此提交将会撤销指定的某次提交的修改。
# 常规提交
若分支提交历史如下,其中HEAD
指向master
。
A —— B (24e888) —— C <-- master <-- HEAD
执行如下命令取消B
的修改,也可执行git revert HEAD^
。
git revert 24e888
运行后Git
会打开Vim
编辑器,可修改此次新提交的提交信息。
Revert "B"
This reverts commit 24e888....
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
...
最终提交历史如下。
A —— B —— C —— ^B <-- master <-- HEAD
# 合并提交
各分支的提交历史大致如下,HEAD
指向master
分支,其中D
为合并提交,其余均为常规提交。
A —— B —— C —— D (110c0d6) —— G <-- master <-- HEAD
\ /
E —— F <-- dev
某些情况下发现dev
分支合并进来的修改存在重大缺陷,需要丢弃掉dev
分支的修改,即E
、F
两次提交的修改。若运行git resert 110c0d6
撤销提交D
,但是Git
不知道撤销哪一个分支的修改,此时需告诉Git
保留哪一个分支的修改,而另一个分支的修改将被撤销。运行如下命令创建还原提交^D
,其中-m
表示此次取消的是一次合并提交,1
表示保留提交D
的第一父提交C
的修改并撤销E
、F
的修改。
git revert -m 1 110c0d6
A —— B —— C —— D (110c0d6) —— G —— ^D <-- master <-- HEAD
\ /
E —— F <-- dev
当dev
分支的重大缺陷修复后,可再次合并进master
。可能直觉上觉得E
、F
、H
的修改均合并进了master
分支,但是注意由于提交^D
撤销了E
、F
的修改,所以master
并不包含E
、F
的修改,即只有H
的修改合并进了master
。
A —— B —— C —— D —— G —— ^D —— I <-- master <-- HEAD
\ / /
E —— F —————————————— H <-- dev
解决上述情况的办法也很容易,用撤销解决撤销,即先撤销^D
的修改。其中d6d7365
为提交^D
的部分校验和,也可执行git revert HEAD
。
git revert d6d7365
A —— B —— C —— D —— G —— ^D (d6d7365) <-- master <-- HEAD
\ /
E —— F ——— H <-- dev
再执行git merge
合并dev
的修改。
A —— B —— C —— D —— G —— ^D —— ^^D —— I <-- master <-- HEAD
\ / /
E —— F ————————————————————— H <-- dev
# 调试工具
# 文件标注
查看某一文件每一行的最后一次修改的提交。输出的每一行中第一个字段是最后一次修改此行的提交的部分校验和,^
开头表示的是此文件第一次加入项目时的提交。括号中的字段分别为作者、提交时间(含时区)、行号。最后为某一行的内容。
查看文件第二行到第五行。其中-L
表示范围,2,5
表示第二行到第五行,都是闭区间,不指定-L
参数和范围则查看文件所有行。
git blame -L 2,5 readme.md
^4832fe2 (xx 2021-01-12 10:31:28 +0800 2) hello
9f6560e4 (xx 2021-01-13 10:32:29 +0800 3) world
cd564aa5 (xx 2021-01-14 10:33:30 +0800 4) and
7f3a6645 (xx 2021-01-15 10:34:31 +0800 5) git
范围也可指定行的个数,+
表示往下,-
表示往上。如下表示从第二行往下三行,则输出行号为2
、3
、4
的行的提交信息。
git blame -L 2,+3 readme.md
# 二分查找
;bisect
命令会对提交历史进行二分查找来帮助尽快找到是哪一个提交引入了问题。
提交历史如下,提交C101
收到了bug
反馈,但是在提交C1
并未存在此bug
,可以确定的是在提交C1
与C101
之间的提交引入了bug
。
C1 (d564aa) —— C2 ··· C50 ··· C100 —— C101 <-- master <-- HEAD
运行如下命令,选择C1
到C101
的提交历史进行二分查找排查,代码库会切换到范围正当中的那一次提交C51
,其中d564aa
为提交C1
的部分校验和。
git bisect start HEAD d564aa
提交C51
下复现bug
,并不存在,说明在C52
到C101
之间,good
表示本次提交C51
没有问题。
git bisect good
;Git
自动切换到C52
与C101
的中点提交C76
,bad
表示本次提交C76
有问题。
git bisect bad
不断重复此过程,最终查找到出问题的那次提交,并打印出那次提交的详细信息。
857293... is the first bad commit
commit 857293...
Author: ...
Date: ...
执行如下命令,退出查错,回到最近一次的代码提交。
git bisect reset
# 打包
;Git
可将分支内容打包成一个二进制文件,用于邮件或其他方式传输。
打包master
分支所有提交历史,其中repo.bundle
为打包生成的二进制文件名。
git bundle create repo.bundle HEAD master
克隆打包生成的二进制文件,其中repos
为自定义的仓库名,也可不指定,如下则默认为repo
。
git clone repo.bundle repos
打包区间范围的提交记录,其中HEAD^^..HEAD
左开右闭区间,即D
、E
两次提交记录。
git bundle create repo.bundle HEAD^^..HEAD master
A —— B —— C —— D —— E <--master <-- HEAD
检查文件是否是合法的Git
包,是否拥有共同的祖先从而导入。其中The bundle requires this ref
表示此包父提交对象校验和为99884a
。
git bundle verify repo.bundle
...
The bundle requires this ref:
99884a...
查看包可导入的分支。
git bundle list-heads repo.bundle
导入包中master
分支的提交到本地dev
分支。
git fetch repo.bundle master:dev
# 凭据存储
;Git
使用HTTP
协议访问远程仓库进行操作时,每一个连接都是需要用户名和密码的。
倘若每次推送或者拉取都输入用户名和密码,显得非常繁琐,Git
提供了一个凭据系统来解决此种问题,部分选项如下。
- 默认:凭据都不缓存,每一次连接都会询问用户名和密码
cache
:将凭据存放在内存中一段时间,密码不会被存储在磁盘中,并且15
分钟后从内存中清除。注意此选项不适用于windows
系统,因为此选项是通过unix
套接字进行通信store
:凭据明文存放在磁盘中永不过期。默认路径为C:/Users/{username}/.git-credentials
manager
:凭据管理至windows
系统中的凭据管理器,可在控制面板中的用户账户的凭据管理器中查看
运行如下命令配置上述选项。
git config --global credential.helper [cache|store|manager]
一般安装Git
会默认使用manager
方式,其中Enable Git Credential Manager
即开启manager
方式
如下窗口输入用户名和密码会被凭据管理器记录。
# 子模块
子模块即一个Git
仓库作为另一个Git
仓库的子目录,两个仓库相互独立,同时一个仓库又依赖另一个仓库。
# 添加
仓库添加子模块,默认子模块会放到与仓库同名的目录中,即主项目中会生成子目录subrepo
。
git submodule add https://github.com/username/subrepo.git
也可在命令结尾指定子目录名称或路径,如下主项目中会生成子目录subrepos
。
git submodule add https://github.com/username/subrepo.git subrepos
注意子模块默认克隆仓库的master
分支,运行如下命令克隆具体分支,其中-b
为--branch
简写,dev
为远程仓库subrepo
的分支。
git submodule add -b dev https://github.com/username/subrepo.git
# 查看
主项目下运行git status
查看状态,注意首次添加子模块,会生成.gitmodules
文件且均为暂存状态,同时.gitmodules
也会被Git
跟踪并管理。
git status
...
Changes to be committed:
...
new file: .gitmodules
new file: subrepo
其中.gitmodules
文件保存了子模块项目URL
和本地目录之间的映射关系。注意主项目中有多个子模块,.gitmodules
就会存在多条记录。
cat .gitmodules
[submodule "subrepo"]
path = subrepo
url = https://github.com/username/subrepo.git
查看暂存区与最近版本库之间的差异,虽然subrepo
是主项目目录的子目录,但是Git
并不会跟踪它的内容,而是将其看做仓库中的一个特殊提交。
git diff --cached
...
+[submodule "subrepo"]
+ path = subrepo
+ url = https://github.com/username/subrepo.git
...
+Subproject commit 3b8ad09...
也可指定--submodule
选项查看差异。
git diff --cached --submodule
...
+[submodule "subrepo"]
+ path = subrepo
+ url = https://github.com/username/subrepo.git
Submodule subrepo 0000000...3b8ad09 (new submodule)
# 提交
主项目下运行git commit
提交,其中160000
是Git
中一种特殊模式,本质上是子模块目录指向某次提交。
git commit -am 'message'
...
create mode 100644 .gitmodules
create mode 160000 subrepo
运行如下命令,查看当前提交树对象,其中子目录subrepo
中的当前版本指向提交记录3b8ad09
。
git ls-tree -r HEAD
100644 blob 9a5259... .gitmodules
160000 commit 3b8ad09... subrepo
# 克隆
克隆仓库repo
,其中仓库repo
含有子模块subrepo
,克隆后默认会包含子模块目录subrepo
,但是其中都没有文件。
git clone https://github.com/username/repo.git
运行如下命令,初始化本地配置。若想验证此方式的执行情况,提供一种思路,首先克隆仓库repo
,然后进入仓库内.git
文件夹,运行git init
,暂存所有文件并提交,即用Git
跟踪Git
的改动,然后返回上级,执行如下命令,最后进入.git
并执行git diff
,则能看到此命令执行后相关文件改动,此种方式也可验证Git
其他命令的执行情况。
git submodule init
Submodule 'subrepo' ( ... ) registered for path 'subrepo'
初始化本地配置后,运行如下命令,抓取子模块内对应版本的文件。注意Git
将会获得这些改动并更新子目录中的文件,但是会将子仓库留在一个游离的HEAD
的状态,即子仓库不存在分支,没有本地分支来跟踪改动,执行git checkout
检出其相应的工作分支即可。
git submodule update
也可运行如下命令,自动初始化并更新仓库中每一个子模块。
git clone --recursive https://github.com/username/repo.git
# 更新
可能仅使用子模块项目并不时地获取更新,但是并不在子模块进行修改。
如下分别在子模块内部和外部更新,其中主项目repo
下包括子模块目录subrepo
。
# 内部
子模块subrepo
运行git fetch
和git merge
合并最新代码,也可查看子模块subrepo
的代码提交历史和差异。注意若此时无法fetch
最新代码,可能由于子模块subrepo
处于游离的HEAD
状态,需运行git checkout
检出工作分支。
合并更新后返回主项目repo
,子模块subrepo
的更新会被仓库repo
记录为一次新的特殊修改,可运行git status
查看工作目录状态,或者运行如下命令,查看目录subrepo
指向的提交记录的改变以及当前指向的提交的提交信息。其中6cc07e3..81afae7
表示子模块subrepo
指向的提交记录由6cc07e3
更新为81afae7
,当前提交记录81afae7
的提交信息为update subrepo readme.md
。
git diff --submodule
Submodule subrepo 6cc07e3..81afae7:
> update subrepo readme.md
# 外部
主项目repo
中再进入目录subrepo
更新代码,稍微显得麻烦,运行如下命令,在仓库repo
下更新所有子模块的代码。
git submodule update --remote
可能子模块较多,仅仅只需要更新某一个子模块,如下仅更新子模块subrepo
。
git submodule update --remote subrepo
运行如下命令,将子模块subrepo
的版本映射修改为另一个分支,其中dev
为分支名,修改后再运行git submodule update --remote subrepo
更新即可。
git config -f .gitmodules submodule.subrepo.branch dev
# 推送
子模块subrepo
修改部分文件,若仓库subrepo
处于游离的HEAD
状态,也需检出相应的工作分支,提交后不推送改动。在主项目repo
中提交并推送,其他人更新子模块subrepo
时,无法获取到子模块所依赖的改动,那些改动只存在本地拷贝中。
运行如下命令,Git
在推送主项目前会检查子模块是否已经推送,若任何子模块的改动没有推送则push
会失败。
git push --recurse-submodules=check
The following submodule paths contain changes that can
not be found on any remote:
subrepo
...
运行如下命令,在推送主项目前推送子模块的改动。注意若子模块因为某些原因推送失败,主项目也会推送失败。
git push --recurse-submodules=on-demand
# 合并
其他人同时改动了子模块的引用,可能会遇到部分问题,需要做一些工作来修复。
# 快进式
若主项目repo
下包括子项目subrepo
,用户A
和用户B
均克隆了远程仓库repo
。用户A
修改子项目下文件,提交并推送了subrepo
和repo
。用户B
不做任何修改,执行git pull
,子项目subrepo
将做一个快进式合并,Git
会选择之后的提交来合并。
git pull
...
Fetching submodule subrepo
...
Fast-forward
subrepo
# 子模块冲突
若主项目repo
下包括子项目subrepo
,用户A
和用户B
均克隆了远程仓库repo
。其中子项目subrepo
含文件readme.md (v1)
。
用户A
修改readme.md (v2)
,同时在repo
和subrepo
均提交和推送了修改。
用户B
修改readme.md (v3)
,同时在repo
和subrepo
均提交了修改,然后再拉取用户A
推送的修改。
git pull
...
Failed to merge submodule subrepo (merge following commits not found)
Auto-merging subrepo
CONFLICT (submodule): Merge conflict in subrepo
...
可查看subrepo
子模块产生了冲突,其中merge following commits not found
表示未找到合并提交记录,即Git
未找到子模块内readme.md
文件v2
和v3
的合并提交版本。
运行git diff
,查看产生冲突的两个提交记录的校验和,其中2ceb726
为用户B
的提交校验和,29b9770
为用户A
的提交校验和。
git diff
index 2ceb726,29b9770..0000000
进入子模块目录subrepo
,当前应该指向提交2ceb726
,运行如下命令创建用户A
的修改的一个临时分支。
git branch theirs 29b9770
合并theirs
分支,在此得到了一个真正的合并冲突。修改后将文件readme.md (v4)
标记为已解决,然后提交并推送子项目subrepo
的修改,最后返回至主项目repo
提交并推送修改。
git merge theirs
...
CONFLICT (content): Merge conflict in readme.md
...
另一种合并方式即在用户A
提交和推送后,用户B
的readme.md (v3)
修改提交后,执行git pull
合并v2
和v3
为v4
,再推送合并版本,然后返回主项目repo
提交。
最后执行git pull
,此时引入用户A
的readme.md (v2)
修改,由于Git
找到了v2
和v3
的合并提交版本v4
,故此合并仅是一次简单的快进式合并。
git pull
...
Fast-forwarding submodule subrepo
Auto-merging subrepo
Merge made by the 'recursive' strategy.
# 删除
若主项目repo
删除子模块subrepo
,首先运行如下命令,Git
会删除子模块目录subrepo
,并且再更新.gitmodules
文件,同时暂存以上修改。
git rm -f subrepo
然后在.git/config
中删减子模块subrepo
相关内容。
[submodule "subrepo"]
url = https://github.com/username/subrepo.git
最后在主项目repo
下提交并推送修改。
# 其他
# 子模块遍历
在主项目中运行如下命令,Git
会在每一个子模块中运行git checkout master
。
git submodule foreach git checkout master
# 切换分支
主项目repo
暂不添加子模块,检出一个新分支dev
后再添加子模块subrepo
并提交。
git checkout -b dev
git submodule add https://github.com/username/subrepo.git
git commit -am 'message'
此时切换到master
分支,警告不能删除非空目录subrepo
。
git checkout master
warning: unable to rmdir 'subrepo': Directory not empty
Switched to branch 'master'
...
;master
分支应该没有子模块,但是却仍然有一个未跟踪的子模块目录subrepo
。
git status
...
Untracked files:
...
subrepo/
运行rm -r subrepo
删除此子模块目录即可,再git switch
切换回dev
分支。但是进入 subrepo
目录并没有任何文件,运行如下命令来重新建立和填充子模块subrepo
。
git submodule update --init
Submodule path 'subrepo': checked out ...
# 子目录
若仓库repo
含子目录subrepo
,subrepo
中含部分文件,仓库repo
此时含master
和dev
分支,当前HEAD
指向dev
分支。
此时添加子模块subrepo
,但是由于subrepo
目录存在,添加失败。
git submodule add https://github.com/username/subrepo.git
'subrepo' already exists in the index
运行如下命令,删除subrepo
目录后再添加子模块,然后提交修改。
git rm -r subrepo
切换分支到master
,注意子目录subrepo
中的文件与dev
分支的子模块中的文件合并了。
解决方式如下,首先删除subrepo
目录,然后撤销修改,最终master
分支的subrepo
目录恢复。
rm -r subrepo
git checkout .
# 🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub (opens new window) / Gitee (opens new window)、GitHub Pages (opens new window)、掘金 (opens new window)、CSDN (opens new window) 同步更新,欢迎关注😉~