现代工业体系中的项目运作从来不是一个人独角戏,必定是多人协作。软件工程也不是计算机产业上古时期某个程序员两三天弄出来的小demo。这一章节我们将讨论如何使用Git参与到项目的协作中来。

我试图使用非常简单的图文来向大家描述Git操作命令,每个参数都有示例并告诉大家这些参数的区别。在进行详细的说明之前,大家可以通过图2-1对Git仓库之间的流转有一个整体的了解。

Git操作命令和管理_java

图2-1 Git操作与仓库之间的关系

2.1 与Git远程仓库交互

要参与到Git项目的协作,我们首先需要了解远程仓库。远程仓库是指托管在网络上的Git仓库,可能有多个,有些是只读镜像,另外的是可写。同他人协作开发某个项目时,需要管理这些远程仓库,以便推送或拉取数据,分享各自的工作进展。管理远程仓库的工作,包括添加远程库,移除废弃的远程库。

本小节我们将详细讨论与Git远程库的交互,我们将以场景的方式来描述Git远程仓库的使用。

2.1.1:在Git服务器中添加Git客户端的SSH KEY
# 在Git客户端中执行如下命令;
$ ssh-keygen -t rsa -b 4096 -f  keyFileName -q -N "newPasswd" -C "this is a comment."
# -t key的类型,默认rsa;-b key的字节数,默认2048;-f key写在哪个文件中
# -C 注释;-q 静默执行;-N 设置rsa新密码
# 查看当前目录将会有两个文件
$ ls
keyFileName  keyFileName.pub
$ cat keyFileName.pub

公钥字符串,将如下字符串粘贴至Git服务器中:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1wPUf9iInniRzEjhl3B9IOlsNnnPs7habIftsrjyiDmNEBOrkHNgoLquY4TTk2eZE8ljwzSSk3IYmFaBf37YNqb1gZIoQQ2z51fhCtNEBnJIVe7ZebaEd/0UMpb9c5qt9a/QbAFWMD4udixq/hgcGCQYnRq0oOk3p6wXJrllDqf/mk97BTVboXXrinPtS5MS2DLT5wd8rgkiZSJlXVp/6jaXNGM44lYK8qq1vGwRFywXmyYALl1sV43ktybqH6zwq3ZIsDAVDLST7FZX8Qp+Flg0II6+BGWelXvhHTfDSrkxL5UD0jHfzTtma+FOx9NNL5YesngqBuyWeRmozQfm/+uv2FQxVQ3CFv/+3njW12+eRAfb6eQzQgnahKlGKY7xoijc6agdWAiEP13jCOlfgIs4ZEYMGOoA+vlQu21ENTxwbuwMGfrZ8xizPsrHQtcWv/75h0xlZWKJ6P8OuAGel86l3lyA2YdBNNQfxsXMK4ECiYV70LnYJPhevR5ekOc4jZbomrqu16ex98TQpk2h+S2ju0OFzlSc9E3poufSd3Z5ZhyzOsDm2lFTg8m5KELd90LIm9aBRQ628SxqDnYHV+84PWPDNrVsu0o6bh//ln1WT5J9WU16U5HslYPl9NCFbbAXJHti63tsKz99YrCDFezRIk0nKPHSfwMZjKJrqSw== this is a comment.
2.1.2:查看当前目录是否与远程仓库相关联
# 查看当前目录是否是Git仓库以及Git仓库状态
$ git status
# 查看当前仓库是否添加了远程仓库
$ git remote -v
2.1.3:克隆一个远程仓库
# 命令中的 “gitHubAlias” 为自定义的名字,clone完成之后,仓库将处于这个自定义名字的文件夹下。
# 如果不填,那么这个仓库的文件夹名默认为远程仓库的名字。
# Git 支持许多数据传输协议。之前的例子使用的是 git:// 协议,不过你也可以用 http(s):// 或者 
# user@server:/path.git 表示的 SSH 传输协议。
$ git clone https://github.com/git/git.git  gitHubAlias
2.1.4:如果本地没有仓库,需要新建本地仓库,并且将本地仓库与空的远程仓库相关联。
# 创建README.md文件,将引号中的字符串写入该文件中
$ echo "# gittest" >> README.md
# 初始化一个本地仓库
$ git init
# 让Git跟踪监控README.md文件,如果不这样做,Git将不会管这个文件。
$ git add README.md
# 将被跟踪文件的变更提交到本地仓库
$ git commit -m "first commit"
# 在本地仓库添加远程仓库。一个本地仓库可添加多个远程仓库。对本地仓库来说只是添加了一个别名而已。
$ git remote add origin https://github.com/niroshea/gittest.git
# 将本地仓库中的提交推送至远程仓库(别名origin)的master分支中。-u 参数:设置默认上传流。
$ git push -u origin master

git push 介绍

# git push 使用本地的对应分支来更新对应的远程分支。
$ git push <远程仓库> <本地分支名>:<远程分支名>
# 注意: 命令中的本地分支是指将要被推送到远端的分支,而远程分支是指推送的目标分支,即将本地分支合并到远程分支。 
# 如果省略远程分支名,则表示将本地分支推送与之存在”追踪关系”的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。
$ git push origin master
# 如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支,这条命令是删除远程master分支。
$ git push origin :master
# 等同于
$ git push origin --delete master
# 如果当前分支与远程分支之间存在追踪关系(即分支名相同),则本地分支和远程分支都可以省略。
$ git push origin
# 如果当前分支只有一个追踪分支,那么主机名都可以省略。
$ git push
# 如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机,这样后面就可以不加任何参数使用git push。
$ git push -u origin master
# 还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用–all选项。
$ git push --all origin
# 如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做git pull合并差异,然后再推送到远程主机。这时,如果你一定要推送,可以使用–force选项。应该尽量避免使用–force选项。
$ git push --force origin
# 最后,git push不会推送标签(tag),除非使用–tags选项。
$ git push origin --tags
2.1.5:本地已有仓库,需将本地仓库与远程仓库相关联,并将本地仓库中的提交推送至空的远程仓库。
# 将本地仓库与远程仓库(别名origin)相关联
$ git remote add originhttps://github.com/niroshea/gittest.git
# 将本地仓库中的提交推送至远程仓库(别名origin)的master分支中。-u 参数:设置默认上传流。
$ git push -u origin master
2.1.6:如果远程仓库不为空,且内容不可忽略,需将本地仓库与远程仓库合并并且相关联。
$ git remote add origin https://github.com/niroshea/gittest.git
# 将远程仓库的所有提交合并至本地仓库。
$ git pull origin

git pull 介绍

# git pull 获取并合并其他的仓库,或者本地的其他分支。
$ git pull <远程仓库> <远程分支>:<本地分支>
# 将origin厂库的master分支拉取并合并到本地的my_test分支上。
$ git pull origin master:my_test
# 如果省略本地分支,则将自动合并到当前所在分支上。如下:
$ git pull origin master
2.1.7:需要拉取远程仓库到本地仓库,先查看区别,再进行合并。
$ git fetch origin master:temp 
#在本地新建一个temp分支,并将远程origin仓库的master分支代码下载到本地temp分支
$ git diff temp 
#来比较本地代码与刚刚从远程下载下来的代码的区别
$ git merge temp
#合并temp分支到本地的master分支
$ git branch -d temp
#如果不想保留temp分支 可以用这步删除
2.1.8:拉取远程非master分支
# 拉取远程仓库origin的dev分支并且合并至本地master分支
$ git pull origin dev:master
# 修改之后,如果需要提交至远程仓库,执行如下命令即可。
$ git push origin master:dev

2.2 创建、删除本地Git仓库

2.2.1 创建Git仓库、添加追踪
# 要对现有的某个项目开始用 Git 管理,只需到此项目所在的目录,执行:
$ git init
# 初始化后,在当前目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中。
$ ls -la
drwxr-xr-x  3 nero nero 4096 1月  31 15:40 .
drwxr-xr-x 56 nero nero 4096 1月  31 15:40 ..
drwxr-xr-x  7 nero nero 4096 1月  31 15:40 .git
# 使用 git add 命令告诉 Git 对文件以及文件的修改进行跟踪。
# git add <文件名,或文件夹名>
$ touch test.sh
# Git跟踪新建的文件
$ git add test.sh
$ vim test.sh

# Git跟踪文件的修改
$ git add test.sh
# 在Git仓库根目录执行如下命令,则仓库中所有的文件以及文件的修改都被跟踪。
# 只有被Git跟踪的文件或文件的修改才能被提交。
$ git add .
2.2.2 解除Git纳管
# 如果不希望Git纳管当前目录,执行如下命令:
$ rm -rf .git
2.2.3 忽略某些文件
# 如何让git忽略某些文件呢,编辑 .gitignore
$ cat .gitignore
# 忽略以.a或者.o结尾的文件
*.[oa]
# 忽略以~结尾的文件
*~
# 忽略以.~开头的文件
.~*
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt

# 可能还需要忽略 log,tmp 或者 pid 目录,以及自动生成的文档等等。
# 要养成一开始就设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。

文件 .gitignore 的格式规范如下:

# 1、所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
# 2、可以使用标准的 glob 模式匹配。
# 3、匹配模式最后跟反斜杠(/)说明要忽略的是目录。
# 4、要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
# 所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。

2.3 Git对比差异、提交、撤销操作


Git操作命令和管理_java_02

图2.3-1 Git操作与仓库之间的关系

# 查看当前仓库的状态,哪些文件追踪、未追踪,哪些文件的修改追踪、未追踪,哪些追踪没有提交都会提示出来。
$ git status
2.3.1 对比差异

如图2.3-1所示,如果要查看未跟踪区暂存区间修改了什么地方,可以用 git diff 命令:

# 实际上 git status 的显示比较简单,仅仅是列出了修改过的文件。
# 如果要查看具体修改了什么地方,可以用 git diff 命令
$ git diff
# 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。

如果要查看暂存区本地仓库之间的不同,执行如下命令:

$ git diff --cached
# Git 1.6.1 及更高版本还允许使用 git diff --staged,效果是相同的。
$ git diff --staged
2.3.2 提交更新
# 想要提交暂存区的内容时,执行git commit 将会将暂存区的内容提交至git仓库中
$ git commit
# 执行之后会弹出默认编辑器,让你输入提交的备注。
# Git是用于项目管理的,别人不知道你提交了什么,如何管理项目。尽量详细写提交备注(可以使用中文)。

# 如果实在没什么好说的,备注很简短,使用如下命令(可以使用中文):
$ git commit -m "我的备注"

跳过暂存区,直接提交

# 尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。
# Git 提供了一个跳过使用暂存区域的方式。
# 只要在提交的时候,给 git commit 加上 -a 选项。
# Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤:
$  git commit -a -m "我的备注"

移除文件

# 1、如果该文件未跟踪,通过OS的命令删除文件
$ rm -f test.go
# 2、如果该文件已跟踪、未修改状态,通过git rm删除
$ git rm test.go
# 3、如果该文件已跟踪、修改未跟踪,通过git rm -f 删除
$ git rm -f test.go
# 4、如果该文件已跟踪,想删除Git仓库的追踪,但在本地工作区保留该文件。
$ git rm --cached test.go
# glob正则式批量删除文件
$ git rm log/\*.log
$ git rm \*~

移动文件

# Git 并不跟踪文件移动操作。
# 如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。
# 不过 Git 非常聪明,它会推断出究竟发生了什么。
# 重命名某个文件。
$ git mv test.go dev.go

git mv 命令在Git看来是运行了如下三个命令:

$ mv README.txt README
$ git rm README.txt
$ git add README

# 不管以什么方式,都一样。
2.3.3 撤销操作

Tips:有些撤销操作是不可逆的,所以请务必谨慎小心,一旦失误,就有可能丢失部分工作成果。

修改最后一次提交

# 有时候我们提交完了才发现漏掉了几个文件没有加,或者提交信息写错了。
# 想要撤消刚才的提交操作,可以使用 --amend 选项重新提交
$ git commit -m 'initial commit'
# 如果此时后悔了,进行如下操作。
$ git add test.go dev.go prod.go
$ git commit --amend -m "我的备注"

取消已暂存的文件

$ git add test.go
$ git status

# 位于分支 master
# 要提交的变更:
#   (使用 "git reset HEAD <文件>..." 以取消暂存)

#   修改:     test.go
$ git reset HEAD test.go
# 上述命令只是取消了暂存,但是工作区的内容没有变化

取消对文件的修改

$ git checkout -- test.go
# 所有对文件的修改都没有了,因为我们刚刚把之前版本的文件复制过来重写了此文件

# 任何已经提交到 Git 的都可以被恢复。
# 即便在已经删除的分支中的提交,或者用 --amend 重新改写的提交,都可以被恢复。
# 所以,你可能失去的数据,仅限于没有提交过的,对 Git 来说它们就像从未存在过一样。
2.3.4 回退版本
# 下面两个命令是等同的,官方建议的版本回退方式。
# 执行命令之后,回退了git commit以及git add操作,工作区内容没有被覆盖。
$ git reset HEAD^
$ git reset --mixed HEAD^

# 执行下面的命令之后,回退了git commit操作,工作区内容没有被覆盖。
$ git reset --soft HEAD^
# 执行下面的命令之后,回退了git commit和git add操作,工作区内容被覆盖。
# 此时工作区的内容被上一版本的内容覆盖。
$ git reset --hard HEAD^
# 回退指定版本
# 使用git log命令查看指定版本的hash值
$ git log
commit e5d4a56997db994b48fea0e671909606fef1c0aa (HEAD -> master)
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:47:06 2019 +0800

   changed the version number

commit d7ad2c1823e55499f36eb7603dac1be2fa0ec682
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:39:15 2019 +0800

   removed unnecessary test code

commit 308de3252b50afb27e1daa2d0d974db7743acedd
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:24:38 2019 +0800

   first commit
# 然后当前的需求,执行下列其中一条命令即可
$ git reset 308de3252b50
$ git reset --soft 308de3252b50
$ git reset --hard 308de3252b50

# 这里有一个注意点:如果通过git reset --hard命令回退了版本,但后悔了,咋办?
# 还记得上述git log中的记录吗,只要再次执行 git reset --hard 高版本号,就能回到之前的版本了
$ git reset --hard e5d4a56997d
# 通过上述命令仍然能够回到最新的版本上。但前提是得记得这个提交hash值,否则真的就回不去了:)

2.4 Git查看提交记录

在提交了若干更新之后,又或者克隆了某个项目,想回顾下提交历史,可以使用 git log 命令查看。

$ git log
commit e5d4a56997db994b48fea0e671909606fef1c0aa (HEAD -> master)
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:47:06 2019 +0800

   changed the version number

commit d7ad2c1823e55499f36eb7603dac1be2fa0ec682
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:39:15 2019 +0800

   removed unnecessary test code

commit 308de3252b50afb27e1daa2d0d974db7743acedd
Author: Administrator <nero@gitlab.nero.com>
Date:   Fri Feb 1 09:24:38 2019 +0800

   first commit

# 默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。
# 每次更新都有一个 SHA-1 校验和、作者的名字和电子邮件地址、提交时间,最后缩进一个段落显示提交说明。

git log 参数

# git log 有许多选项可以帮助你搜寻感兴趣的提交,接下来我们介绍些最常用的。
# -p 参数:展开显示每次提交的内容差异;-1 仅显示最近的一次更新
$ git log -p -1
# --stat,仅显示简要的增改行数统计
$ git log --stat
# 常用的 --pretty 选项,可以指定使用完全不同于默认格式的方式展示提交历史。
# 比如用 oneline 将每个提交放在一行显示,这在提交数很大时非常有用。
$ git log --pretty=oneline
# 另外还有 short,full 和 fuller 可以用,展示的信息或多或少有些不同
$ git log --pretty=short
$ git log --pretty=full
$ git log --pretty=fuller


# format,可以定制要显示的记录格式,这样的输出便于后期编程提取分析:
$ git log --pretty=format:"%h - %an, %ar : %s"

常用的格式占位符写法及其代表的意义

选项 说明
   %H 提交对象(commit)的完整哈希字串
   %h 提交对象的简短哈希字串
   %T 树对象(tree)的完整哈希字串
   %t 树对象的简短哈希字串
   %P 父对象(parent)的完整哈希字串
   %p 父对象的简短哈希字串
   %an 作者(author)的名字
   %ae 作者的电子邮件地址
   %ad 作者修订日期(可以用 -date= 选项定制格式)
   %ar 作者修订日期,按多久以前的方式显示
   %cn 提交者(committer)的名字
   %ce 提交者的电子邮件地址
   %cd 提交日期
   %cr 提交日期,按多久以前的方式显示
   %s 提交说明
# 用 oneline 或 format 时结合 --graph 选项,可以看到开头多出一些 ASCII 字符串表示的简单图形
$ git log --pretty=format:"%h %s" --graph

git log 一些其他常用的选项及其释义

选项 说明
   -p 按补丁格式显示每个更新之间的差异。
   --stat 显示每次更新的文件修改统计信息。
   --shortstat 只显示 --stat 中最后的行数修改添加移除统计。
   --name-only 仅在提交信息后显示已修改的文件清单。
   --name-status 显示新增、修改、删除的文件清单。
   --abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
   --relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
   --graph 显示 ASCII 图形表示的分支合并历史。
   --pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和         format(后跟指定格式)。

按照时间作限制的选项,比如 --since--until

# 下面的命令列出所有最近两周内的提交
$ git log --since=2.weeks
# 列出所有2019年以来的提交
$ git log --since="2019-01-01" 
# 列出多长时间之内的提交
$ git log --since="2 years 1 day 3 minutes ago"
# 查看 Git 仓库中,2018 年 10 月期间,nero 提交的但未合并的测试脚本
# (位于项目的 t/ 目录下的文件)
$ git log --pretty="%h - %s" --author=nero --since="2018-10-01" \
   --before="2018-11-01" --no-merges -- t/

使用图形化工具查阅提交历史:gitk、GitKraken

gitk

# 有时候图形化工具更容易展示历史提交的变化,随 Git 一同发布的 gitk 就是这样一种工具。
# 它是用 Tcl/Tk 写成的,基本上相当于 git log 命令的可视化版本。
# 凡是 git log 可以用的选项也都能用在 gitk 上。
# 需要自行安装。

GitKraken

# gitKraken,这款工具操作比较方便,UI也是我喜欢的风格。
# 对没有太多git使用经验的新手比较友好,学习成本相对较低。
# 尤其喜欢的一点就是它的分支和提交非常清晰。
# 官网地址:https://www.gitkraken.com/

2.5 Git中打版本标签

我们在发布某个软件版本(如 v2.1 )的时候,经常需要给某一时间点上的版本打上标签。Git支持上述操作。本节我们将学习如何新建标签、列出所有可用的标签、各种不同类型标签之间的差别。

查看标签

#列出现有标签。在Git仓库目录下执行:
# 显示的标签按字母顺序排列。
$ git tag
   v1.2
   v2.5
# 可以用特定的搜索模式列出符合条件的标签。
$ git tag -l 'v2.1.3.*'
   v2.1.3.1
   v2.1.3.2
   v2.1.3.3

新建标签

# Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。
# 1、轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用。
# 不需要带任何参数
$ git tag v2.0
# 查看v2.0标签信息
$ git show v2.0 
# 2、含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息。
# -a (译注:取 annotated 的首字母)指定标签名字:
$ git tag -a v2.1 -m "标签说明v2.1"
# 包含着标签的名字、电子邮件、日期、标签说明。
$ git show v2.1
# 标签允许使用 GNU Privacy Guard (GPG) 来签署或验证。
# 只需要把之前的 -a 改为 -s (译注: 取 signed 的首字母)(需安装GPG)。
$ git tag -s v2.2 -m '标签说明v2.2'
# 运行 git show 会看到对应的 GPG 签名也附在其内
$ git show v2.2
# 一般我们都建议使用含附注型的标签,以便保留相关信息;
# 如果只是临时性加注标签,或者不需要旁注额外信息,用轻量级标签也没问题

验证标签

# git tag -v [tag-name] (译注:取 verify 的首字母)的方式验证已经签署的标签。
# 此命令会调用 GPG 来验证签名,所以你需要有签署者的公钥,存放在 keyring 中,才能验证
# 如果没有签署者的公钥,会报错。
$ git tag -v v2.2

后期加注标签

# 如果早前的某次提交忘记打标签,可以补打标签
$ git log --pretty=oneline
e5d4a56997db994b48fea0e671909606fef1c0aa (HEAD -> master) commitInfo3 
d7ad2c1823e55499f36eb7603dac1be2fa0ec682 commitInfo2
308de3252b50afb27e1daa2d0d974db7743acedd commitInfo1

# 如果想要给 commitInfo2 打上版本号,只要在git tag 后跟上相应的hash值即可
$ git tag -a v2.4 d7ad2c182
# 使用 git tag 命令查看
$ git tag

上传标签至远程服务器

# 默认情况下,git push 并不会把标签传送到远端服务器上,只有通过显式命令才能分享标签到远端仓库
# 其命令格式如同推送分支,运行 git push origin [tagname] 即可
$ git push origin v2.4
# 一次推送所有本地新增的标签上去,可以使用 --tags 选项
$ git push origin --tags
# 现在,其他人克隆共享仓库或拉取数据同步后,也会看到这些标签。

2.6 Git分支

管理各式远程库分支,定义是否跟踪这些分支。

版本控制器分支功能的意义就在于,我们可以使用分支功能将当前的工作从开发主线上分离开来,然后在不影响主线的同时继续工作。在很多版本控制系统中,这是个昂贵的过程,但对于Git来说却是非常轻松的事情。所以,与大多数版本控制系统不同的是,Git 鼓励在工作流程中频繁使用分支与合并。理解分支的概念并熟练运用后,你才会意识到为什么 Git 是一个如此强大而独特的工具,并从此真正改变你的开发方式。

2.6.1 什么是Git分支

在理解Git分支之前,我们回顾一下Git 是如何存储数据的。前面说过,Git 保存的不是文件差异或者变化量,而只是一系列文件快照。

假设当前工作区有两个新增文件fileA和fileB,通过git add跟踪这两个文件,并且git commit提交,将会触发如下动作:

  • 执行git add fileA fileB

  • Git对这两个文件各做一次校验和计算(SHA-1算法)

  • 将fileA和fileB这两个文件当前的快照存储至Git仓库中(BLOB类型存储,Binary Large Object)

  • 将校验和加入暂存区

  • 执行git commit -m "my comment"

  • Git计算每一个子目录(包括项目根目录)的校验和

  • Git创建tree对象,将每一个子目录校验和保存在该对象中,并存储在Git仓库中

  • Git创建提交对象,包含着指向这个tree对象的指针

在本场景下,一次提交产生了4个对象:一个提交对象,一个tree对象,一个fileA快照对象,一个fileB快照对象。如果进行了2次提交,那么fileA和fileB就有两个版本的快照。当然如果没有发生改变,Git不会进行重复的提交操作。如果fileA没有变化,fileB修改了,新的Git提交对象中的fileA快照指针仍然指向原来的快照,不会新建fileA快照。下图为提交对象与分支的对应关系图。

图2.6.1-1 提交对象与分支

现在我们来理解什么是分支。分支的本质就是指向提交对象的可变指针。如下图所示,分支的本质就是图中的devGroup1指针。

Git操作命令和管理_java_03

图2.6.1-2 分支创建示意图1

上图中只有一个master主分支,有三个指针:HEAD指针,master指针,devGroup1指针。

  • HEAD指针:永远指向当前工作区所在的提交对象上。图中HEAD指针指向master分支某次提交上。

  • master指针:永远指向主分支。

  • devGroup1指针:通过git branch devGroup1命令创建,该命令只能在当前提交对象上新建分支指针。如果需要切换提交对象,通过git reset命令切换提交对象(注意当前所在分支)。

Git操作命令和管理_java_04

图2.6.1-3 分支创建示意图2

如上图所示,devGroup1指针永远指向devGroup1分支,HEAD指针指向devGroup1分支某次提交上,也就是说我们当前的工作区正处在devGroup1分支上的某次提交上。

理解了上述的三个指针的含义我们就来使用Git分支管理的命令了。

图2.6.1-4 分支创建切换图解

创建分支

$ git branch devGroup1
# 创建分支的开销就是创建一个指向当前提交对象的指针而已,相对其他大多数版本控制器而言,非常廉价。
# 如图2.6.1-4所示

切换分支

# 创建分支并不等于切换了分支,需要执行如下命令才能切换分支。如图2.6.1-4所示
$ git checkout devGroup1

图2.6.1-5 master和devGroup1分支相互切换提交图解

在devGroup1分支上提交一次

$ git commit -m "devGroup1"
# 如图2.6.1-5中第一步所示

切换到master分支

$ git checkout master
# 如图2.6.1-5中第二步所示

在master分支上提交一次

$ git commit -m "master"
# 如图2.6.1-5中第三步所示
2.6.2 创建并切换至分支
# 创建并切换至fz1 分支,-b 参数:创建分支,checkout切换分支。
$ git checkout -b fz1
# 上述命令相当于下面两个命令
$ git branch fz1
$ git checkout fz1
2.6.3 合并分支
# 将devGroup1分支合并至master
$ git checkout master
$ git merge devGroup1

# 注意:合并分支有可能产生冲突,但不要紧张,发现冲突点,修复冲突即可提交。

Tips: 在合并分支之前,需要明确将哪个分支合并至哪个分支。如果将A分支合并至B分支,需要先切换至B分支,然后再执行git merge命令。

Fast forward 合并

Git操作命令和管理_java_05

图2.6.3-1 将devGroup1分支合并至master分支

由于当前 master 分支所在的提交对象是要并入的 devGroup1 分支的直接上游,Git 只需把 master分支指针直接右移。这种合并方式称为Fast Forward 合并,只需要移动master指针,开销非常小。

常规Git合并

Git操作命令和管理_java_06

图2.6.3-2 将devGroup1分支合并至master分支2

不同于Fast Forward 合并的是,当前master指针指向的并不是devGroup1分支的直接祖先,Git 不得不进行一些额外处理。如上图2.6.3-2所示,Git会使用当前master指针指向的提交对象C2,以及当前devGroup1指针指向的提交对象C3,以及C2和C3的共同祖先C1进行一次简单的三方合并计算(见图中三个红色的圆),并自动创建一个指向当前HEAD所在分支的提交对象C4。此时C4的父指针就有两个,分别指向C2和C3。值得一提的是 Git 可以自己裁决哪个共同祖先才是最佳合并基础。

2.6.4 Git合并遇到冲突

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,Git 就无法干净地把两者合到一起。

# 当合并遇到冲突时,会提示如下信息:
$ git merge devGroup1
   CONFLICT (content): Merge conflict in xxxx

Git 作了合并,但没有提交,它会停下来等你解决冲突。要看看哪些文件在合并时发生冲突,可以用 git status 查阅。

# 运行如下命令之后,会提示未合并的路径
$ git status

查看遇到冲突的文件

$ cat test.txt
<<<<<<< HEAD
2222
=======
111111
>>>>>>> dev

在合并发生冲突的时候,Git会在冲突的地方自动添加<<<<<<<=======>>>>>>>。其中=======分隔了两个分支冲突的内容,<<<<<<<部分为当前所在分支的内容,>>>>>>>为另一个分支的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。解决冲突过程中需要手动删除Git自动添加的<<<<<<<=======>>>>>>>

在解决了所有文件里的所有冲突后,运行 git add 将把它们标记为已解决状态。再次提交即完成了此次合并操作。

如果你想用一个有图形界面的工具来解决这些问题,不妨运行 git mergetool,它会调用一个可视化的合并工具并引导你解决所有冲突。

$ git mergetool

上述命令需要你设置Git默认的合并工具merge.tool参数。

$ git config --global merge.tool vimdiff

这里我们选择的了vimdiff,Git目前支持meld、opendiff、kdiff3、 tkdiff 、xxdiff 、tortoisemerge 、gvimdiff 、diffuse 、diffmerge 、ecmerge 、p4merge 、araxis、 bc 、codecompare、 emerge 、vimdiff。读者可自行选择。

合并遇到冲突的几个场景如下:

  • 情景一:多个分支代码合并到一个分支时;

  • 情景二:多个分支向同一个远程分支push代码时;

  • 情景三:从远程分支向本地仓库pull代码时;

实际上,push操作即是将本地代码merge到远端库分支上。push和pull其实就分别是用本地分支合并到远程分支 和 将远程分支合并到本地分支,所以这两个过程中也可能存在冲突。

git的合并中产生冲突的具体情况:

  • 两个分支中修改了同一个文件(不管什么地方)

  • 两个分支中修改了同一个文件的名称

两个分支中分别修改了不同文件中的部分,不会产生冲突,可以直接将两部分合并。

需要手工方式去解决冲突。

  • 情景一:查看冲突的文件以及文件内容,在当前分支上,修改冲突代码-->add-->commit-->merge。

  • 情景二:在本地当前分支上,修改冲突代码-->add-->commit-->push。

  • 情景三:将冲突的文件备份至仓库以外的位置,执行pull操作,然后修改冲突文件执行提交操作。

2.6.5 删除分支

完成合并之后,删除分支

# 分支合并至主分支之后,分支如果完成了它的使命,我们就可以删除它了。
$ git branch -d devGroup1

未完成合并,删除分支(不建议这样做,会导致分支上的修改全部丢失)

# 未完成合并,使用上述命令是无法删除分支的
$ git branch -D devGroup1

Tips:如果当前正处于要删除的分支上,执行上述命令是无法删除分支的,要先checkout至其他分支上。

2.6.6 分支查看

我们已经学会了创建、合并、删除分支。除此之外,我们还需要学习如何查看分支,这样才能管理分支。

# 查看当前所有分支
$ git branch -a
 dev
* master

分支前的 * 字符:它表示当前所在的分支

查看各分支最后一次提交的信息

$ git branch -v

查看哪些分支已被并入当前分支,也就是说哪些分支是当前分支的直接上游

$ git branch --merge

查看未发生合并操作的分支

$ git branch --no-merged

2.7 Git常用小技巧

本小节,与到家分享Git使用的小技巧,会使得Git 的使用更加简单、方便、轻松、高效。

自动补全功能

# 如果是windows,默认使用Git Bash,输入Git命令的时候敲两次Tab键。可以看到所有可用命令。
$ git pu<tab><tab>
   pull push
$ git log --s<tab>

# 如果是Linux或者Mac,使用Bash shell,可以试试tab键,如果无法使用。
# 下载 Git 的源代码,进入 contrib/completion 目录,会看到一个 git-completion.bash 文件。
# 将此文件复制到你自己的用户主目录中
$ cp git-completion.bash ~/.git-completion.bash
# 并把下面一行内容添加到你的 .bashrc 文件中
$ source ~/.git-completion.bash
# Mac 上将此脚本复制到 /opt/local/etc/bash_completion.d 目录中
# Linux 上则复制到 /etc/bash_completion.d/ 目录中

Git命令别名

# 如果想偷懒,少敲几个命令的字符,可以用 git config 为命令设置别名
# 下面的命令中,将checkout 别名为 co
$ git config --global alias.co checkout
# 以后当使用到checkout相关的命令时,可以按照如下方式执行:
# 相当于 git checkout test.go
$ git co test.go

Git命令别名最佳实践

# 设置 git status 为 git st
$ git config --global alias.st status
# 设置 git checkout 为 git co
$ git config --global alias.co checkout
# 设置 git commit 为 git ci
$ git config --global alias.ci commit
# 设置 git branch 为 git br
$ git config --global alias.br branch
# 设置 git reset HEAD -- test.go 为 git unstage test.go
$ git config --global alias.unstage 'reset HEAD --'
# 设置 git log -1 HEAD 为 git last
$ git config --global alias.last 'log -1 HEAD'
# 设置 git log 为 git lg
$ git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

# 有时候我们希望运行某个外部命令,而非 Git 的子命令,这个好办,只需要在命令前加上 ! 就行
# 如果你自己写了些处理 Git 仓库信息的脚本的话,就可以用这种技术包装起来.
# 我们可以设置用 git visual 启动 gitk。
$ git config --global alias.visual '!gitk'