# 比较全面的 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分支中的提交,即CD提交。

git log master..dev

  也可查看即将推送到远端的某些提交,如下查看当前分支推送到远端master分支的提交有哪些。

git log origin/master..HEAD

# 排除

  也可加上^字符或者--not来指明不希望包含某个分支的提交,如下三个命令等价。

git log A..B
git log ^A B
git log B --not A

  查看AB包含的但是C不包含的提交。

git log A B ^C
git log A B --not C

# 三点

  筛选被两个中的一个包含但又不包括两者同时包含的提交。如下查看masterdev中包含但是不包括两者共有提交,即EFCD

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:暂存文件,键入2u后输入文件对应的数字暂存文件(多个文件用,隔开),每个文件前面的*意味着选中的文件将会被暂存,>>提示符后不输入任何东西并回车表示执行此次命令
  • 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

  某一行满足多个条件,如下命令满足某一行包括AB,其中--or可省略。

git grep -e A --or -e B

  某一行包括A并且包括B

git grep -e A --and -e B

  某一行包括AB或者AC

git grep -e A --and \( -e B -e C \)

# 交互式变基

  用于变基时对提交历史记录进行复杂的修改,可用于调整提交顺序、改变提交中的提交信息或修改文件、压缩提交或拆分提交、也可用于移除提交等。

# 提交区间

D  d34...  <-- master HEAD
|
C  c23...
|
B  b12...
|
A  <-- HEAD~3

  运行如下命令显示交互式变基界面,其中-i选项后为提交记录区间,HEAD~3HEAD范围的提交记录,左开又闭,即为BCD。区间终点可省略,默认为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

# 提交顺序

  调整编辑器内提交记录顺序,保存并退出。如下将提交顺序由BCD调整为DBC

pick d34... D
pick b12... B
pick c23... C

# 修改提交信息

  将pick修改为reword,保存并退出。如下将修改CD的提交信息。

pick b12... B
reword c23... C
reword d34... D

  保存并退出后进入C的提交信息编辑界面,然后再进入D的提交信息编辑界面。运行git log --oneline查看提交历史。

d89... D'
c36... C'
b12... B

# 压缩提交

  将多个提交压缩为一个提交,如下将CD压缩到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 commitIndex中的内容保存为快照,然后创建指向快照的提交对象,更新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指向v2HEAD再指向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.mdHEAD复制到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。由于BC两次提交都是修改同一功能,因此需要压缩。

      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 commitBC的修改压缩为一次新的提交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,则两次修改等效,且忽略合并过来的修改BA修改为helloworldB修改为hello world,则两次修改冲突

# 冲突状态

  查看未合并的文件,运行如下命令。其中包括两者共同祖先的版本1、当前版本2HEAD)、合并过来的版本3MERGE_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

  此时并不知道保留哪一处修改,缺少更多的参照信息,运行如下命令可查看oursbasetheirs三个版本的差异。可通过配置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。其中BD提交修改了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分支的修改,即EF两次提交的修改。若运行git resert 110c0d6撤销提交D,但是Git不知道撤销哪一个分支的修改,此时需告诉Git保留哪一个分支的修改,而另一个分支的修改将被撤销。运行如下命令创建还原提交^D,其中-m表示此次取消的是一次合并提交,1表示保留提交D的第一父提交C的修改并撤销EF的修改。

git revert -m 1 110c0d6

A —— B —— C —— D (110c0d6) —— G —— ^D <-- master <-- HEAD
      \       /
       E —— F <-- dev

  当dev分支的重大缺陷修复后,可再次合并进master。可能直觉上觉得EFH的修改均合并进了master分支,但是注意由于提交^D撤销了EF的修改,所以master并不包含EF的修改,即只有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

  范围也可指定行的个数,+表示往下,-表示往上。如下表示从第二行往下三行,则输出行号为234的行的提交信息。

git blame -L 2,+3 readme.md

# 二分查找

  ;bisect命令会对提交历史进行二分查找来帮助尽快找到是哪一个提交引入了问题。

  提交历史如下,提交C101收到了bug反馈,但是在提交C1并未存在此bug,可以确定的是在提交C1C101之间的提交引入了bug

C1 (d564aa) —— C2 ··· C50 ··· C100 —— C101 <-- master <-- HEAD

  运行如下命令,选择C1C101的提交历史进行二分查找排查,代码库会切换到范围正当中的那一次提交C51,其中d564aa为提交C1的部分校验和。

git bisect start HEAD d564aa

  提交C51下复现bug,并不存在,说明在C52C101之间,good表示本次提交C51没有问题。

git bisect good

  ;Git自动切换到C52C101的中点提交C76bad表示本次提交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左开右闭区间,即DE两次提交记录。

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提交,其中160000Git中一种特殊模式,本质上是子模块目录指向某次提交。

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 fetchgit 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修改子项目下文件,提交并推送了subreporepo。用户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),同时在reposubrepo均提交和推送了修改。

  用户B修改readme.md (v3),同时在reposubrepo均提交了修改,然后再拉取用户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文件v2v3的合并提交版本。

   运行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提交和推送后,用户Breadme.md (v3)修改提交后,执行git pull合并v2v3v4,再推送合并版本,然后返回主项目repo提交。

  最后执行git pull,此时引入用户Areadme.md (v2)修改,由于Git找到了v2v3的合并提交版本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含子目录subreposubrepo中含部分文件,仓库repo此时含masterdev分支,当前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) 同步更新,欢迎关注😉~

最后更新时间: 3/6/2022, 9:06:37 PM