Git的基本用法和扩展

Git 简史

Git的简单介绍和相关原理

Linux内核是一个超大规模的开源软件项目,早期对Linux的维护采用的是通过传递补丁和归档文件的方式来实现的。直到2002年,Linux内核项目开始采用一个叫BitKeeper的专有分布式版本控制系统。

2005年,Linux内核开发者社区与BitKeeper的研发公司关系破裂,该公司收回了软件的免费使用权,促使Linux开发社区(尤其是Linux之父林纳斯·托瓦兹)在吸取BitKeeper的使用经验上,开发出了自己的版本控制系统Git。

新的版本控制系统具有如下特点:

  1. 速度快
  2. 设计简洁
  3. 对非线性开发强有力的支持(即git的分支管理功能)。
  4. 完全分布式设计。
  5. 能够有效的处理像Linux内核这种大型项目。

Git与其他版本控制系统最大的不同在于其对待数据的方式,其他大多数版本控制系统(SVN、CVS等)以文件变化列表的方式存储信息。这类系统将其存储的信息视为一组文件以及对这些文件随时间所做的变更。

而Git并没有采取这种方式对待和存储数据。它更像是将数据视为一个微型文件系统的一组快照。每次提交或在Git中保存项目的状态时,Git基本上会抓取一张所有文件当前状态的快照,然后存储一个之昂想该快照的引用。处于效率考虑,如果文件并没有发生变动,Git则不会再重新保存文件,而是留下一个指向先前已保存过的相同的文件的链接。

还有一点是Git中的大部分操作只需要用到本地文件和资源。一般无需从网络上获取信息。项目完整的历史记录都存在本地磁盘上,所以绝大多数的操作都能瞬间完成。

Git使用的核心内容

在Git中,文件可以处于以下三种状态之一:已提交、已修改、已暂存。

  1. 已提交:表示数据已经被安全存入本地数据库中。
  2. 已修改:表示已经改动了文件,但尚未提交到数据库。
  3. 已暂存:表示对已修改的文件的当前版本做出了标识并将其加入下一次要提交的快照中。

文件状态变更的流程如下所示:

Git基础

获取Git仓库

建立Git项目的方式有两种

第一种是在现有项目的目录中初始化Git仓库,操作步骤如下:

进入项目目录并输入 git init,这会创建一个.git的隐藏文件夹(快捷键⌘ + ⇧ + . 可显示隐藏文件),这个文件夹包含了构成Git仓库骨架的所有必须的文件。但此时Git尚未跟踪项目中的任何文件。可以执行命令git add [filename]使Git跟踪指定的文件,也可以输入git add .跟踪当前目录下的所有文件,然后再输入git commit命令即可:

1
2
3
$ git add *.c
$ git add LICENSE
$ git commit -m 'init version'

第二种是克隆现有仓库

如果需要获取一份现有仓库的副本,可以使用git clone命令,该命令默认会从服务器上把整个项目历史中每个文件的所有历史版本都拉取下来,克隆仓库需要使用git clone [url]命令,例如:

$ git clone https://github.com/libgit2/libgit2

执行此命令后会在终端当前的目录创建一个名为libgit2的新目录,并在其中初始化.git目录,然后将远程仓库中所有数据拉取到本地并检出最新版本的可用副本。如果想将项目克隆到其他名字的目录中,可以把目录名作为命令行选项传入:

$ git clone https://github.com/libgit2/libgit2 mylibgit

在Git仓库中记录变更

请记住,工作目录下的每个文件都处于两种状态之一:已跟踪(tracked)或未跟踪(untracked)。已跟踪的文件又可分为未修改、已修改和已暂存三种状态。如果修改了文件,他们在Git中的状态会变成已修改,这意味着自上次提交以来文件已经发生了变化。接下来要把这些已修改的文件添加到暂存区,提交所有已暂存的变更,随后重复这个过程。

