写在前面
对于一门技术而言,20% 的知识可以解决你工作中遇到的 80% 的问题,而剩下的 80% 属于冷门知识,你可能很少会用到它们。对于 Git 而言,亦是如此。
因此,我不可能将全部 Git 相关的知识点尽数列出,这也是不切实际的,因为技术总是在进步,在更新,在迭代。我能做的唯有将这门技术的精华与原理尽可能的说与你听,当然这其中必然掺杂着描述上的错误抑或本人理解上的不正确。所以我希望你可以在阅读之余,参考其他博主的文章来提高自己,亦纠正我的错误。
本文演示使用的 Git 版本为 1.8,操作系统为 CentOS 7。话不多说,下面就开始 Git 之旅吧!
一次完整的提交
创建 mygit 目录,并将 mygit 目录初始化为 Git 仓库:
git init
该指令执行完成后,可以发现 mygit 目录下出现隐藏目录 .git :
在 mygit 目录下创建文件 test.txt,查看 Git 工作目录下的文件状态:
git status
此时文件 test.txt 处于未跟踪(untracked)状态:
跟踪 test.txt 文件,并将该文件从工作区提交到暂存区:
git add test.txt
再次执行 git status 指令查看 Git 工作目录下的文件状态,可以看到 test.txt 文件的状态从未跟踪变成了等待提交(已暂存):
将暂存区的文件提交至版本库:
git commit
由于是首次提交,Git 会提示需要先配置用户名和邮箱:
按照提示信息,配置用户名和邮箱:
git config --global user.name 'zhangsan'
git config --global user.email 'zhangsan@test.com'
配置完成后,再次执行 git commit 指令。此时会打开操作系统默认的文件编辑器,提示输入提交说明:
提交说明填写完成之后,即可将暂存区的文件提交到版本库。
查看 Git 仓库的提交日志:
git log
提交日志会显示每条提交记录的作者、提交时间和提交说明:
至此,一次完整的提交结束。
工作区、暂存区和版本库
Git 仓库可以分为三个区域:
- 工作区:查看、编辑、保存文件的地方,也是用户能直接操作到的地方。
- 暂存区:英文为 stage。对应 .git/index 文件,因此有时也将暂存区叫作索引(index)。实际上就是一个包含文件索引的目录树,在这个目录树中,记录了文件名、文件的状态信息(时间戳、文件长度等),文件的内容并不存储其中,而是保存在 Git 对象库(.git/objects)中,文件索引建立了文件和对象库中对象实体之间的对应。
- 版本库:即 Git 仓库下隐藏目录 .git 。
注:上一节中我用了 “Git 工作目录” 这个词,可以理解为工作区和暂存区的统称。例如,执行 git status 指令会列出工作区和暂存区的文件状态变化,而我在上一节中描述为:“查看 Git 工作目录(即 mygit 目录)下的文件状态”。
status
Git 工作目录下的每一个文件不外乎两种状态:未跟踪(untrackded)或已跟踪(tracked)。
Git 官网对二者的定义如下:已跟踪的文件指纳入版本库的文件,在工作一段时间后,它们的状态可能处于未修改、已修改或已暂存。工作目录中除已跟踪文件之外的所有其他文件都属于未跟踪文件。
上面定义中的“已跟踪”,我更愿意称之为“广义上的已跟踪”。在一次完整的提交小节中,对未跟踪文件 test.txt 执行 git add <file>指令后,该文件的状态就已变成已跟踪状态(已暂存)了,而此时 tes.txt 文件并未被纳入版本库。但是这种已跟踪状态是暂时的,如果 test.txt 文件在被提交到版本库之前你就从将它从暂存区移除,那么该文件的状态会重置为未跟踪。
因此,广义上的已跟踪指的是文件被纳入版本库,狭义上的已跟踪指的是文件存在于暂存区。这个概念你只要了解就行了,后文提到的已跟踪文件指的采用狭义上的已跟踪。
简而言之,Git 管理的文件分为两类:
- 未跟踪
- 已跟踪:又可细分为未修改、已修改、已暂存
使用 Git 管理的文件生命周期如下(Ps:理想状态下):
用下面的指令就可以查看 Git 工作目录下的文件状态:
git status
使用该指令的输出信息十分详细,同时也过于繁琐。对于很熟悉文件状态的人,可以使用下面的指令得到更为简洁的输出:
git status --short // 等价于
git status -s
输出结果示例:
新添加的未跟踪文件前面用 ?? 标记,新添加到暂存区的文件前面用 A 标记,修改过的文件前面有 M 标记。你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件(已纳入版本库)被修改了并放入了暂存区。至于 test2.txt 文件,它在工作区被修改并提交到暂存区后又在工作区中被修改了,所以在暂存区和工作区都有该文件被修改的记录。
config
前文提到使用 git config 指令可以配置用户名和邮箱:
git config --global user.name 'zhangsan'
git config --global user.email 'hangsan@test.com'
该指令中使用的是 --global 参数,除此之外还有 --system 和 --local 两个参数,它们的含义如下:
- --system:包含系统上每一个用户及他们仓库的通用配置,配置文件为 ./etc/gitconfig(执行该命令之后才会创建该文件)
- --global:只针对当前用户,配置文件为 ~/.gitconfig(执行该命令之后才会创建该文件)
- --local:针对该仓库,配置文件为 .git/config(使用 git init 命令就会创建该文件)
如果省略级别参数,则默认使用的 --local 参数。若三种级别的用户名和邮箱都做了配置,则低级别会覆盖高级别的配置,所以 .git/config 中的配置变量会覆盖 /etc/gitconfig 中的配置变量。
顺带一提,在 Windows 系统中三个配置文件分别位于:
- system:C:\Program Files\Git\mingw64\etc\gitconfig(需要使用管理员权限配置)
- global:C:\Users\username\.gitconfig
- local:Git 工作目录下 .git/config
虽然不使用 git config 指令,也可以进入对应配置文件进行 Git 相关信息的配置,因为执行 git config 指令的本质就是就是操作配置文件(强烈不建议这么做)。例如执行上面的两条指令,其本质就是在 ~/.gitconfig 文件中写入如下信息:
即使你误将 user.email 写成了 user.mail,配置文件中还是会写入对应的信息:
如果要删除写错的信息,执行下面的指令即可:
git config --global --unset user.mail
如果你想查看配置的所有用户名和邮箱,可以使用下面的指令列出所有 Git 能找到的配置信息(包括系统、全局和本地配置):
git config --list
add
在一次完整的提交小节中,执行 git add <file> 指令做了两件事情:
- 跟踪文件
- 将文件从工作区提交到暂存区
这是因为该指令操作的对象是未跟踪的新文件。如果对纳入版本库的文件执行该指令,那么 git add <file> 所表达的含义就只有将文件从工作区提交到暂存区。
若需要提交的文件数量较多,该指令便略显鸡肋,此时可以使用下面的指令:
git add .
该指令将工作区修改(不包括删除)和新增的文件添加到暂存区。
从上图可以看到,git add 指令默认使用 --ignore-removal 参数(忽略删除)。若想把删除的文件也提交到暂存区,需要使用 --all 或 -A 参数:
git add --all // 等价于
git add -A
该指令表示将工作区所有的新增和修改(包括删除)的文件添加到暂存区。
若只想将已跟踪的文件添加到暂存区,可以使用 -u (update)参数(包括删除的文件,但不添加未追踪的文件):
git add -u
如果想要强制添加某个文件,不管 .gitignore(见后文详述)是否包含这个文件,可以使用 -f 参数:
git add -f <file>
commit
git commit 指令用于将暂存区的文件纳入版本库,直接输入该指令会启动系统默认的文本编辑器以输入提交的说明(一般是 vi,我在Windows 的 cmd 中尝试之后,发现竟然也打开了 vi)。当然你也可以使用下面的指令设定你喜欢的文本编辑器:
git config --global core.editor <editor>
每次都弹出文件编辑器未免繁琐,如果只需要提交简短的说明,可以使用 -m 参数指定提交说明,这样就不会进入文件编辑模式:
git commit -m <message>
甚至我们也可以跳过 git add 步骤,直接提交工作区的文件。这里需要使用 -a 参数:
git commit -a
需要注意,这种方式只能提交已跟踪的文件。
如果提交之后才发现漏掉了几个文件没有添加,或者提交信息写错了,可以使用 --amend 参数重新提交:
git commit --amend
注意,执行该指令会生成新的提交记录,覆盖上一次的提交记录。
log
使用下面的命令可以查看所有的提交日志:
git log
不使用任何参数,该指令会按提交时间降序列出 Git 版本库中所有的提交记录,最近的提交记录排在最上面。 这个命令会列出每次提交的 SHA-1 值(commit)、作者的名字和电子邮件地址(Author)、提交时间(Date)以及提交说明。
一般情况下,该指令会搭配各种参数使用。下面是几个常用的参数:
- --graph:图形化显示
- --stat:文件变化信息
- --pretty=oneline:每个提交放在一行显示
- --abbrev-commit:仅显示 SHA-1 的前几个字符
除了提交日志以外,还可以查看操作日志:
git reflog
rm 与 git rm
有时候我们需要将 Git 工作目录下的某个文件删除,有两种方式:
rm <file>:系统自带的文件删除功能,直接将文件删除
git rm <file>:删除文件并将此次操作提交到暂存区(未跟踪的文件不要使用该指令,如果使用 git rm 会报错)
对于被纳入版本库的文件(执行过 git commit 的文件),如果出现误删,这两种删除都是可以还原的:
1、使用 rm 指令删除文件,由于文件只在工作区被删除,执行下面的指令直接将文件从暂存区恢复到工作区,一般用于丢弃工作区对该文件的修改:
git checkout -- <file>
2、使用 git rm 指令删除文件,由于该文件在工作区和暂存区均已删除,需要先将版本库中最近一次的提交恢复到暂存区:
git reset HEAD
执行上面的指令可以将当前所在分支(见后文详述)的暂存区重置为最新一次提交的样子。接着就和上面恢复删除文件的做法一样,将文件从暂存区恢复到工作区即可:
git checkout -- <file>
两种删除方式总结:
- rm <file>:只删除工作区的文件,直接执行 git checkout -- <file> 就可以还原(未纳入版本库的文件无法还原)
- git rm <file>:同时将该文件在工作区和暂存区删除,需要从版本库恢复被删除的文件,直接执行 git checkout -- <file> 是无法还原的。
当然 git rm 也可以只删除暂存区的文件,只需要加上 --cached 参数即可:
git rm --cached <file>
该指令只会删除暂存区的文件,工作区的文件仍然会保留。
如果是没有纳入 Git 版本库的已跟踪文件(执行过 git add 但是没有执行 git commit),使用 git rm 删除该文件,也会出现警告信息。
这是因为删除此文件只存在于工作区和暂存区,版本库中并没有该文件,所以无法从版本库中复原该文件。想要删除此类文件需要使用 -f 参数:
git rm -f <file>
clean
借助于 git rm 指令,可以删除已跟踪的文件。那么未跟踪的文件(既没有 git add 也没有 git commit)就只能通过 rm 指令删除了吗?自然 Git 不会允许这种情况出现,可以使用下面的指令删除当前所在目录下的未跟踪文件:
git clean
如果直接执行该指令你应该可以看到报错信息,这是因为 Git 配置中有一个参数 clean.requireForce,该参数默认值为 true,表示执行 git clean 需要确认。你可以使用下面的指令修改该参数的默认值(但是不建议这么做,因为有可能之后你不小心执行该指令就直接将文件删除了):
git config --local clean.requireForce false
推荐的做法是在执行该指令的时候加上 -f 参数,表示强制删除文件(不会删除 .gitignore 文件中指定的文件, 不管这些文件是否被跟踪):
git clean -f
如果想删除 .gitignore 文件中的指定的文件,则需要使用 -x 参数:
git clean -xf
使用该指令会删除当前所在目录下所有未跟踪的文件,不管是否是 .gitignore 文件中是否指定该文件。
如果要连带删除文件夹,可以使用 -d 参数:
git clean -df
执行该指令会删除当前目录下的未跟踪的文件和文件夹。
因为在执行删除之前,最好先执行下面的指令:
git clean -n
执行该指令会提示哪些文件会被删除,并不会真正的删除文件:
.gitignore
初始化 Git 仓库并不提供该文件,.gitignore 文件需要手动创建,配置在 Git 仓库根目录下即可。执行某些指令时,Git 会自动忽略在该文件中指定的文件夹或文件。例如上一节提到的 git clean -f。
常用忽略提交规则如下(**表示任意个文件夹):
# :表示此为注释,将被Git忽略
*.a :表示忽略所有 .a 结尾的文件
!lib.a :表示但lib.a除外
/TODO:
表示仅仅忽略项目根目录下的 TODO 文件,不包括 subdir
/TODO
fd1/* :表示忽略目录 fd1 下的全部内容;注意,不管是根目录下的 /fd1/
目录,还是某个子目录 /subdir/fd1/
目录,都会被忽略
/fd1/
* :表示忽略根目录下的 /fd1/
目录下的的全部内容
**
/foo
: 表示忽略
/foo
,a
/foo
,a
/b/foo
等
a/**
/b
: 表示忽略a
/b
, a
/x/b
,a
/x/y/b
等
debug/*.obj: 表示忽略debug
/io
.obj,不忽略 debug
/common/io
.obj和tools
/debug/io
.obj
branch
广义上的分支
分支是 Git 最重要的特性,在讲解分支操作之前,有必要说明一下分支是什么。
在工作区、暂存区和版本库小节,提到暂存区管理文件的索引,而文件的内容则保存在 Git 对象库中。那么对象库中的对象指的是什么呢?对象指的就是提交对象(commit object),在进行提交操作时,Git 会在对象库中创建一个提交对象。
提交对象包含作者的名字和邮箱、提交时间和说明以及本次提交的 SHA-1 值,也就是执行 git log 时看到的信息。其实除了这些可见的信息之外,提交对象中还包含两条不可见的信息:上一次和下一次提交记录的 SHA-1 值(即父提交和子提交)。
借助于可视化工具 gitk(下一篇文章中介绍),我们来看一下这两条信息:
通过提交对象的父提交和子提交属性,所有的提交记录就形成了一条提交链。广义上,我们将提交链称之为分支。
分支操作
初始化 Git 仓库,只有一个分支 —— master 分支。使用下面的指令可以查看所有分支以及当前所在分支:
git branch
创建分支(该分支默认指向所在分支最新一次提交):
git branch <branch>
创建完成之后,执行下面的指令就可以切换到新创建的分支:
git checkout <branch>
先创建分支再切换到新分支,两条指令可以直接用下面的指令代替:
git checkout -b <branch>
需要注意的是,分支切换时如果原分支上有未提交的文件修改,且该文件修改和要切换后分支上的文件没有冲突(冲突见后文详述),那么该文件修改会带到切换后分支的工作区和暂存区。如果你在切换后分支上进行提交操作,那么原分支上的文件修改会被记录在切换后分支上。
但是如果原分支上未提交的文件修改和切换后分支上的文件存在冲突,那么此时 Git 会提示你分支切换后你在原分支所做的修改会丢失,建议你在原分支提交修改或者将文件修改储藏(见后文详述)。如下图所示:
不过一般来说,进行分支切换时我们都会选择将原分支的工作内容储藏。
各个分支都会有不同提交记录,使用 -v 参数可以查看各分支最新的提交记录:
git branch -v
上图中,创建 newb 分支时 master 分支的最新一次提交的提交说明为 first commit,所以 newb 分支默认指向此次提交。
如果某个分支不再需要,可以使用 -d 参数删除:
git branch -d <branch>
注意删除分支,不可以删除所在的分支。如果要删除所在分支,需要先切换到其他分支再删除分支:
如果要删除的分支存在没有合并(见后文详述)的提交记录,则需要使用 -D 参数强制删除分支:
git branch -D <branch>
使用 -m 参数可以修改分支名:
git branch -m <branch> <newbranch>
狭义上的分支
进入 .git/refs/heads 目录,查看该目录下的文件:
可以看到该目录下存放着所有的分支信息,查看 master 分支信息:
对比 master 分支的历史提交记录,不难发现 master 分支记录的是最新一次提交记录的 SHA-1 值。换而言之,所谓的分支其实只是一个指针,指向最新一次的提交对象。
所以在解释 git branch <branch> 指令时,我使用了“指向”这个词。因为狭义上,分支是一个指针。
checkout
在 rm 与 git rm 小节 和 branch 小节中都提到了 git checkout 指令,两次出现该指令的作用分别是:
git checkout -- <file> :将文件从暂存区恢复到工作区
git checkout <branch> :切换分支
除了切换分支,通过该指令还可以切换到历史快照(即历史提交记录):
git checkout <SHA-1>
SHA-1 值长达40个字符,不需要全部输入,只需输入前7、8个字符即可。当切换到历史快照时,你将处于一种游离状态:
在游离状态下,你当前所在的分支就像下面这样:
而一旦你进行分支切换操作,这个“游离分支”将会消失:
因此,如果你在游离状态下提交记录,那么最好在该提交记录上创建分支,否则你很难找回该提交记录。
方式一:在提交记录上直接执行 git checkout -b <branch>。
方式二:如果你忘记创建分支就切换到了别的分支,那么可以通过 git reflog 找到那次提交记录的 SHA-1 值,然后执行 git branch <branch> <SHA-1> 在指定提交记录上创建分支。
checkout 原理
当执行 git branch 指令时,你可以看到当前所在的分支。那么必然存在着一个数据记录我们当前所在的分支,这个数据就是 HEAD 文件,在 rm 与 git rm 小节中我们就已经见过它了(不过 reset 相关知识会在下一小节详解,这里不过多描述)。
查看 HEAD 文件中的内容:
HEAD 文件中存放的就是当前所在分支的引用,而通过上一节我们知道,分支本质上是指针,所以 HEAD 的本质也是一个指针,是一个指向指针的指针,最终指向一个提交对象。
切换到 newb 分支,再次查看 HEAD 文件的内容:
那么当我们切换到历史快照时,HEAD 文件中存放的又是什么呢?
当我们处于游离状态时,HEAD 就充当了分支的作用,存放历史提交记录的 SHA-1 值,指向一个具体的提交对象。
reset
在 rm 与 git rm 小节中提到,git reset HEAD 指令可以将版本库中最新一次的提交恢复到暂存区。更确切的说,应该是将所在分支回退到上一次提交时的状态:
git reset HEAD
执行上面的指令回退版本会修改暂存区,但是不改变工作区。这就是为什么版本回退之后,还需要执行 git checkout -- <file> 指令的原因。
当然你也可以在回退版本时,搭配参数使用。使用 --soft 参数,可以在版本回退时不改变暂存区和工作区:
git reset --soft HEAD
而 --hard 参数则会在版本回退的同时,改变暂存区和工作区:
git reset --hard HEAD
如果只想将某个文件版本回退,可以在执行 git reset HEAD 指令时加上文件名(使用此指令时,不可搭配 soft 或 hard 参数):
git reset HEAD <file>
有时候我们需要回退多个版本,应该怎么做呢?此时可以在 git reset HEAD 的后面加上 ^,有几个 ^ 就表示回退多少个版本,例如回退到倒数第二个版本:
git reset HEAD^
如果需要回退多个版本时,岂不是需要使用很多的 ^?自然不是这样,此时就可以使用 ~n 表示回退多少个版本:
git reset HEAD~n
如果需要回退到很久远的版本,就可以使用 git log --abbrev-commit 查看版本的 SHA-1 值(只需要前7、8个字符就可以),然后使用下面的指令回退到指定版本:
git reset <SHA-1>
那么问题来了,怎么样才能从历史版本回到最新版本呢?这里就需要使用之前提到过的一个指令:git reflog。借由该指令可以查看每一次操作的记录,找到需要回到那一次操作的 SHA-1 值,再使用上面的指令就可以回到最新的版本。
reset 原理
通过前文的学习,我们知道执行 git checkout <SHA-1> 可以修改 HEAD 文件,那么 git reset HEAD 是否也修改了 HEAD 文件呢?从字面意思上看该指令是修改了 HEAD 文件,其实该指令修改的是 HEAD 记录的分支的指向。
当我们在 master 分支上执行 git reset HEAD^ 指令时,Git 会根据 HEAD 文件中记录的分支引用,找到该分支指向的提交对象,然后根据该提交对象的 parent 属性找到父提交对象,最后将该分支指向提交对象替换为父提交对象。
通过对 branch、checkout 和 reset 的讲解,我们可以总结出提交对象、分支、HEAD 的关系,如下如所示:
merge
branch 小节中提到:删除分支时,如果该分支存在没有合并的提交记录会提示警告信息。那么如何进行分支合并呢?执行下面的指令,就可以将指定分支合并的当前所在的分支:
git merge <branch>
合并分支需要填写提交说明,默认的说明格式为“Merge branch <branch1> into <branch2>”:
上图演示的是没有冲突情况下的合并,如果要合并的分支和所在分支存在冲突,那么就需要你手动解决冲突。
冲突
什么是冲突呢?其实冲突很简单,就是两个分支对同一个文件的同一个位置都做了修改。如果出现冲突,Git 在进行合并操作时,就不知道以哪个分支为准,所以需要你手动解决冲突——也就是选用哪个分支的修改。
出现冲突时,执行 git status 指令就能看到是哪个文件发生了冲突:
查看发生冲突的文件:
如上图所示,发生冲突的文件中冲突部分会用不同符号标记出来:
<<<<<<< HEAD:表示所在分支文件冲突内容
=======:表示分割线
>>>>>>> m:表示合并分支文件冲突内容
解决冲突后,使用 git add <file> 指令标记冲突解决,然后提交修改即可。
在 add 小节中总结了 git add 指令有两种用法:跟踪文件、将文件从工作区提交到暂存区,在这里出现的是该指令的第三种也是最后一种用法:标记冲突解决。
fast-forward
除了冲突以外,在合并时我们还会遇到另一种情况—— fastforward(快进)。若你当前所在分支是要合并分支的 root commit,那么执行上面的指令时就会触发 fast-forward 。
例如,在 master 分支上创建并切换到 bugfix 分支后,一直在 bugfix 分支上提交记录,而 master 分支上没有新的提交记录,那么 master 分支就是 bugfix 分支的 root commit 。如下所示:
此时我所在分支为 master,需要将 bugfix 分支合并过来。那么执行合并操作时就会触发 fast-forward,输出信息会提示本次合并使用的是 fast-forward 方式:
可能你注意到了:使用 ff 合并不需要书写提交记录,这意味着这种方式的合并直接修改了当前分支的指向。因此使用 ff 合并,原有分支的特性会被覆盖,不利于以后查看记录。
所以使用 ff 方式合并后,就应该将合并的分支删除(Git 官方推荐)。或者合并分支的时候,不使用 ff 方式(顺带一提,我在GitBash 上执行这条指令时, GitBash直接就卡死了):
git merge --no-ff <branch>
使用该指令进行合并,合并之后的效果就变成了这样:
从上图可以看到,不使用 ff 方式合并,bugfix 分支的特性依然存在。
stash
实际开发中,时常会遇到这样的状况:所在分支的工作没有完成,但是需要切换到其他的分支工作,而所在分支的工作内容不足以到达提交的要求。此时就可以使用下面的指令将工作目录下已跟踪的文件储藏:
git stash
从提示信息可以看到,工作区和暂存区的文件都会被储藏。执行该指令后,文件会暂时从工作区和暂存区移除,回到上次提交时的状态。此时我们查看工作目录下的文件状态,就没有文件修改信息了。
通过下面的指令可以查看储藏的工作内容:
git stash list
WIP 即 Work In Process,正在进行中的工作。
等到在别的分支的工作结束后,我们就可以切换回原分支恢复之前的工作内容。执行下面的指令,可以将最新一次储藏的工作内容恢复到工作区和暂存区:
git stash pop
执行上面的指令,恢复工作内容的同时也会删除储藏记录:
如果想恢复工作内容的同时保留储藏记录可以使用下面的指令:
git stash apply
需要注意的是,多个分支共用一个储藏室:
所以在恢复工作内容时一定要先查看储藏记录,而不要盲目的使用 git stash pop 指令恢复最新的一条储藏记录。如上图所示,如果我们想恢复第三条储藏记录,就需要使用下面的指令:
git stash apply stash@{2}
前文我说过 git apply 指令不会删除储藏记录,恢复工作内容后可以使用下面的指令删除指定的储藏记录:
git stash drop stash@{1}
如果想要清空所有储藏记录,可以使用下面的指令:
git stash clear
默认情况下,储藏记录格式如下:
stash@{num}: WIP on <branch>: <SHA-1> <commit message>
如果全部使用默认的储藏记录格式,一旦储藏记录多起来就很难区别每条储藏记录的内容,因此在储藏工作内容时最好加上备注:
git stash save <message>
使用备注后,储藏记录格式如下:
前文提到 git stash 指令会储藏已跟踪的文件。如果想要储藏 文件,则需要使用 --include-untracked 参数:
git stash --include-untracked
此外还有一个参数:--keept-index,使用该参数表示不要储藏暂存区的文件:
git stash --keep-index
blame 与 diff
如果一个文件被多个人修改,想知道某一行是谁修改的,此时就可以使用下面的指令:
git blame <file>
执行上面的指令会显示文件每一行具体的来自于哪次提交:
除 blame 之外,Git 还提供 diff 操作,可以比较文件在工作区、暂存区和版本库的差异:
git diff
默认情况下 diff 操作比较比较暂存区和工作区的文件差异,--- 代表源文件,+++ 代表目标文件,通过合并的方式描述源文件如何变成目标文件:
上图中,a/test.txt 表示暂存区的 test.txt 文件,b/test.txt 表示工作区的 test.txt 文件,暂存区的 test.txt 一共一行,增加一行即可变成工作区的 test.txt 文件。
如果要比较版本库和工作区的文件,可以在指令后面加上 HEAD,比较最新一次提交和工作区的文件差异:
git diff HEAD
通过 reset 小节的我们可以知道,HEAD 本质上也是一个指针,指向一个提交对象。所以我们完全可以用 SHA-1 值代替 HEAD:
git diff <SHA-1>
上面指令的含义为,比较某个历史版本和工作区的文件差异。
当然,我们也可以将目标文件修改为暂存区的文件,只需要加上 --cached 参数即可。执行下面的指令就可以比较版本库和暂存区之间的文件差异:
git diff --cached
默认情况下,diff 操作会比较两个区域的所有文件,你也可以只比较单个文件差异:
git diff -- <file>
除了比较所在分支文件之间的差异,也可以比较其他分支和所在分支之间工作区的文件差异:
git diff <branch>
当然上面的两条指令都可以和 --cached 、HEAD 参数配合使用。例如:
既然你可以比较其他分支和本分支的文件差异,那么你也可以指定两个分支进行比较:
git diff <branch1>..<branch2>
tag
当项目开发到一定程度,一般就会发布一个新的版本,此时就可以给这一次具有特殊意义提交打上标签。
标签分为两种:轻量标签和附注标签。创建一个轻量标签:
git tag <tag>
通过下面的指令就可以查看标签信息:
git show <tag>
创建一个附注标签:
git tag -a <tag>
加上 -m 参数还可以为辅助标签添加备注信息:
git tag -a <tag> -m <message>
对比查看轻量标签和附注标签的输出信息可知,除了打标签时那次提交记录的详细信息,附注标签会比轻量标签多出标签名、作者、邮箱、日期和备注信息。
通过下面的指令可以查看所有的标签:
git tag
如果错过了打标签的时机也没有关系,你可以为通过下面的指令为历史快照创建标签:
git tag -a <tag> <SHA-1>
一旦项目大了起来,标签的数量也会随之水涨船高,此时就可以使用 -l 参数查找标签:
git tag -l 'v1.8.*'
和分支一样,使用 -d 参数就可以删除标签:
git tag -d <tag>
tag 原理
对于一个轻量标签,当我们执行 git show <tag> 指令时, 就会输出打标签时那次提交记录的详细信息。那么标签到底是什么呢?标签文件位于 .git/refs/tags 目录中,查看标签 v1.0:
和分支一样,轻量标签文件中存放的也是打标签时那次提交记录的 SHA-1 值。这样一来我们就知道,轻量标签本质上也是一个指针。这也就是为什么标签文件夹和分支文件夹同在 refs 目录下,因为它们本质上是一类东西。
那么附注标签呢?它也是一个指针么?确实,附注标签也是一个指针,只不过它指向的不是提交对象,而是标签对象。标签对象中包含标签名、作者、邮箱、日期、备注信息以及提交记录的 SHA-1 值,所以附注标签更多的是类似 HEAD,指向指针的指针。
从上图可以看到,附注标签文件中存放的 SHA-1 值和任何一次提交记录的 SHA-1 值都不匹配。
顺带一提
1、Git 指令的参数提示符有时为 --,有时又使用 - 。什么时候用哪种参数提示符呢?一般来说使用 -- 时后面跟一个单词,而使用 - 的时候后面跟一个单词的首字母。
当然也存在特殊情况,例如仅靠一个单词无法描述该参数的具体含义,就需要使用两个单词。第一个单词和第二个单词之间使用 - 隔开:--abbrev-commit。
2、使用 Git 管理项目时,一般会用到下面四类分支:
- master:版本发布分支
- develop:开发分支(频繁迭代)
- test:测试分支
- bugfix:紧急 bug 修复分支
下一篇:Git详解(二) 远程操作
参考: