工欲善其事,必先利其器---GIT(二)

远程版本库
Git版本库就是项目目录下的 .git 目录,里面包括所有的历史版本文件。如果这个.git目录发生损坏或被删除那么整个版本库就永远丢失了,或者不小心把整个项目目录删了,那么所有的努力都白费了,我们使用版本控制工具就是为了保证代码不丢失。还有,我们总是需要写协作分享我们的代码,那怎么分享自己的项目目录给别人呢?当然不是靠QQ或发邮件了。

克隆

git clone 命令可以克隆一个版本库,主要有3种形式:

用法1 : git clone <repository> <directory>

用法2 : git clone —bare <repository> <directory>

用法3 : git clone —mirror <repository> <directory>

用法1会克隆一个 <repository>指向的版本库到 <directory> 目录,相当于copy了一个 repository的副本,里面有着一样的工作区,一样的 .git目录。差别是新克隆出来的这个版本库里的.git/config文件会记录上游版本库repository的位置。

用法2克隆出来的版本库不包括工作区,直接就是版本库的内容,也就是不包括.git目录而是直接就是.git目录里面的内容。这样的版本库称为裸版本库。(通过bare名字就可以看出)

用法3和用法2类似,也是克隆出一个裸版本库。不过是可以通过git fetch命令与上游版本库repository持续同步。

$git clone git-demo A #clone一个对等的版本库 A

Cloning into ‘A’…

done.

$git clone —bare git-demo B #clone一个裸的版本库 B

Cloning into bare repository ‘B’…

done.

$git clone —mirror git-demo C #clone一个裸的镜像版本库 C

Cloning into bare repository ‘C’…

done.

$ls -a A #对等版本库A 和 git-demo有着同样的工作区,同时也有.git目录

.git README.md script

$ls -a B #裸版本库B和C里面直接就是.git目录里面的内容

HEAD config description hooks info objects packed-refs refs

$ls -a C

HEAD config description hooks info objects packed-refs refs

$cat A/.git/config

[core]

repositoryformatversion = 0

filemode = true

bare = false #说明是非裸版本库

logallrefupdates = true

ignorecase = true

precomposeunicode = true

[remote “origin”] #当前版本库的上游版本库,名字为 origin

url = /Users/christian/work/tmp/git/git-demo #上游版本库的位置(URL)

fetch = +refs/heads/*:refs/remotes/origin/* #git fetch时的默认引用表达式

[branch “master”] #本地master分支与上游版本库分支的映射,这样执行git pull时 相当于 git pull origin master

remote = origin

merge = refs/heads/master

$cat B/config

[core]

repositoryformatversion = 0

filemode = true

bare = true #说明是裸版本库

ignorecase = true

precomposeunicode = true

[remote “origin”]

url = /Users/christian/work/tmp/git/git-demo #上游版本库的位置(URL)

$cat C/config

[core]

repositoryformatversion = 0

filemode = true

bare = true #说明是裸版本库

ignorecase = true

precomposeunicode = true

[remote “origin”]

url = /Users/christian/work/tmp/git/git-demo

fetch = +refs/*:refs/*

mirror = true #说明是--mirror , 可以执行git fetch命令和上游保持同步

版本库之间的交互有3个命令:

git clone

git pull

git push

git fetch

有两点需要特别注意:

在非裸版本库中可以执行 git pull 和 git push命令同步和推送代码,也可以从一个在非裸版本库pull代码,但是不能向在非裸版本库中push,因为在非裸版本库有工作区,push会导致工作区混乱。

在裸版本库中不可以执行 git pull 和 git push命令,但是可以从裸版本库pull和向裸版本库push.

Tips

Git的裸版本库约定以”.git”结尾,如标准的裸版本库名 git-demo.git 。这里为了例子演示方便,取了好写好记的名字 A B C…

为了方便我们的实验,需要再clone两个版本库

$git clone B D #基于裸版本库B clone一个非裸版本库 D

Cloning into ‘D’…

done.

$git clone B E #基于裸版本库B clone一个非裸版本库 E

Cloning into ‘E’…

done.

没看错,可以从一个裸版本库clone出一个非裸版本库。

$ls -a E #可以看到,E有工作区,也有.git目录

.git README.md script

现在有6个版本库,看起来的关系是这样的

下面我们通过实例,来看一下 git pull,git push,git fetch 这3个命令

$cd git-demo

$git commit —allow-empty -m ‘c1’ #为了方便测试,在git-demo里提交了一个空的commit

[master 472f2ea] c1

$cd ../A

$git pull #在非裸版本库A中,把上游版本库master分支最新的提交pull下来

remote: Counting objects: 1, done.

remote: Total 1 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (1/1), done.

From /Users/christian/work/tmp/git/git-demo

e6361ed..472f2ea master -> origin/master

Updating e6361ed..472f2ea

Fast-forward

$git log —oneline

472f2ea c1 #确实将c1提交pull下来了。

e6361ed add shell and perl scprit.

dd98199 init repo and add README.md

$git commit —allow-empty -m ‘c2’ #在A中也提交了一个新的commit

[master eef952e] c2

$git push #想要推送到上游版本库,但是失败了,因为上游git-demo是个非裸版本库

Counting objects: 1, done.

Writing objects: 100% (1/1), 171 bytes | 0 bytes/s, done.

Total 1 (delta 0), reused 0 (delta 0)

remote: error: refusing to update checked out branch: refs/heads/master

$cd ../B

$git pull #我们到版本库B中,也想pull上游的最新提交c1,但是失败了,因为B没有工作区,是个裸版本库

fatal: This operation must be run in a work tree

$cd ../git-demo

$git push ../B master #我们到git-demo中把提交push到B,说明可以向裸版本库push数据

Counting objects: 1, done.

Writing objects: 100% (1/1), 170 bytes | 0 bytes/s, done.

Total 1 (delta 0), reused 0 (delta 0)

To ../B

e6361ed..472f2ea master -> master

$cd ../C

$git fetch #我们到mirror镜像版本库中fetch上游git-demo的新提交

From /Users/christian/work/tmp/git/git-demo

e6361ed..472f2ea master -> master

通过上面的例子我们基本了解了3中不同类型版本库的区别,下面我们再看个好玩的例子来加深对clone版本库的理解

$cd ../D

$git pull #到非裸版本库D中pull上游裸版本库B的提交

$git log —oneline

472f2ea c1 #提交c1已经被pull下来,说明可以从裸版本库pull代码

e6361ed add shell and perl scprit.

dd98199 init repo and add README.md

$git commit —allow-empty -m ‘c2’ #我们在当前D中创建一个新commit ‘c2’

[master be5099d] c2

$git push origin master #把提交push到上游B, 说明可以向裸版本库push代码

Counting objects: 1, done.

Writing objects: 100% (1/1), 170 bytes | 0 bytes/s, done.

Total 1 (delta 0), reused 0 (delta 0)

To /Users/christian/work/tmp/git/B

472f2ea..be5099d master -> master

$cd ../E

$git pull #到非裸版本库E中pull上游裸版本库B的提交

remote: Counting objects: 2, done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 2 (delta 1), reused 0 (delta 0)

Unpacking objects: 100% (2/2), done.

From /Users/christian/work/tmp/git/B

e6361ed..be5099d master -> origin/master

Updating e6361ed..be5099d

Fast-forward

$git log —oneline #我们看到了D中最新的提交 c2 , 这是从上游裸版本库B 中pull下来的。

be5099d c2

472f2ea c1

e6361ed add shell and perl scprit.

dd98199 init repo and add README.md

上面例子的这个流程有没有觉得很眼熟 ? 对,这就是我们平时的开发流程,从一个远程中央版本库拉取(pull)代码,本地修改,然后推送(push)回远程中央版本库。在这里,版本库 B 就是 远程中央版本库。

版本库 B 就是中央版本库。虽然它在本地而不是在”远程”,大家不要觉得奇怪,因为没有人说中央版本库必须在”远程”。

对于Git版本库来说,从广义上来讲,除了本身以外,其他的版本库都是远程版本库。每个版本库都是平等的,无非是有的版本库处于同一个本地磁盘,有的在网络上。根据所处的位置不同,Git会采用不用的通信协议来进行交互。在本地就用本地协议,在网络上就用 SSH ,GIT,HTTP(S),FTP(S)等网络协议。不同的协议对使用来讲具体来说就是URL不同,其他的原理和使用方式没有任何不同。

在我们的例子中,因为使用的都是 /path/to/file ,这个是本地协议。在最开始的例子中,git clone git@gitlab.renrenche.com:tianle/git-demo.git ,这个用的就是HTTP协议。

不知道大家有没有注意到上面的例子中的这行命令:

$git push ../B master #我们到git-demo中把提交push到B,说明可以向裸版本库push数据

我们在例子中在克隆出来的版本库里向上游push,从上游pull代码。看起来天经地义,因为他们都有个上游可以追溯。但是虽然B是 git-demo的下游,但是对于git-demo里说并不知道这个下游,因为在git-demo没有任何记录下游的信息。(想想也是,一个仓库可以clone出来N个新的仓库,无需也不可能记下来所有的仓库)。好啦,这不是我们要说的重点,重点想说的是git-demo可以向一个没有什么关联的仓库push 。所以,向一个版本库push代码的完整命令可以写为:

git push 版本库 分支

只要Git可以找到这个版本库,就具备向它push的条件。这个版本库可以有3中形式:

1.版本库的路径

版本库的路径就是一个明确的URL,可以指向本地也可以指向网络上的版本库

$cd D

为了实验,我们在gitlab上新建一个名为git-demo2的空project

$git push http://git.dangdang.com/tianle/git-demo2.git master #可以把版本库 D push 到 gitlab上面的git-demo2,虽然它们从未建立过任何联系

Counting objects: 11, done.

Delta compression using up to 4 threads.

Compressing objects: 100% (6/6), done.

Writing objects: 100% (11/11), 805 bytes | 0 bytes/s, done.

Total 11 (delta 2), reused 0 (delta 0)

To http://git.dangdang.com/tianle/git-demo2.git

  • [new branch] master -> master

$git push ../C master #版本库C和D也从未建立过关联,但仍然可以push

Counting objects: 1, done.

Writing objects: 100% (1/1), 170 bytes | 0 bytes/s, done.

Total 1 (delta 0), reused 0 (delta 0)

To ../C

472f2ea..be5099d master -> master

$git init F

Initialized empty Git repository in /Users/christian/work/tmp/git/F/.git/

$cd F

$git pull http://git.dangdang.com/tianle/git-demo2.git master #可以直接从git-demo2 pull代码,而不是必需先clone

remote: Counting objects: 11, done.

remote: Compressing objects: 100% (6/6), done.

remote: Total 11 (delta 2), reused 0 (delta 0)

Unpacking objects: 100% (11/11), done.

From http://git.dangdang.com/tianle/git-demo2

  • branch master -> FETCH_HEAD

说明,只要git能找到命令中的这个版本库,就可以对其push 和 pull操作,并不一定需要clone来建立关联。

2.远程版本库的名字

我们可以为版本库指定一个名字,而不需要每次都写完整的URL。如果我们把下面的配置加到 .git/config配置文件中,就可以给版本库D注册一个远程版本库。

[remote “git-demo2”]

url = http://git.dangdang.com/tianle/git-demo2.git

fetch = +refs/heads/*:refs/remotes/git-demo2/*

$cat .git/config

[core]

repositoryformatversion = 0

filemode = true

bare = false

logallrefupdates = true

ignorecase = true

precomposeunicode = true

[remote “origin”] #名为origin的远程版本库,URL : /Users/christian/work/tmp/git/B

url = /Users/christian/work/tmp/git/B

fetch = +refs/heads/*:refs/remotes/origin/*

[branch “master”] #本地master分支对应的远程版本库分支

remote = origin

merge = refs/heads/master

[remote “git-demo2”] #名为origin的远程版本库,URL : http://git.dangdang.com/tianle/git-demo2.git

url = http://git.dangdang.com/tianle/git-demo2.git 

fetch = +refs/heads/*:refs/remotes/git-demo2/*

$git fetch git-demo2 #直接写名字,不需要写完整URL

From http://git.dangdang.com/tianle/git-demo2

  • [new branch] master -> git-demo2/master

3.省略版本库和分支名

如果 .git/config文件中有类似下面的配置,那么 执行 git push 等价于 git push origin master ,git pull 等价于 git push origin pull

[remote “origin”] #名为origin的远程版本库,URL : /Users/christian/work/tmp/git/B

url = /Users/christian/work/tmp/git/B

fetch = +refs/heads/*:refs/remotes/origin/*

[branch “master”] #本地master分支对应的远程版本库分支

remote = origin

merge = refs/heads/master

理论上只要能定位到一个版本库就可以push/pull但是必须满足三个条件:

1: 必需是裸版本库

2: 有这个版本库push和pull的权限

3: 满足快进式Fast-forward模式,否则无法push成功,pull只会把代码fetch下来,但不会merge

快进式(Fast-forward)

一般情况下,只允许快进式push.所谓快进式推送,就是要推送的本地版本库的提交是建立在远程版本库相应分支的现有提交基础上的,即远程版本库相应分支的最新提交是本地版本库最新提交的祖先提交。非快进式(non-fast-forward)提交是不被允许的,因为这样每个人都随便提交的话会互相覆盖,会弄乱版本库,版本库无法保证一个完整的提交链。

Tip

广义上来说,当前版本库之外的版本库都是远程版本库,而上游版本库指的是通过git clone 或 git remote add 所指向的那个版本库。所以上游版本库是远程版本库的一个子集。但是在实际工作中,几乎所有的版本库都是通过git clone而来,所以一般情况下远程版本库和上游版本库是同一个意思。

因为每个版本库都是平等的,没有一个严格主次之分,所以说,Git是分布式的版本库,每个版本库都保存这所有的历史记录

远程版本库

我们现在已经了解了Git版本库之间的关系和交互方式,我们也应该清楚了Git是分布式的版本库,而不是向SVN这种集中式的版本库。各版本库之间没有主次之分,是平等的。

对于一个项目而言,你可以从你团队中任何一个人机器上的版本库上同步代码,也可以向他的版本库推送代码,所以在理论上来说,并不像SVN那样需要一台远程中央版本库。

但是在实际工作中,这比较难做到,因为首先不能保证个人的机器都不关机,而且最重要的原因是每个人版本库的提交都不一样,不能今天从A那同步代码,明天又向B提交代码。

所以就一定要在这个团队中固定一个人的版本库,大家都从他的版本库同步代码,也向他的版本库提交代码。这样大家的代码在能保证是同步更新的。那即使选定了一个固定的人但又不能保证他一直不关机。所以,我们需要找一个空闲的单独的稳定的服务器来做这个版本库,大家都从这里更新,向这个提交。这个版本库就是实际工作中的“远程版本库”。

因为这本版本库一定是在网络上,所以本机上的版本库和它交互就不能通过本地协议了,需要通过HTTP HTTPS SSH GIT等网络协议才行。

虽然这是一个远程版本库,但并不是一个主版本库,它和每个人机器上的版本库一样,没啥区别。只不过它在一台稳定的不断电的机器上,并且大家都通过它来协作而已。这个所谓的“远程”版本库就是一个普通的git版本库,只不过它恰巧在一个大家都通过网络可以访问到的机器上。

好了,不绕弯子了。我们实际工作中的这个远程版本库就是 git.dangdang.com

我们通过例子看一下是怎么和这个远程版本库交互的,其实这些例子和上面的例子没有任何不同,只是会更具体一些,也对实际工作更有意义。

其实说的还是 git clone, git pull, git push, git fetch这些命令,我们现在就来看看这几个命令是怎么应用于实际工作的。

我们要参与一个项目,首先我们要先从gitlab上clone一个工程到本地。git clone URL 本地目录

$git clone git@gitlab.renrenche.com:tianle/git-demo.git

$cd git-demo #工程目录名是git-demo

这样我们就把git-demo工程克隆到本地的 git-demo目录。当然我可以指定这个目录的名称

$git clone git@gitlab.renrenche.com:tianle/git-demo.git your_code

$cd your_code #工程目录名是your_code

我们注意到远程版本库的后缀是’.git’,根据前面介绍的约定 ‘.git’后缀的版本库是裸版本库。

所以,远程版本库服务器上面的版本库都是裸版本库。

克隆下来的版本库会默认把他所克隆的这个版本库注册为上游版本库,并且起名为 origin . origin的英文就是‘起源’的意思,可以望文生义。上游版本库可以叫任何名字,只不过origin是git默认的上游版本库的名字,当需要写这个名字的地方却省略时,git默认认为是origin。除此以外,还做了一个本地master分支和远程版本库master分支的映射。

就因为有了这两个映射,所以在本地master分支上执行git pull origin master等价于 git pull, git push origin master等价于 git push

$cat .git/config

[core]

repositoryformatversion = 0

filemode = true

bare = false

logallrefupdates = true

ignorecase = true

precomposeunicode = true

git clone之后在本地版本库一定有下面的这2个配置节点

[remote “origin”] #注册上游版本库

url = git@gitlab.renrenche.com:tianle/git-demo.git

fetch = +refs/heads/*:refs/remotes/origin/*

[branch “master”] #本地master分支到origin的master分支的映射

remote = origin

merge = refs/heads/master

和 git init 一样 git clone之后版本库默认只有一个master分支,master分支指向了origin/master分支相同的提交号 。

但是远程有master和develop两个分支,那我们如何得到develop分支呢?等等,是怎么知道远程版本库有master和develop的呢?

奥秘就存在于文件 .git/packed-refs 之中 ,git clone之后 会把了远程版本库的分支和对应的提交号存在这个文件里

$cat .git/packed-refs #这个文件记录了远程版本库的分支和对应的提交号

472f2eaf555b622c4996d7cd17e2337c0c7fc448 refs/remotes/origin/develop

e6361ed35aa40f5bae8bd52867885a2055d60ea2 refs/remotes/origin/master

我们得到了这些commitID就自然找到了对应的commit对象,找到了commit对象也就找到了他的所有tree,blob对象。也就是得到了这个提交对应的所有文件。

那么这些对象在哪呢?当然是在 .git/objects里了。这个在前面已经说了无数遍了。

git clone 命令会把远程版本库的git对象下载下来到 .git/objects目录里,把分支引用保存到.git/packed-refs文件中。得到了引用和git对象,就得到了整个版本库了。

如果这个手册是从头到尾学习的,那么到现在为止理解这个概念应该非常容易了。

现在我们关注下 .git/refs目录

$tree .git/refs/

.git/refs/

├── heads

│ ├── develop

│ └── master

├── remotes

│ └── origin

│ ├── HEAD

└── tags

我们知道本地的分支文件在 .git/refs/heads目录下。从这个目录结构看,聪明的我们一定会想到,远程的分支文件应该在 .git/refs/heads/remotes/origin下才对。

本地的分支文件在 .git/refs/heads目录下,引用指向 .git/refs/objects目录里的git对象,远程的分支在 .git/refs/heads/remotes/origin下,引用也指向 .git/refs/objects目录里的git对象的话,这样就特别好理解了。可惜,目前看 .git/refs/heads/remotes/origin里面并没有master 和 develop两个分支文件。而是把分支引用保存在 .git/packed-refs文件里。

其实这只是暂时的,当远程版本库有新提交,并且本地版本库执行了git pull或 git fetch命令后,.git/refs/heads/remotes/origin目录下会生成对应的分支文件。

其实在git2.0之前,git clone之后就会有这2个分支文件,而不是 .git/packed-refs。为什么git2.0之后是这样的设计,我也不是很清楚,表示不理解。

好,我们现在知道了git clone的原理了。那我们就基于origin/develop来建一个本地develop分支

$git checkout -b develop origin/develop

我们来理解下这个命令。

我们知道 git checkout -b <commit> 命令可以建立并切换到一个分支。所以 git checkout -b develop origin/develop 就是建立develop分支并指向 origin/develop 的提交号。

origin/develop 就是远程develop分支,他的提交号保存在 .git/packed-refs文件里。

也可以通过 git rev-parse 命令查看

$git rev-parse origin/develop

472f2eaf555b622c4996d7cd17e2337c0c7fc448

其实就是想说明,git clone 把远程版本库的git对象和分支引用下载到本地后,一切的操作和本地的任何操作是一样的。

唯一区别是,分支文件的目录不同罢了

本地分支文件保存在 .git/refs/heads 目录

远程分支文件保存在 .git/refs/heads/origin目录

这下,明白了吗

让我们在看下现在 .git/config 文件的内容

$cat .git/config

[remote “origin”]

url = git@gitlab.renrenche.com:tianle/git-demo.git

fetch = +refs/heads/*:refs/remotes/origin/*

[branch “master”]

remote = origin

merge = refs/heads/master

[branch “develop”] #多了一个新的[branch]节点

remote = origin

merge = refs/heads/develop

git checkout -b develop origin/develop 命令会自动为新建的分支设置一个上游(upstream)对应分支.

好了,现在我们本地有两个分支master和develop分别对应着远程的master,develop分支。实际工作中就是这么做的,保持同样的名字是为了规范便于记忆,方便管理。

但是本地分支和远程分支的映射的名字并没有任何限制。做个简单实验说明一下

$git checkout -b abc123 origin/develop

Branch abc123 set up to track remote branch develop from origin.

Switched to a new branch ‘abc123’

$cat .git/config

[remote “origin”]

url = git@gitlab.renrenche.com:tianle/git-demo.git

fetch = +refs/heads/*:refs/remotes/origin/*

[branch “master”]

remote = origin

merge = refs/heads/master

[branch “develop”]

remote = origin

merge = refs/heads/develop

[branch “abc123”] #多了一个新的映射

remote = origin

merge = refs/heads/develop

好了,到这里聪明的你已经不需要我在过多解释了。

git clone之后本地就会有一个名为 origin的上游,上游的分支信息保存在 .git/refs/remotes/origin 目录下 。

其实git版本库可以有N个上游版本库,这完全取决于实际的需要。

git remote add 上游名 URL 命令可以添加上游版本库。现在我们就用这个命令再添加一个上游版本库,随便起个名字比如叫 upstream2

$git remote add upstream2 http://git.dangdang.com/tianle/git-demo2.git

$cat .git/config

[remote “origin”]

url = git@gitlab.renrenche.com:tianle/git-demo.git

fetch = +refs/heads/*:refs/remotes/origin/*

多了一个[remote]节点,名为upstream2

[remote “upstream2”]

url = http://git.dangdang.com/tianle/git-demo2.git

fetch = +refs/heads/*:refs/remotes/upstream2/*

我们还可以修改、重命名和删除一个上游版本库

$git remote set-url upstream2 git@gitlab.renrenche.com:tianle/git-demo.git #修改upstream2上游版本库的URL

$git remote rename upstream2 upstream1 #把upstream2重命名成upstream1

$cat .git/config

[remote “origin”]

url = git@gitlab.renrenche.com:tianle/git-demo.git

fetch = +refs/heads/*:refs/remotes/origin/*

[remote “upstream1”] #名字变为了upstream1,之前是upstream2

url = git@gitlab.renrenche.com:tianle/git-demo.git #URL变成了git-demo.git , 之前是git-demo2.git

fetch = +refs/heads/*:refs/remotes/upstream1/*

$git remote rm upstream1 #删除上游版本库upstream1,.git/config文件的[remote “upstream1”]节点会被删掉

总结一下,git remote 的相关命令就是修改 .git/config文件而已,其实我们也可以手动编辑完成。

git fetch 命令可以把上游版本库的更新下载到本地版本库,这个更新包括Git对象和分支引用。

当上游版本库有新的提交后,上游版本库相关的分支文件会被更新,objects目录里会多了最新提交所对应的commit , tree,和blob对象。git fetch命令就是负责把这些更新下载到本地版本库。

现在我们通过一个实验来直观的看一下git fetch干了什么,并且这个实验可以把前面的一些命令串起来。

实验步骤:

克隆一份新版本库demo-for-fetch,demo-for-fetch向develop分支推送一个新的提交

在本地老的版本库里git fetch

克隆一份新版本库demo-for-fetch,demo-for-fetch向develop分支推送一个新的提交

$git clone git@gitlab.renrenche.com:tianle/git-demo.git demo-for-fetch

$cd demo-for-fetch

$git checkout -b develop origin/develop

$echo ‘.jar’ >> .gitignore ; echo ‘.war’ >> .gitignore ; echo ‘.ear’ >> .gitignore; echo ‘/target/*’ >> .gitignore

$git add .

$git commit -m ‘add .gitignore file’

$git push origin develop

$cat .git/refs/heads/develop

d83003a2e746416d37101acd6d8fbb8557aab63e #develop分支的提交号是d83003a

$cd git-demo

$git fetch

remote: Counting objects: 3, done. #下载了3个对象文件

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

From http://git.dangdang.com/tianle/git-demo

472f2ea..d83003a develop -> origin/develop #更新分支文件 注意提交号 d83003a

$tree .git/refs/remotes/origin/

.git/refs/remotes/origin/

├── HEAD

└── develop #fetch后 在 refs/remotes/origin生成了一个分支文件

$cat .git/refs/remotes/origin/develop

d83003a2e746416d37101acd6d8fbb8557aab63e #origin/develop分支的提交号是d83003a

在我们的例子中远程版本库master并没有新的提交,所以fetch后本地refs/remotes/origin目录并没有生成一个master分支文件。有兴趣的话可以查看下 .git/objects目录,看看不是不多了3个文件。

篇幅所限,不在这里罗列了。

git pull命令等价于 git fetch + git merge

git pull 命令先把远程版本库的更新下载到本地,然后和本地的分支合并 。

$git pull origin develop

上面这条命令和用下面的两条命令的做的事情一样

$git fetch

$git merge origin/develop

所以 git pull命令是分成了来给你个阶段完成的。阶段一从远程下载更新,需要网络。阶段二完全是正常分支间的合并操作,都是本地完成,不需要网络。

git pull命令是工作中最重要,最常用的命令之一,但是真的没什么需要过多描述的,因为他是两个命令的组合,fetch命令详细介绍完了,merge命令会在后面详细介绍

git push 命令会把本地版本库的git对象上传到远程版本库,并用本地分支引用更新远程版本库对应分支的引用。

当然不是把本地的所有对象都上传,而只是新提交所对应的那些git对象。

在push之前,必需要获得远程版本库的最新提交信息,而这个提交必需是我们本地提交的父提交,这就是前面提到的 快进式(Fast-forward),否则不能push成功。

在push操作时,必需要指定一个远程的分支,也就是要把提交推送到一个明确的分支。 所以在push之前,我们要先取得这个远程分支的提交并作为我们本地提交的祖先提交。

在这个祖先提交之后所有提交就是最新提交。push就是把这些提交对应的git对象上传到远程版本库。

所以,在push之前,一定要先pull 。git pull会取得最新提交并和当前分支merge,merge之后最新提交就是本地提交的祖先提交了。

在为本地分支建立了一个上游分支之后可以通过git status查看本地分支领先了远程分支几个commit

$git commit —allow-empty -m ‘c2’

$git status

On branch develop

Your branch is ahead of ‘origin/develop’ by 1 commit. #仔细看这一行提示,现在origin/develop分支之后有了1次commit

(use “git push” to publish your local commits)

nothing to commit, working tree clean

$git commit —allow-empty -m ‘c3’

$git status

On branch develop

Your branch is ahead of ‘origin/develop’ by 2 commits. #仔细看这一行提示,现在origin/develop分支之后有了2次commit

(use “git push” to publish your local commits)

nothing to commit, working tree clean

现在我们执行 git push origin develop会成功吗?答案是 不一定。

由于我们的本地develop分支是基于origin/develop分支创建的,所以这之后的提交的祖先提交就是origin/develop分支所对应的提交。

如果在这个时间段内,远程版本库没有新的提交,那么就符合快进式提交条件,可以push成功。如果这个时间段内远程有新的提交,就不能push成功。

所以 , 在每次push前 一定要先pull一下,同步一下远程版本库的最新信息。

因为目前除了我没有人会更新git-demo,所以我们的push当然时成功的啦

$git push origin develop

Counting objects: 2, done.

Delta compression using up to 4 threads.

Compressing objects: 100% (2/2), done.

Writing objects: 100% (2/2), 256 bytes | 0 bytes/s, done.

Total 2 (delta 1), reused 0 (delta 0)

remote:

remote: Create merge request for develop:

remote: http://git.dangdang.com/tianle/git-demo/merge_requests/new?merge_request%5Bsource_branch%5D=develop

remote:

To git@gitlab.renrenche.com:tianle/git-demo.git

d83003a..05d68dd develop -> develop

push不光可以推送到远程已经存在的分支,也可以推送到不存在的分支,当推送到不存在分支时,就创建了这个远程分支。

push到已经存在的分支时需要受到快进式(Fast-forward)的限制,但是push到不存在的分支时一定是成功的。

$git checkout develop

$git push origin develop:feature

  • [new branch] develop -> feature #这样就在远程版本库创建了一个feature新分支

所以 push命令的比较完整的写法是 git push 上游版本库 本地分支:远程分支,当本地分支于远程分支同名时只需 git push 上游版本库 本地分支

$git push origin :feature # git push 上游版本库 :远程分支 可以删除远程分支

To git@gitlab.renrenche.com:tianle/git-demo.git

  • [deleted] feature

重要命令详解

start a working area (see also: git help tutorial)

clone Clone a repository into a new directory

init Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)

add Add file contents to the index

mv Move or rename a file, a directory, or a symlink

reset Reset current HEAD to the specified state

rm Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)

bisect Use binary search to find the commit that introduced a bug

grep Print lines matching a pattern

log Show commit logs

show Show various types of objects

status Show the working tree status

grow, mark and tweak your common history

branch List, create, or delete branches

checkout Switch branches or restore working tree files

commit Record changes to the repository

diff Show changes between commits, commit and working tree, etc

merge Join two or more development histories together

rebase Reapply commits on top of another base tip

tag Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)

fetch Download objects and refs from another repository

pull Fetch from and integrate with another repository or a local branch

push Update remote refs along with associated objects

‘git help -a’ and ‘git help -g’ list available subcommands and some

concept guides. See ‘git help <command>‘ or ‘git help <concept>‘

to read about a specific subcommand or concept.

可以直接参考官方英文文档 : https://git-scm.com/docs

config

config命令可以设置Git的一些信息,分为系统、用户和项目3个作用域。

系统作用域的配置文件

/etc/gitconfig

这个文件里配置信息的作用范围是整台台机器

用户作用域的配置文件

/.gitconfig

这个文件里配置信息的作用范围是当前用户

项目作用域的配置文件

repo/.git/config

这个文件里配置信息的作用范围是仅在当前的项目

git config 命令加上不同的参数就可以配置不同作用域的文件。

相同配置项的优先级是 项目作用域 > 用户作用域 > 系统作用域

git config —system 设置 /etc/gitconfig

git config —global 设置 ~/.gitconfig

git config 设置 repo/.git/config

设置当前用户的name和email

$git config —global user.name “tianle”

$git config —global user.email “tianle@dangdang.com”

设置全局的status命令别名

$git config —system alias.st status

在一个Git项目目录下执行这个命令,只对这个项目生效

$git config alias.ci commit

加上 -e ,会直接打开~/.gitconfig文件然后手工编辑。当然了,也可以直接 vim ~/.gitconfig编辑,是一样的

$git config -e —global

$git config —global user.name #这会显示user.name的值

tianle

init

git init 命令可以创建一个空的Git Repository ,也可以把任何一个目录变成一个 Git Repository 。

git init 命令后跟着一个目录名就会创建一个空的Git Repository,否则就是把当前的目录变成一个 Git Repository。

一个空的Git Repository目录里面除了有一个.git目录之外什么都没有

$git init demo #创建新git仓库

Initialized empty Git repository in /Users/christian/demo/.git/

$git init #把当前目录变成git仓库

Initialized empty Git repository in /Users/christian/.git/

clone

clone 命令可以克隆一个版本库。非常简单好用,完整形式是 git clone <目录> ,目录可以省略

$git clone git@gitlab.renrenche.com:tianle/git-demo.git

Cloning into ‘git-demo’…

$git clone git@gitlab.renrenche.com:tianle/git-demo.git abc

Cloning into ‘abc’…

add

git add 命令将工作区的修改写到暂存区。完整形式是 git add <file>, 不仅是将新文件添加进暂存区,也将已添加问价的修改写到暂存区

$git add READMD.md .gitignore # 多个文件用空格分开

在工作中经常是修改了很多文件,这样一个个添加些起来太麻烦,所以一般常用的是下面3中形式:

git add . :他会监控工作区的状态树,使用它会把工作时的所有变化提交到暂存区,包括文件内容修改(modified)以及新文件(new),但不包括被删除的文件。

git add -u :他仅监控已经被add的文件(即tracked file),他会将被修改的文件提交到暂存区。add -u 不会提交新文件(untracked file)。(git add —update的缩写)

git add -A :提交所有变化

status

git status 命令可以查看工作区和暂存区的状态,它的结果就是输出一堆信息,初用git时会很晕,很烦,慢慢就习惯了

$git status

On branch develop

Your branch is up-to-date with ‘origin/develop’.

Changes to be committed: #暂存区需要commit到版本库的文件

(use “git reset HEAD <file>…” to unstage)

modified:  .gitignore

Changes not staged for commit: #已经加入到暂存区之后又修改了的文件,这时要么再次add暂存区,要么用暂存区的快照覆盖工作区这个文件

(use “git add <file>…” to update what will be committed)

(use “git checkout — <file>…” to discard changes in working directory)

modified:  README.md

Untracked files: #还从来没有add到暂存区的文件 ,Untracked ,未跟踪

(use “git add <file>…” to include in what will be committed)

a.txt

diff

git diff 命令可以查看工作区,暂存区,版本库之间文件的差别

git diff 工作区和暂存区之间的差别

git diff HEAD 工作区和版本库之间的差别

git diff —staged 暂存区和版本库之间的差别

commit

git commit 命令生成一个commit对象以及这个commit对象对应的一系列tree对象和blob对象。这个commit对象的tree对象的与暂存区的目录树指向同样的内容。

常用的用法是 git commit -m ‘commit message’ ,最近的一次 ‘commit message’会保存在 .git/COMMIT_EDITMSG 文件中。我们知道对于已经Tracked的文件(即已经加入到暂存区里的),

修改之后还是需要 git add 后才能提交,有时这样会比较麻烦 ,所以可以用 git commit -am ‘commit message’ 来直接提交这些修改,而不需要先 add , 但是这种做法并不被推荐。

看一下提交的SHA1哈希值的生成方法:

先看一下头指针所对应提交的SHA1

$git rev-parse HEAD

05d68dddd48bef2a05f974571b771788749fe3f4

查看提交05d68dd的内容

$git cat-file -p HEAD

tree 27d5aa6b43e7b30317d3b977c92d9a2e5b918572

parent c34ab20d3710fae6e90c60e00bfb734f8a1c982c

author tianle tianle@dangdang.com 1495011229 +0800

committer tianle tianle@dangdang.com 1495011229 +0800

c3

提交05d68dd的内容共有207个字符

$git cat-file -p HEAD |wc -c

207

通过这个shell命令可以得出SHA1哈希值

$(printf “commit 207\000”;git cat-file -p HEAD) | sha1sum

05d68dddd48bef2a05f974571b771788749fe3f4 - #与git生成的SHA1哈希值一样

可以通过 git commit —amend -m ‘c3’ 修改上一次提交的描述,也可以 git commit —amend 直接打开编辑器修改。

log

git log 命令可以查看当前分支的提交历史,加上不同的参数显示的内容会略有不同。

常见的git log 使用形式:

git log 这是最常用的一种方式,显示当前分支的提交历史

$git log

commit e6361ed35aa40f5bae8bd52867885a2055d60ea2

Author: tianle tianle@dangdang.com

Date: Wed May 10 11:07:52 2017 +0800

add shell and perl scprit.

commit dd981999876726a1d31110479807e71bba979c44

Author: tianle tianle@dangdang.com

Date: Fri May 5 19:00:48 2017 +0800

init repo and add README.md

git log —oneline 精简的现实,每个提交只显示一行

$git log —oneline

e6361ed add shell and perl scprit.

dd98199 init repo and add README.md

git log —graph 通过简单图形现实分支之间的merge关系,但是不太好用,一般都用工具看。

git log —pretty=raw 现实提交的详细内容

git log —stat 可以现实本次提交所新增和修改的文件

git log -数字 只现实数字指定的最近几次提交

$git log -1 —pretty=raw —stat

commit e6361ed35aa40f5bae8bd52867885a2055d60ea2

tree 8d384da6a7ebc4b88cc5fc5e45d609faf9b2cb29

parent dd981999876726a1d31110479807e71bba979c44

author tianle tianle@dangdang.com 1494385672 +0800

committer tianle tianle@dangdang.com 1494385672 +0800

add shell and perl scprit.

script/perl/test2.pl | 1 +

script/test1.sh | 1 +

2 files changed, 2 insertions(+)

branch

git branch 配合不同的参数可以查看、创建、删除、重命名一个分支。

git branch 查看本地分支 ,git branch -r 查看远程分支 , git branch -a 查看所有分支,git branch -v 现实分支对应的提交号

git branch 分支名 ,基于头指针(HEAD)创建一个分支

git branch 分支名 提交号 基于指定的提交号创建一个分支

git branch -D 分支名删除一个分支

git branch -m 旧分支名 新分支名 重命名一个分支

由于git branch命令很简单,这里就不再贴演示代码了,可以自行练习

checkout

git checkout 命令是Git最常用的命令之一,主要用来切换分支和重写工作区。

git checkout 分支名 用来切换分支,更新HEAD以指向branch分支。

git checkout 或 git checkout HEAD 汇总显示工作区,暂存区与HEAD的差异。

git checkout — 文件名 用暂存区的文件覆盖工作区的相应文件。

git checkout . 最后的这个点’.’是参数, 用暂存区的所有文件替换工作区的所有文件,但是不包括Untracked 的文件。

git checkout 提交号或分支名 — 文件名 用提交中的文件替换暂存区和工作区的相应文件。

git checkout 提交号或分支名 . 最后的这个点’.’是参数,用提交中的文件替换暂存区和工作区的相应文件,但是不包括Untracked 的文件。

git checkout 可以切换分支,其实就是这个命令修改了HEAD文件的内容

reset

git reset 命令是Git最常用的命令之一,用来重置分支的引用和替换暂存区和工作区。

git reset —hard 提交号 这个命令会将当前分支的引用指向新的提交号。替换暂存区,替换后暂存区的内容和新提交的tree一致。替换工作区,替换后工作区内容和暂存区一致。执行这个命令后会让工作区 暂存区 版本库的内容一致。

git reset —soft 提交号 这个命令只会更变当前分支的引用,不影响暂存区和工作区。比如 git reset —soft HEAD^ 会将引用向前回退一次。

git reset —mixed 提交号 这是git reset命令默认的形式,改变当前分支的引用和替换暂存区,不影响工作区。

git reset 或 git reset HEAD 命令会用HEAD指向的目录树(tree)重置暂存区,不影响工作区。

git reset 文件名 或 git reset HEAD 文件名 这个命令会将对应文件的改动撤出暂存区,即用HEAD提交中的文件替换暂存区中的文件,不影响工作区。

git reset 可以重置分支引用,是因为他修改了 refs/heads/下的分支文件的内容。

tag

git tag 命令可以创建和删除里程碑

git tag 查看当前的里程碑

git tag tagname 创建一个轻量级的里程碑

git tag -m tagname 创建一个带说明的里程碑

git tag -d tagname 删除一个里程碑

git push 上游版本库 tagname 把本地里程碑上传到远程版本库

git push 上游版本库 :tagname 删除远程里程碑

merge

git merge 命令可以合并两个分支。如果这两个分支合并后不是fast-forward模式,那么会生成一个新的合并提交。这个合并的提交会有两个父提交,分别是这两个分支的最新提交。

fast-forward前面介绍过,就是一个分支的最新提交是另一个分支的祖先提交时,那么就是fast-forward(快进式)。

如果在master分支上合并develop分支,那么develop就是被合并的分支。从master分支检出develop后,我们在在develop分支上开发,提交。然后在合并到master ,这时就是就是fast-forward。

如果master分支上也产生了新的提交,那么合并时就不是fast-forward,这样就会产生一个新的提交。合并后的分支会包括两个分支所有的提交。

$git init test

Initialized empty Git repository in /Users/christian/tmp/git/test/.git/

$cd test

$git commit —allow-empty -m ‘c1’

[master (root-commit) ce2518d] c1

$git checkout -b develop

Switched to a new branch ‘develop’

$git commit —allow-empty -m ‘c2’

[develop 0f2635c] c2

$git checkout master

Switched to branch ‘master’

$git commit —allow-empty -m ‘c3’

[master 33c2269] c3

$git merge develop

Already up-to-date!

Merge made by the ‘recursive’ strategy.

$git log

commit 78eb08a6bdabab81442433a9878ade17c94fea5e #非fast-forward合并会自动生成一个commit ,这个commit的2个parent分别指向了2个分支的头提交

parent 33c2269c9c3fe268ee693d9873bc5d416705db5f

parent 0f2635c440b355dbba4753212cb3a742483ede0d

Merge: 33c2269 0f2635c

Author: tianle tianle@dangdang.com

Date: Thu May 18 12:43:52 2017 +0800

Merge branch 'develop'

commit 33c2269c9c3fe268ee693d9873bc5d416705db5f

Author: tianle tianle@dangdang.com

Date: Thu May 18 12:43:48 2017 +0800

c3

commit 0f2635c440b355dbba4753212cb3a742483ede0d

Author: tianle tianle@dangdang.com

Date: Thu May 18 12:43:39 2017 +0800

c2

commit ce2518dd68f43baf9e53b08d9adc7108ab84166d

Author: tianle tianle@dangdang.com

Date: Thu May 18 12:43:25 2017 +0800

c1

merge可能会产生冲突,有冲突不要怕。处理完冲突文件后 git add 再 git commit就完成了合并。如果觉得冲突实在是太闹心,可以 git merge —abort 放弃合并。

如果有冲突,会生成一个 .git/MERGE_MSG文件,记录了冲突的文件列表。

rebase

git rebase 命令也可以用来合并两个分支,也可以用来压缩提交历史。

merge合并分支可能会产生一个合并的提交,这样 git log —graph 看分支的话就会有两条线。 很起来会乱一些。rebase合并分支,会让历史看起来像没有经过任何合并一样。

git merge命令合并之后

$git log —graph —oneline

  • 78eb08a Merge branch ‘develop’

|\

| * 0f2635c c2

  • | 33c2269 c3

|/

  • ce2518d c1

现在我们在上面的例子基础上用rebase合并master 和 develop

$git reset —hard HEAD^^

$echo a > a.txt

$git add .

$git commit -m ‘c3’

[master b802612] c3 #注意这个commitID : b802612

$git rebase develop

$git log —graph —oneline #没有产生合并提交,提交历史是一条线,但是c3点的commitID变了。所以原来的c3提交被取消,又重新生成了一个

  • 48be474 c3

  • 0f2635c c2

  • ce2518d c1

这些命令会把你的”master”分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到”.git/rebase”目录中),然后把”master”分支更新 到最新的”develop”分支,最后把保存的这些补丁应用到”master”分支上。

当’master’分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。

借用网上的图说明下。

show-ref

git show-ref 可以快速查看本地分支,远程分支,Tag所对应的提交SHA1哈希值

$git show-ref

5a9bf387e76c63c481c2453ede6e6c87678b9ecb refs/heads/develop

e6361ed35aa40f5bae8bd52867885a2055d60ea2 refs/heads/master

d83003a2e746416d37101acd6d8fbb8557aab63e refs/remotes/origin/develop

e6361ed35aa40f5bae8bd52867885a2055d60ea2 refs/remotes/origin/master

baa10bc4dc2ba6ff13e0bd0cdd50d8e9d36564a8 refs/tags/v1.0

rev-parse

git rev-parse 可以查看一个引用名字所对应的提交SHA1哈希值

$git rev-parse HEAD

e6361ed35aa40f5bae8bd52867885a2055d60ea2

$git rev-parse master

e6361ed35aa40f5bae8bd52867885a2055d60ea2

$git rev-parse refs/heads/master

e6361ed35aa40f5bae8bd52867885a2055d60ea2

$git rev-parse v1.0

baa10bc4dc2ba6ff13e0bd0cdd50d8e9d36564a8

$git rev-parse origin/master

e6361ed35aa40f5bae8bd52867885a2055d60ea

cat-file

git cat-file 命令可以查看一个Git对象的类型和对象内容

git cat-file -t 对象引用名或SHA1 ,查看对象类型; git cat-file -p 对象引用名或SHA1 查看对象内容

$git cat-file -t HEAD

commit

$git cat-file -t HEAD^{tree}

tree

[christian~/work/tmp/git/git-demo] (master) ]$git cat-file -p HEAD^{tree}

100644 blob 44601d12328ea8e04367337184dcccb85859610e README.md

040000 tree 16a87dbed191bcfb19a4af9d0cc569f6448a01cc script

[christian~/work/tmp/git/git-demo] (master) ]$git cat-file -p HEAD

tree 8d384da6a7ebc4b88cc5fc5e45d609faf9b2cb29

parent dd981999876726a1d31110479807e71bba979c44

author tianle tianle@dangdang.com 1494385672 +0800

committer tianle tianle@dangdang.com 1494385672 +0800

add shell and perl scprit.

[christian~/work/tmp/git/git-demo] (master) ]$git cat-file -p e6361ed

tree 8d384da6a7ebc4b88cc5fc5e45d609faf9b2cb29

parent dd981999876726a1d31110479807e71bba979c44

author tianle tianle@dangdang.com 1494385672 +0800

committer tianle tianle@dangdang.com 1494385672 +0800

add shell and perl scprit.

常见问题简答

如何创建Git版本库?

git init 或 git clone

如何为当前本地版本库添加远程版本库?

git remote add origin URL

git checkout 和 git reset的区别 ?

checkout修改HEAD文件的内容,reset修改refs/heads下面分支文件的内容。同时他们都可以替换工作区和暂存区的文件。

git merge 和 git rebase的区别?

都可以合并分支,但是一般建议用merge。因为rebase会改变已经生成的提交号,如果这个分支是大家协作的分支,那么你改了提交号之后会带来整个分支的混乱。

SHA值是怎么生成的?

通过SHA1消息摘要算法,可以使用系统命令 sha1sum

如何产生公钥?

ssh-keygen -t rsa -C “tianle@dangdang.com”

cat ~/.ssh/id_rsa.pub

超棒的git在线学习网站

https://try.github.io

http://learngitbranching.js.org/

参考

https://git-scm.com/

http://nvie.com/posts/a-successful-git-branching-model/

https://sandofsky.com/blog/git-workflow.html

http://gitbook.liuhui998.com/index.html

https://git.wiki.kernel.org/index.php/GitSvnComparsion