检测文件所处状态主要命令是git status,虽然git status命令的输出信息很全面,但也着实冗长。对此,Git提供了一个现实简短状态的命令行选项git status -s 例如:

1
2
3
4
5
6
$ git status -s
M README
MM Rakefile
A lib/git.rb
M lib/simlegit.rb
?? LICENSE.txt

未被跟踪的文件旁边会有一个??标记,已暂存的新文件会有A标记,而已修改的文件会有M标记,已修改并被添加到暂存区,之后又被修改过会有MM标记。

有时候你并不希望某一类文件被Git自动添加,甚至不希望这些文件被显示在未跟踪的列表中。这种情况下,可以创建名为.gitignore的文件,在其中列出待匹配文件的模式,下面是一个.gitignore文件的例子:

1
2
3
$ cat .gitignore
* .[oa]
*~

其中第一行告诉Git忽略所有以.o.a结尾的文件,第二行告诉Git忽略所有以~结尾的文件。你也可以让Git忽略log目录、tmp目录、pid目录以及自动生成的文档等。最好在开始工作前配置好.gitignore文件。Github维护了一份相当全面的.gitignore参考示例列表。如果想用它作为自己项目的参考,请访问https://github.com/github/gitignore

如果git status命令的输出信息对你来说太过泛泛,你想知道具体的内容,而不仅仅是你更改了哪些文件,这是你可以使用git diff命令,该命令会显示你具体添加和删除了哪些行。换句话说,git diff的输出是补丁。如果你想看看有哪些已暂存的内容会进入下一次提交,可以使用git diff —staged命令。

所有未暂存的变更都不会进入到提交的内容中,而所有已暂存的变更在执行git commit命令后都会进入到本地仓库。完成上述提交还有另外一种方式,那就是直接在命令行上键入提交信息。如:git commit -m 'first commit',请记住,提交时记录的是暂存区中的快照,任何未暂存的内容任然保持着已修改状态。

如果你想要跳过暂存区直接提交,Git为你提供了更快捷的途径。给git commit命令传入-a选项,就能让Git自动把已跟踪的所有文件添加到暂存区,然后再提交,这样你就不用再执行git add命令了。例如:git commit -am 'first commit'

查看提交历史

在完成了几次提交,或者克隆了一个已有提交历史的仓库后,你可能想要看看历史记录。可以使用git log命令来实现,这是最基础却又最强大的一条命令。下面是截取的MBProgressHUD的log一部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit c954ef3806f135a4d6bb4e18e9b4b64dfd52995a (HEAD -> master, origin/master, origin/HEAD)
Merge: a90765f c63cf18
Author: Matej Bukovinski <matej@bukovinski.com>
Date: Thu Nov 30 21:19:46 2017 +0100
Merge pull request #513 from niveus/Remove-NSLog
Remove NSLog

commit c63cf1840e70f2980e9fae992866945389fa07f1
Author: jason <jason.gabriele@dexcom.com>
Date: Thu Nov 30 09:52:15 2017 -0800
Remove NSLog

默认不加参数的情况下,git log会按照时间顺序列出仓库中所有提交,其中最新的提交显示在最前面。每一个提交都会列出它的SHA-1校验和、作者的姓名和邮箱、提交日期以及提交信息。

git log有很多不同的选项,可以直观的展示出所需的内容。现在我们来看一些常见的选项。-p,它会显示出每次提交所引入的差异。你还可以加上-2参数,只输出最近两次提交。--stat选项会在每个提交下面列出如下内容:改动的文件列表、共有多少文件被改动以及文件里有多少新增行和删除行。另外还会在最后输出总计信息。--pretty它可以更改日志输出的默认格式。Git有一些预置的格式供你选择。例如oneline它可以在每一行中显示一个提交。shortfullfuller格式选项会分别比默认输出减少或增加一些信息:

1
2
3
4
5
6
$ git log --pretty=oneline
c954ef3806f135a4d6bb4e18e9b4b64dfd52995a (HEAD -> master, origin/master, origin/HEAD) Merge pull request #513 from niveus/Remove-NSLog
c63cf1840e70f2980e9fae992866945389fa07f1 Remove NSLog
a90765f5f2a825507490355dc9d253e2c202c7ee Merge pull request #90 from matej/matej/hide-timer-race
549fa2854636d00225819a7b120c505b324b5dd9 Add a test case for the hide delayed race
1f1d7ce8bdb6a82d9275163323d503c221cdfe65 Use the new method signature in comments

最值得注意的选项是format,它允许你指定自己的输出格式。下面是截取的MBProgressHUD的log一部分:

1
2
3
4
5
6
7
$ git log --pretty=format:"%h - %an, %ar : %s"
c954ef3 - Matej Bukovinski, 3 months ago : Merge pull request #513 from niveus/Remove-NSLog
c63cf18 - jason, 3 months ago : Remove NSLog
a90765f - Matej Bukovinski, 3 months ago : Merge pull request #90 from matej/matej/hide-timer-race
549fa28 - Matej Bukovinski, 3 months ago : Add a test case for the hide delayed race
1f1d7ce - Matej Bukovinski, 3 months ago : Use the new method signature in comments
16e8622 - Matej Bukovinski, 3 months ago : Prevent a race when invalidating the hide
格式选项 输出的格式描述
%H 提交对象的散列值(校验和)
%h 提交对象的简短散列值
%T 树对象的散列值
%t 树对象的简短散列值
%P 父对象的散列值
%p 父对象的简短散列值
%an 作者的名字
%ae 作者的电子邮箱地址
%ad 创作日期
%ar 相对于当前日期的创作日期
%cn 提交者的名字
%ce 提交者的电子邮箱地址
%cd 提交日期
%cr 相对于当前日期的提交日期
%s 提交信息的主题

onelineformat这两个选项如果与log命令的另一个选项--graph一起使用,就能发挥更大的作用。具体来说--graph选项会用ASCII字符形式的的简单图表来显示Git分支和合并历史,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*   d55891f  Merge branch 'master' of git://github.com/jdg/MBProgressHUD
|\
| * 886d26f hudWasHidden delegate method should be required
| * 6d55851 Remove changelog.txt; update README.
| * df6df4c Merge branch 'master' of git://github.com/zenwheel/MBProgressHUD
| |\
| | * 8985308 fix for leak when changing indicator style
| | * 5ba1a24 added setNeedsDisplay to get label changes to appear
| * | bcccd47 Sleep before showing.
| * | ce752d9 Added delayed display.
| |/
| * fee25a5 Adding offsets and delays. Offsets work, but don't protect against over/underflow; delays don't work yet.
| * 26ab9ca Updating gitignore.
| * 4f5b96f Reformatting the code to look like I like it.
| * 4c9eba6 Adapting the HUD Demo to the slightly changed directory structure.
| * 059cf87 Importing 0.31 from zip download
* | 5423124 Updated labelText and detailsLabelText properties.
|/
* 201c6cd Add hide/show functions.

撤销操作(版本回退)

有一种撤销操作的常见使用场景是提交之后才发现自己忘了添加某些文件,或者写错了提交信息。如果这时你想要重新尝试提交,可以使用--amend选项:

1
2
3
$ git commit -m 'initial commit'
$ git add forgottn_file
$ git commit --amend

上面的例子最终会产生一个提交,因为第二个提交命令修正了第一个提交的结果。

假如你不小心将一个无关文件添加到了暂存区,你想要将该文件移出暂存区,可以使用git reset HEAD <file>…命令把文件移出暂存区。

如果你突然发现,自己不再需要对CONTRIBUTING.md文件所做的更改,可以使用git checkout -- <file>..

如果我们需要回退到某一版本并且放弃所有的修改,可以使用git reset --hard 校验码,把当前的版本回退到上X个版本可以使用git reset --hard HEAD~x。如果我们想会退到之后的版本,可以先执行git reflog找到你要回退的那个版本的校验码,然后使用git rest --hard 校验码就可以成功的会退到当前版本之后的版本了(前提是你是有之后的版本的)。

回退远程仓库的版本

先在本地切换到远程仓库要回退的分支对应的本地分支,然后本地回退至你需要的版本,然后执行:git push <仓库名> <分支名> -f

如何以当前版本为基础,回退指定个commit

首先,确认你当前的版本需要回退多少个版本,然后计算出你要回退的版本数量,执行如下命令:git reset HEAD~X //X代表你要回退的版本数量,是数字!!!!

需要注意的是,如果你是合并过分支,那么背合并分支带过来的commit并不会被计入回退数量中,而是只计算一个,所以如果需要一次回退多个commit,不建议使用这种方法

如何回退到和远程版本一样

有时候,当发生错误修改需要放弃全部修改时,可以以远程分支作为回退点退回到与远程分支一样的地方,执行的命令如下

1
git reset --hard origin/master // origin代表你远程仓库的名字,master代表分支名

远程仓库的使用

远程仓库是指在互联网或其他网络上托管的项目版本仓库。你可以拥有一个或多个远程仓库,而对于其中每个仓库,你可能拥有只读权限或读写权限。要同别人协作就需要管理这些远程仓库,在需要分享工作成果时,向其推送数据,从中拉取数据。管理远程仓库需要知道如何添加远程仓库、移除无效的远程仓库、管理各种远程仓库的分支和设置是否跟踪这些分支。

要查看已经设置了哪些远程仓库,请使用git remote命令。该命令会列出每个远程仓库的简短名称。在克隆某个仓库之后,你至少可以看到名为origin的远程仓库,这是Git给克隆源服务器取的默认名称。你也可以使用-v参数,这样会显示出Git存储的每个远程仓库对应的URL:

1
2
3
4
5
➜  MBProgressHUD git:(master) git remote
origin
➜ MBProgressHUD git:(master) git remote -v
origin https://github.com/jdg/MBProgressHUD.git (fetch)
origin https://github.com/jdg/MBProgressHUD.git (push)

要添加一个远程仓库,并给它起个简短的名称以便引用,可以执行git remote add [shortname] <url>

要从远程仓库获取数据,可以执行git fetch [remote-name]命令。

当你的项目进行到某个阶段,需要与他人分享你的工作成果时,就要把变更推送到远程仓库中去。可以使用git push [remote-name] <branch-name>,如果想把本地的master分支推送到远程的origin服务器上,那么可以执行以下命令,把任意提交推送到服务器端:

1
$ git push origin master

上述命令能够正常工作的前提是必须拥有克隆下来的远程仓库的写权限,并且克隆后没有任何其他人向远程仓库中推送数据。如果别人和你都克隆了这个仓库,而他先推送,你后推送,那么你的这次推送会直接被拒绝。你必须先拉取别人的变更,将其整合到你的工作成果中,然后才能推送。

可以用git remote rename来重命名远程仓库。如果想要把pb重命名为paul,可以用git remote rename命令来实现,如下所示:

1
$ git remote rename pb paul

有时出于某种原因,需要删除某个远程仓库地址,可以使用git remote rm命令,如下所示:

1
$ git remote rm paul

标签

就像大多数版本控制系统一样,Git可以把特定的历史版本标记为重要版本。其典型的应用场景就是标出发布版本(v1.0等)。本节你可学习到如何列举所有可用的标签,如何创建新的标签以及不同标签之间的差异。

在Git中,列举可用标签的操作很简单,只需键入git tag即可:

1
2
3
$ git tag
v0.1
v1.3

Git使用的标记主要有两种类型:轻量标签和注释标签。

轻量标签像是一个不变的分支——他只是一个指向某次提交的指针。

注释标签会作为完整对象存储在Git数据库中。Git会计算其校验和,还包含如标记者的名字、邮箱地址和标签的创建时间,还有标记消息,一般推荐使用注释标签。

注释标签的创建只需执行带有-a选项的tag命令即可:

$ git tag -a v1.4 -m "my version 1.4"

轻量标签基本上就是把提交的校验和保存到文件中,除此不包含任何信息:

$ git tag v1.4

你还可以随后再给之前的提交添加标签。如下

1
2
3
4
5
6
$ git log --pretty=oneline
c954ef3806f135a4d6bb4e18e9b4b64dfd52995a (HEAD -> master, origin/master, origin/HEAD) Merge pull request #513 from niveus/Remove-NSLog
c63cf1840e70f2980e9fae992866945389fa07f1 Remove NSLog
a90765f5f2a825507490355dc9d253e2c202c7ee Merge pull request #90 from matej/matej/hide-timer-race
549fa2854636d00225819a7b120c505b324b5dd9 Add a test case for the hide delayed race
1f1d7ce8bdb6a82d9275163323d503c221cdfe65 Use the new method signature in comments

假如你忘记在Remove NSLog这次提交上添加v1.2的标签,只需在命令最后指定提交的校验和(或部分校验和),执行git tag -a v1.2 c63cf18

默认情况下,git push命令不会把标签传输到远程服务器上。在创建标签之后,你必须明确地将标签推送到服务器上。这个过程有点像推送分支,对应的命令是git push origin [tagname]

$ git push origin v1.5

如果你有很多标签需要一次性推送,可以使用git push命令的--tags选项。

$ git push origin --tags

Git别名

如果你键入的Git命令不完整,Git不会自动推断命令并补全命令。虽然如此,如果你想每次都费力的键入完整的Git命令,也可以轻松通过git config设置每个Git命令的别名。下面是一些你想设置的别名:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

Git分支机制

有些人把Git的分支模型称为Git的杀手锏特性,而这项特性也确实使得Git从众多版本控制系统中脱颖而出。实际上,Git分支功能轻量到了极致,以至于有关分支的操作几乎是及时完成的,并且在不同分支之间切换基本上也同样迅速。

分支机制简述

Git的分支只不过是一个指向某次提交的轻量级的可移动的指针。Git默认的分支名称是master。当你发起提交时,就有了一个指向最后一次提交的master分支。每次提交时,它都会自动向前移动。

基本的分支与合并操作

Git默认的分支名是master。当你想要创建一个新的分支可以使用git branch [branch_name],这会创建一个指向当前提交的新指针。Git是如何确定你当前处在哪个分支上的呢?实际上Git维护着一个名为HEAD的特殊指针。在Git中,HEAD是一个指向当前所在的本地分支的指针。

要切换到已有的分支,可以执行git checkout命令,例如:

1
$ git checkout testing

这条命令会改变HEAD指针,使其指向testing分支。而有一个简单方式可以在创建分支的同时又将HEAD指针切换到该分支上,使用命令git checkout -b

如果你想将分支hotfix合并到master主分支上,可以使用git merge命令来完成操作:

具体操作流程如下:

1
2
$ git checkout master
$ git merge hotfix

合并时你会注意到出现了fast-forward的提示,由于当前所在的master分支指向的提交是要并入的hotfix分支的直接上游,因而Git会将master分支指针向前移动。因为这种单线历史不存在有分歧的工作。这就叫fast-forward

合并完成后我们就可以删除hotfix分支,并继续完成iss53的工作

1
2
3
$ git brancn -d hotfix
$ git checkout iss53
$ git commit -am 'commit'

