---

2015/10/25

layout: post
title: "Git pro"
category: Reading Notes

tags: ["读文章", "git"]

{% include JB/setup %}

基本命令

忽略某些文件

$ cat .gitignore

*.[oa] # 忽略所有.a结尾的文件

!lib.a # 但lib.a除外

/TODO # 仅仅忽略项目跟目录下的TODO文件,不包括subdir/TODO

移除文件

要从git中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。可以用git rm命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中。

如果只是简单地从工作目录中手工删除文件,运行git status时就会在“Changed but not updated”部分看到;
然后在运行git rm记录此次移除文件的操作。

最后提交的时候,该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项-f,以防误删除文件后丢失修改的内容。

比如一些大型日志文件或者一堆.a编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在.gitignore文件中不上,用--cached选项即可

$ git rm --cached readme.txt

移动文件

$ git mv file_from file_to

其实,运行git mv就相当于运行了下面三条命令:

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

查看提交历史

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

git log有许多选项可以帮助你搜寻感兴趣的提交

$ git log -p -2
    我们用-p选项展开显示每次提交的内容差异,用-2则仅显示最近的两次更新
--stat 仅显示简要的增改行数统计
--pretty选项,可以指定使用完全不同于默认格式的方式展示提交历史

$ git log --pretty=oneline
    将每个提交放在一行显示

使用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  提交说明

下面的命令列出所有最近两周内的提交:

$ git log --since=2.weeks

例子:

如果要查看 Git 仓库中,2008 年 10 月期间,Junio Hamano 提交的但未合并的测试脚本(位于项目的 t/ 目录下的文件),可以用下面的查询命令:

$ git log --pretty="%h:%s" --author=gitster --since="2008-10-01"    --before="2008-11-01" --no-merges -- t/

撤销操作

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend 

上面的三条命令最终得到一个提交,第二个提交命令修正了第一个的提交内容

取消已经暂存的文件

如果用git add *全加到了暂存区域,可以使用

$ git reset HEAD <filename>

来取消暂存文件

$ git checkout -- benchmarks.rb

添加远程仓库

$ git remote
origin
$ git remote add pb git://github.com/paulboone/ticgit.git
$ git remote -v
origin  git://github.com/schacon/ticgit.git
pb  git://github.com/paulboone/ticgit.git

然后使用

$ git fetch pb

推送数据到远程仓库

$ git push origin master

查看远程仓库信息

$ git remote show origin
* remote origin
URL:git://github.com/schacon/ticgit.git
Remote branch merged with 'git pull' while on branch master
    master
Tracked remote branches
    master
    ticgit

远程仓库的删除和重命名

$ git remote rename pb paul
$ git remote
origin
paul

Git分支

Git保存的不是文件差异或者变化量,而只是一系列文件快照

在Git中提交时,会保存一个提交(commit)对象,它包含一个指向暂存内容快照的指针,作者和相关附属信息,以及一定数量(也可能没有)指向改提交对象直接祖先的指针:第一次提交是没有直接祖先的,普通提交有一个祖先,有两个或多个分支合并所产生的提交则有多个祖先。

为了直观起见,我们假设在工作目录中有三个文件,准备将他们暂存后提交。暂存操作会对每一个文件计算效验和(即SHA-1哈希字串),然后把当前的文件快照保存到Git仓库中,并将效验和加入暂存区域:

$ git add README test.rb LICENSE2
$ git commit -m 'initial commit of my project'

当使用git commit新建一个提交对象前,Git会先计算每个子目录的效验和,然后在Git仓库中将这些目录保存为树(tree)对象。之后Git创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。

现在Git仓库中有五个对象:三个表示文件快照内容的blob对象一个记录着目录树内容及其中各个文件对应blob对象索引的tree对象;以及一个包含指向tree对象(根目录)的索引和其他提交信息元数据的commit对象。仓库中的每个对象保存的数据和相互关系看来应该是这样:

一次提交后仓库里的数据

做些修改再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针。两次提交后,仓库历史会变成如下样子:

多次提交后的Git对象数据

Git中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的master分支,它在每次提交的时候都会自动向前移动。

新建一个Git分支,就是创建一个新的分支指针

$ git branch testing

这会在当前commit对象上新建一个分支指针:
多个分支指向提交数据的历史

Git是如何知道你当前在哪个分支上工作的呢?它保存着一个名为HEAD的特别指针。在Git中,它是一个指向你正在工作中的本地分支的指针。运行git branch命令,仅仅是建立了一个新的分支,但不会自动切换到这个分支中去,所以在这个例子中,我们仍然在master分支里工作

HEAD 指向当前所在的分支

要切换到其他分支,可以执行git checkout命令。我们现在转换到新建的testing分支:

$ git checkout testing

这样HEAD就指向了testing分支

HEAD 在你转换分支时指向新的分支

这样的实现方式会给我们带来什么好处呢?好吧,现在不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'

每次提交后 HEAD 随着分支一起向前移动

现在 testing 分支向前移动了一格,而 master 分支仍然指向原先 git checkout 时所在的 commit 对象

$ vim test.rb
$ git commit -a -m 'made other changes'

分叉了的分支历史

由于Git中的分支实际上仅是一个包含所指对象效验和(40个字符长度SHA-1字串)的文件,所以创建和销毁一个分支就变得非常廉价。说白了,新建一个分支就是向一个文件写入41个字节(外加一个换行符)那么简单。

这和大多数版本控制系统形成了鲜明对比,他们管理分支大多采取备份所有项目文件到特定目录的方式,所以根据项目文件数量和大小不同,可能花费的时间也会有相当大的差别,快则几秒,慢则数分钟。而Git的实现与项目复杂度无关,它永远可以在几毫秒的时间内完成分支的创建和切换。同时,因为每次提交时都记录了祖先信息,所以以后要合并分支时,寻找恰当的合并基础的工作其实已经完成了一大半,实现起来非常容易。