假设现在iss53上面的工作已经完成,可以合并到master分支了。这次合并操作实现与上面的相同。但操作的结果却不一样了,在这次合并中,开发历史从某个早先的时间点开始有了分叉。由于当前的master分支指向的提交不是iss53分支的直接祖先,因而Git必须要做一些额外的工作。本例中,Git执行的操作是简单的三方合并。三方合并操作会使用两个待合并分支上最新提交的快照,以及这两个分支的共同祖先的提交快照。

与之前的做法不同,这一次Git会基于三方合并的结果创建新的快照,让后再创建一个提交指向新建的快照。这个提交叫做‘’合并提交’‘。合并提交的特殊性在于他拥有不止一个父提交。

与分支有关的工作流

长期分支

很多使用Git的开发者都喜欢用这种方式创建他们自己的工作流。例如,其中一种流程就是在master分支上只存放稳定版的代码。他们会使用另一种叫作develop或next的平行分支用于开发,或是测试代码的稳定性。这个分支不会一直保持稳定版本,不过一旦他达到了稳定版本的状态,就可以把它合并到master分支去。这样的分支也被用来接收主题分支(例如前面的iss53分支)的合并,来确保这些新开发的特性能够通过所有测试而不会引发新的错误。

如下是一个典型的工作流模式:

远程分支

远程分支是指向远程仓库的分支的指针,这些指针存在于本地且无法移动。当你与服务器进行任何网络通信时,它们会自动更新。远程分支有点像书签,它们会提示你上一次连接服务器时远程仓库中每个分支的位置。

假设我们现在有个github.ourcompany.com的服务器,服务器上已存在正在进行的项目:

如果你将内容从服务器上克隆到本地,Git的clone命令会自动把这台服务器命名为origin,并拉取它的全部数据,然后会在本地创建指向服务器上master分支的指针,并命名为origin/master。Git接着会帮你创建你自己的本地master分支。如下所示:

假设你在本地的master分支上进行了一些工作,于此同时,别人向github.outcompany.com推送了数据,更新了服务器上的master分支,这时你的历史提交就与服务器的历史产生了偏差。而且,只要你不与服务器通信,你的origin/master指针就不会移动。

要与服务器同步,需要执行git fetch origin命令。这条命令会查询origin对应的服务器地址,并从服务器取得所有本地尚未包含的数据,然后更新本地数据库,最后把origin/master指针移动到最新位置上去

当需要同别人共享某个分支上的工作成果时,就要把它推送到一个具有写权限的远程仓库。假设你有一个叫做serverfix的分支需要与其他人协作开发,你可以按照之前推送第一个分支的方式推送它。只需执行git push (remote_name) (branch_name)命令即可

1
$ git push origin serverfix

当你和你的同事已经完成了一个功能,并且把工作合并到了远程的主分支master上,你已经不再需要这个功能的远程分支了。可以通过git push --delete选项来删除远程分支。例如:

1
$ git push origin —delete serverfix

变基

在Git中,要把更改从一个分支整合到另一个分支,有两种主要的方式:合并(merge)和变基(rebase)。

比如上面这种情况,我们之前采用的是merge命令,我们依然记得该命令会对两个分支上的最新提交快照以及这两个提交快照最近的共同祖先,进行一次三方合并,并创建一个新的合并提交。

实际上除了上述方式之外还有一种方式:你可以把C5提交的更改以补丁的形式应用到C4上。在Git中,这就叫变基操作。该操作使用的是rebase命令,会把某个分支上的所有提交的更改在另一个分支上重现一遍。这样我们可以对上面的这种情况进行如下操作:

1
2
$ git checkout iss53
$ git rebase master

现在你可以回到master分支进行快进合并(fast-forward merge):

1
2
$ git checkout master
$ git merge iss53

到此为止,通过rebase和merge的方法得到的最终快照是完全一样的,但使用变基的方式可以获得更简洁的提交历史。

变基操作可以带来种种好处。但它并非完美无缺,其缺点可以总结成一句话:不要对已经存在于本地仓库之外的提交执行变基操作。