基本的分支与合并

基本分支

首先,我们正在工作,并且已经提交了几次更新:
一部分简短的提交历史

现在,你决定要修补问题追踪系统上的#53问题,运行:

$ git checkout -b iss53
Switched to a new branch "iss53"

相当于下面这两条命令

$ git branch iss53
$ git checkout iss53

然后修正iss53并提交

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

iss53 分支随工作进展向前推进

现在你就接到了网站有问题的紧急电话,要求马上修补。有了git,我们就不需要同时发布这个补丁和iss53里做出的修改,也不需要在创建和发布该补丁到服务器之前花费很多努力来复原这个修改。唯一需要的仅仅是切换回master分支。

$ git checkout master
Switched to branch "master"

接下来进行紧急修补。我们创建了一个紧急修补分支(hotfix)来开展工作

$ git checkout -b 'hotfix'
Switched to a new branch "hotfix"

$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix]: created 3a0874c: "fixed the broken email address"
1 files changed, 0 insertions(+), 1 deletions(-)

 hotfix 分支是从 master分支所在点分化出来的

有必要做些测试,确保修补是成功的,然后把它合并到master分支并发布到生产服务器。用git merge命令来进行合并

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast forward
README |    1 -
1 files changed, 0 insertions(+), 1 deletions(-)

注意:在合并时出现了“fast forward”提示。由于当前master分支所在的commit是要并入hotfix分支的直接上游,Git只需要把指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支,那么Git在合并两者时,只会简单地把指针前移,因为没有什么分歧需要解决,所以这个过程叫做快进(fast forward)

现在的目录变为当前master分支指向的commit所对应的快照,可以发布了。

合并之后,master 分支和hotfix分支指向同一位置

在那个超级重要的修补发布以后,你想要回到被打扰之前的工作。因为现在 hotfix 分支和 master 指向相同的提交,现在没什么用了,可以先删掉它。使用 git branch 的 -d 选项表示删除:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

基本合并

在完成问题#53的工作之后,可以合并到master分支,实际操作同前面合并hotfix分支差不多,只需检出想要更新的分支(master),并运行git merge命令制订来源:

$ git checkout master
$ git merge iss53
Merge made by recursive.
README |    1 +
1 files changed, 1 insertions(+), 0 deletions(-)

请注意,这次合并的实现,并不同于之前 hotfix 的并入方式。这一次,你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的 commit (C4)并非想要并入分支(iss53)的直接祖先,Git 不得不进行一些处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)和它们的共同祖先(C2)进行一次简单的三方合并计算

Git为分支合并自动识别出最佳的同源合并点

Git并没有简单地把分支指针右移,而是对三方合并的结果做了一个新的快照,并自动创建一个指向它的commit(c6)。我们把这个特殊的commit叫做合并提交(merge commit),因为它的祖先不止一个。

Git 自动创建了一个包含了合并结果的 commit 对象

合并的冲突

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

如果你在解决问题#53的过程中修改了hotfix中修改的部分,将得到类似下面的结果:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Git作了合并,但是没有提交,它会停下来等你解决冲突。

要看看哪些文件在合并时发生冲突,可以用git status查阅:

[master*]$ git status
index.html: needs merge
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)\#
#
#       unmerged:   index.html
#

======= 隔开的上半部分,是 HEAD(即 master 分支,在运行 merge 命令时检出的分支)中的内容,下半部分是在 iss53 分支中的内容。解决冲突的办法无非是二者选其一或者由你亲自整合到一起。

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
    please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

如果觉得满意了,并且确认所有冲突都已解决,也就是进入了缓存区,就可以用 git commit 来完成这次合并提交。

分布式工作流程

许多使用Git的开发者都喜欢以这样方式来工作,比如仅在master分支中保留完全稳定的代码,即已经发布或即将发布的代码。与此同时,他们还有一个名为develop或next的平行分支,专门用于后续的开发,或仅用于稳定性测试。这样,在确保这些已完成的特征分支能够通过所有测试,并且不会引入更多错误之后,就可以并到主干分支中,等待下一次的发布。

稳定分支总是比较老旧

稳定分支的指针总是在提交历史中落后一大截,而前沿分支总是比较靠前。

特征分支

在任何规模的项目中都可以使用特征(topic)分支。一个特征分支是指一个短期的,用来实现单一特征或与其相关工作的分支。

例子:

起先我们在 master 工作到 C1,然后开始一个新分支 iss91 尝试修复 91 号缺陷,提交到 C6 的时候,又冒出一个新的解决问题的想法,于是从之前 C4 的地方又分出一个分支 iss91v2,干到 C8 的时候,又回到主干中提交了 C9 和 C10,再回到 iss91v2 继续工作,提交 C11,接着,又冒出个不太确定的想法,从 master 的最新提交 C10 处开了个新的分支 dumbidea 做些试验。

拥有多个特性分支的提交历史

假定两件事情:我们最终决定使用第二个解决方案,即 iss91v2 中的办法;另外,我们把 dumbidea 分支拿给同事们看了以后,发现它竟然是个天才之作。所以接下来,我们抛弃原来的 iss91 分支(即丢弃 C5 和 C6),直接在主干中并入另外两个分支。

合并了 dumbidea 和 iss91v2 以后的历史

远程分支

远程分支(remote branch)是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行Git的网络活动时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。

我们用(远程仓库名)/(分支名)这样的形式表示远程分支。

gitpro