之前提到的Git的所有操作都是在本地完成的,而实际项目开发并不是一个人就可以搞定的,通常需要团队的协作,而这些协作可能又不是在同一个地区的,这就涉及到Git的分布式特性了。
Git的分布式特定会涉及到克隆(clone)的概念,每个克隆都是版本库的副本,其包含所有的原始对象,同时每个克隆都是独立的,可以在本地独立地工作,这也是Git分布式协作的基础。
克隆只是共享代码的前提,还必须为这些版本库之间进行关联,为数据交换建立连接,Git是通过远程版本库完成版本库的连接的。
远程版本库(remote)是一个引用,通过文件系统或网络指向另一个版本库。一旦远程版本库建立,Git就可以使用pull操作或push操作来在版本库之间传输数据。通常做法为:
- 将原始版本库克隆到本地,以保持同步状态
- 或创建一个远程版本库从克隆版本库传输数据到原始版本库
- 或设置两个版本库进行双向信息交换
要跟踪其它版本库中的数据,Git使用远程追踪分支(remote-tracking branch)。版本库中的每个远程追踪分支都作为远程版本库中特定分支的一个代理,要集成本地修改和远程追踪分支对应的远程修改,可以建立一个本地追踪分支(local-tracking branch)来建立集成的基础。
最后,可以将版本库提供给他人,Git一般称之为发布版本库。
版本库概念
裸版本库和开发版本库
一个Git库要么是一个裸版本库(bare),要么是一个开发版本库(development)。
开发版本库用于常规的日常开发,其保持当前分支的概念,并在工作目录中提供检出当前分支的副本。
而裸版本库中没有工作目录,,并且不应该用于正常开发,裸版本库也没有检出分支的概念,裸版本库可以简单看作是.git目录的内容,也就是说,不应该在裸版本库中进行提交。
$ git init aaa
Initialized empty Git repository in C:/Users/wood/Desktop/GIT/tmp/aaa/.git/
$ cd aaa
$ ls -a
./ ../ .git/
$ cd .git/
$ ls -a
./ ../ HEAD config description hooks/ info/ objects/ refs/
$ git init --bare bbb
Initialized empty Git repository in C:/Users/wood/Desktop/GIT/tmp/bbb/
$ cd bbb
$ ls -a
./ ../ HEAD config description hooks/ info/ objects/ refs/
上面的git init使用--bare创建了裸版本库,可以看出--bare所带来的差异。
默认情况下,git开发版本库中可以使用引用日志,但在裸版本库中不行,同样,裸版本库中无法创建远程版本库。
而如果要创建一个版本库以供开发人员推送修改,那么其应该是裸版本库,实际上,发布的版本库应该是裸版本库。
克隆
git clone会创建一个新的版本库,同时Git并不需要复制原始版本库中的所有信息,相反,Git会忽略只跟原始版本库相关的信息,比如远程跟踪分支。
在正常使用git clone命令时,原始版本库中存储在refs/heads下的本地开发分支,会成为新的克隆版本库中refs/remotes中的远程追踪分支。原始版本库中refs/remotes下的远程追踪分支不会克隆,即克隆不需要关心原始版本库的克隆版本库。
同时克隆操作会将原始版本库中的标签复制到克隆版本库,但版本库特定的信息,如钩子,配置文件,引用文件和储藏都不会被克隆。
Git中的克隆操作可以通过git clone来克隆远程版本库。
默认情况下,每个新的克隆版本库都通过一个成为origin的远程版本库,建立一个链接以指回它的父版本库。但原始版本库并不知道任何克隆版本库,也不维护指向克隆版本库的链接,即是一个单向关系。
而同时origin这个名称也只是一个常用的叫法,可以在克隆操作过程中通过--origin选项指定替代名。
Git还使用默认的fetch refspec配置默认的origin远程版本库:
fetch = +refs/heads/*:refs/remotes/origin/*
而该refspec表示需要通过从原始版本库中抓取变更来持续更新本地版本库,此时远程版本库的分支在克隆版本库中是可用的,只要在分支名前加上origin/前缀即可。
远程版本库
在开发中使用的版本库称为本地或当前版本库,而交换文件用的版本库称为远程版本库。
Git使用远程版本库和远程追踪分支来引用另一个版本库,并有助于和该版本库建立连接。同时远程版本库还为版本库提供了可读性更好的名字,以代替实际的URL,而一个远程版本库还形成了该版本库远程追踪分支名称的基本部分。
通常远程版本库可以使用git remote命令来进行创建,删除,查看和操作。同时引入的所有远程版本库都记录在.git/config文件中,可以使用git config来操作。
和远程版本库相关的常用Git命令有:
git clone // 克隆原始版本库
git fetch // 从远程版本库抓取对象及其相关的元数据
git pull // 跟fetch类似,但会合并修改到相应的本地版本
git push // 传输对象及其相关的元数据到远程版本库
git ls-remote // 显示给定远程版本库的引用列表
追踪分支
一旦克隆了版本库,就可以和原始版本库保持同步。
而为了区分,需要对不同功能的分支进行区分:
- 远程追踪分支:与远程版本库相关联,专门用来追踪远程版本库中每个分支的变化
- 本地追踪分支:与远程追踪分支相配对,是集成分支,用于收集本地分支和远程追踪分支中的变更
- 任何本地的非追踪分支:通常称为特定或开发分支
- 远程分支:是非本地的远程版本库的分支
由于远程追踪分支都集中在自己的命名空间中,因此开发分支和远程追踪分支是很容易进行区分。而远程追踪分支专门用于追踪另一个版本库中的变化,因此应视为其为只读的,不应合并或提交到一个远程追踪分支。
引用其它版本库
为了协调原始版本库和本地的版本库,可以定义一个远程版本库,这里是指存在版本库配置文件中的一个实体名。其由两个不同的部分组成,第一部分以URL的形式指出其它版本库的名称,第二部分称为refspec,该refspec指定一个引用(通常表示一个分支)如何从一个版本库的命名空间映射到其它版本库的命名空间。
引用远程版本库
Git支持多种形式的URL,这些URL可以用来命名远程版本库,这些形式指定访问协议和数据的位置和地址。
Git URL最简单的形式指代本地文件系统上的版本库,其可以是一个真正的物理文件系统,也可以是通过网络文件系统挂载到本地的虚拟文件系统。
/path/to/repo.git
file://path/to/repo.git
上面两种形式虽然相似。前者使用文件系统中的硬链接来直接共享版本库和远程版本库之间相同的对象,后者则复制对象,而不是直接共享它们。为了避免与共享版本库相关的问题,建议使用后一种形式。
其它形式的Git URL指代远程系统上的版本库。
当存在必须要通过网络获取的真正的远程版本库时,数据传输的最有效形式通常称为Git原生协议,它指的是Git内部用来传输数据的自定义协议。Git原生协议的URL示例包括:
git://example.com/path/to/repo.git
git://example.com/~user/path/to/repo.git
git-daemon用这些形式来发布匿名读取的版本库,其他用户可以使用这些URL形式来克隆和抓取。
使用这些形式的客户端不需要经过身份验证,不要求输入密码,因此~user格式可以用来指代用户的主命令,仅有一个没扩展的裸~是没用的,没有经过身份验证的用户不可以使用主目录。此外,只有当服务器端用--user-path选项允许时~user格式才有效。
对于经过身份验证的安全连接,Git原生协议可以通过SSH连接使用如下URL模板进行隧道封装:
ssh://[user@]example.com[:port]/path/to/repo.git
ssh://[user@]example.com/path/to/repo.git
ssh://[user@]example.com/~user2/path/to/repo.git
ssh://[user@]example.com/~/path/to/repo.git
第三种形式允许存在两个不同的用户名,第一个是验证会话的用户,第二个是访问主目录的用户。
Git还支持与scp语法类似的URL形式,这和SSH形式相同,但无法指定端口参数。
[user@]example.com:/path/to/repo.git
[user@]example.com:~user/path/to/repo.git
[user@]example.com:path/to/repo.git
同时Git还支持HTTP,HTTPS,Rsync等协议来引用远程版本库。
refspec
refspec把远程版本库中的分支名映射到本地版本库中的分支名。
因为refspec必须同时从本地版本库和远程版本库指定分支,所以完整的分支名在refspec是必需的。在refspec中,通常会看到开发分支名有refs/heads/前缀,远程追踪分支名有refs/remotes/前缀。
refspec语法:
[+]source:destination
上述形式主要由源引用(source ref),冒号和目标引用(destination ref)组成。完整的格式还可以在前边加上一个可选的加号。如果由加号则表示不会在传输过程中进行正常的快进安全检查,此外,星号*允许用有限形式的通配符匹配分支名。
在某些应用中,源引用是可选的,在另一些应用中,冒号和目标引用是可选的。
refspec在git fetch和git push中都使用,其数据流向为:
操作 | 源 | 目标 |
push | 推送的本地引用 | 更新的远程引用 |
fetch | 抓取的远程引用 | 更新的本地引用 |
典型的git fetch命令会使用refspec,比如:
+refs/heads/*:refs/remotes/remote/*
上面的形式可以理解为,在命名空间refs/heads/中来自远程版本库的所有源分支映射到本地版本库,使用由远程版本库名来构造命名,并放在refs/remotes/remote命名空间中。同时上面的星号*适用于远程版本库中的refs/heads/中的多个分支,同时也是因为该规范导致远程版本库的特性分支作为远程追踪分支,被映射到本地版本库的命名空间,并将它们分成基于远程版本库名的子名。
也就是说,通常会将远程版本库分支放在refs/remotes/remote/*下。
Git命令git show-ref会列出当前版本库中的引用,git ls-remote会列出远程版本库的引用。
在git push操作中,用户通常要提供并发布在本地特性分支上的变更,在用户上传变更后,为了让他人在远程版本库中找到该用户的变更,用户所做的更改必须出现在该版本库的特性分支中。因此,在典型的git push命令中,会把用户的版本库中的源分支发送到远程版本库,方法是使用如下的refspec:
+refs/heads/*:refs/heads/*
此处的refspec可以解释为,从本地版本库中,将源命名空间refs/heads/下发现的所有分支名,放在远程版本库的目标命名空间refs/heads/下的匹配分支中,使用相似的名字来命名。
第一个refs/heads/指的是用户的本地版本库,而第二个指远程版本库,星号确保所有分支都复制。
多个refspec可在git fetch和git push的命令行中给出,在远程版本库的定义中,可以指定多个抓取refspec,多个推送refspec或者它们的组合。
而如果git push命令中没有明确指定的远程版本库,Git会假设使用origin,如果没有refspec,git push会将提交发送到远程版本库中与上游版本库共有的所有分支。不在上游版本库中的任何本地分支都不会发送到上游,分支必须已经存在,并且名字匹配。因此,新分支必须显式地用分支名来推送。之后,可以在默认情况下用简单地git push,因此,默认的refspec使用以下两条等价的命令:
git push origin branch
git push origin branch:refs/heads/branch
使用远程版本库的示例
本机说明如何将一个初始版本库放在仓库中,把仓库中的开发版本库克隆出去,在其中进行开发,然后在仓库中将之同步。
创建权威版本库
权威版本库可以存在文件系统的任何地方,这里将之放在depot/这个目录中,而开发工作不应和该版本库处于同一目录,相反,用户的工作应在本地克隆中完成。
实际上,权威版本库或者说初始版本库可以位于任何地方,或者托管于任何服务器上。这里的初始版本库和本地版本库均处在同一主机中。通常会将该权威版本库放在特殊的目录中,该命令称为仓库(depot)。
首先用一个初始版本库填充depot/,这里使用和depot/处于同一目录下的rem/中的版本库作为复制对象,将之放在depot/rem.git下:
$ cd depot
$ ls -a
./ ../
$ git clone --bare ../rem/ rem.git
Cloning into bare repository 'rem.git'...
done.
$ cd ../rem
$ ls -a
./ ../ .git/ file
$ cd .git
$ ls -a
./ COMMIT_EDITMSG config hooks/ info/ objects/
../ HEAD description index logs/ refs/
$ cd ../../depot/rem.git/
$ ls
HEAD config description hooks/ info/ objects/ packed-refs refs/
git clone命令会将rem中的Git远程版本库复制到当前工作目录depot/下,最后一个参数rem.git为版本库重命名为rem.git。而裸版本库一般都有一个.git后缀。
这里的rem.git就是权威版本,因为在克隆操作过程中使用了--bare选项,所以Git没有引入一般默认的origin远程版本库。
制作origin远程版本库
现在存在两个基本相同的版本库,区别在于初始版本库中存在工作目录,而克隆的裸版本库没有。
同时,因为初始版本库是使用git init创建的,因此它没有origin。
如果目标是在初始版本库中进行更多开发,然后把该开发推送到仓库中新建的权威版本库中,则需要添加远程版本库。
从仓库克隆的版本库会自动创建一个origin远程版本库,比如这里再新建一个tmp目录,然后从仓库中克隆版本库出来,则可以看到自动创建了origin远程版本库。
$ mkdir tmp
$ cd tmp
$ git clone ../depot/rem.git/
Cloning into 'rem'...
done.
$ ls -a
./ ../ rem/
$ cd rem
$ ls -a
./ ../ .git/ file
$ cd .git
$ ls -a
./ HEAD description index logs/ packed-refs
../ config hooks/ info/ objects/ refs/
$ cd refs/remotes/
$ ls -a
./ ../ origin/
同时操纵远程版本库的命令是git remote。该操作在.git/config文件中引入了一些新设置:
cd rem
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
$ git remote add origin ~/Desktop/GIT/depot/rem.git
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = C:/Users/wood/Desktop/GIT/depot/rem.git
fetch = +refs/heads/*:refs/remotes/origin/*
上面的代码中,git remote在配置中增加了一个新的remote为origin。
remote在当前版本库和远程版本库之间建立连接,此时,URL值为url = C:/Users/wood/Desktop/GIT/depot/rem.git。现在该版本库中,origin可以作为仓库中远程版本库中的简写,同时上边的代码还增加了默认的fetch refspec。
包含远程版本库引用的版本库(引用者)和远程版本库(被引用者)之间的关系是不对称的。remote始终从引用者单向指向被引用者。被引用者不知道有其它版本库指向它。
然后在原始版本库中建立新的远程追踪分支,代表来自远程版本库的分支,以完成建立origin远程版本库的进程。
首先,只能看到一个分支,即master分支:
$ git branch -a
* master
$ git remote update
Fetching origin
From C:/Users/wood/Desktop/GIT/depot/rem
* [new branch] master -> origin/master
$ git branch -a
* master
remotes/origin/master
Git在版本库中引入了一个新的分支origin/master,这是origin远程版本库中的远程追踪分支,没有人在这个分支上进行开发。该分支的作用是跟踪origin远程版本库的master分支中的提交。
而由git remote redate产生的更新操作并不意味着远程版本库更新了,相反其表示本地版本库中的origin已被基于远程版本库的信息更新了。
同时普通的git remote update会导致在该版本库中的每个remote都被更新,会从每个remote指定的版本库中检查并抓取新提交。此时可以限定只从一个remote获取更新,只要给git remote update命令指定remote名:
git remote update remote_name
而最初添加远程版本库时,使用 -f 选项将导致立即对该远程版本库指定fetch:
git remote add -f origin repository
经过上述操作,本地的版本库就连接到了仓库中的远程版本库了。
在版本库中进行开发
$ git show-branch -a
* [master] first file
! [origin/master] first file
--
*+ [master] first file
$ cat abcde > file1
cat: abcde: No such file or directory
$ git add file1
$ git commit -m "commit file1"
[master cecdadd] commit file1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
$ git show-branch -a
* [master] commit file1
! [origin/master] first file
--
* [master] commit file1
*+ [origin/master] first file
在添加file1之前,本地master和origin/master是同步的,之后master存在新的提交,而origin/master仍在追踪远程版本库。
推送变更
从上面来看,本地的修改提交只存在于本地master,并不存在远程版本库中。若要把master分支推动到origin远程版本库可以使用git push命令。
$ git push origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 254 bytes | 254.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To C:/Users/wood/Desktop/GIT/depot/rem
522e1b9..cecdadd master -> master
此时,Git已经提取了master分支的变更,并将之捆绑发送到了名为origin的远程版本库中。并且Git取出了那些相同的变更,也将它们添加到本地版本库中的origin/master分支中。实际上,Git使原本在本地版本库中的master分支的变更发送到远程版本库,然后再请求将之放回到origin/master的远程追踪分支。
$ git show-branch -a
* [master] commit file1
! [origin/master] commit file1
--
*+ [master] commit file1
现在,两个分支都同步了。
同时也可以在远程版本库中查询:
$ cd ..
$ cd depot/rem.git/
$ git show-branch
[master] commit file1
或:
$ git ls-remote origin
cecdadd7cb288b781aa6e9920a90010d18af4b4a HEAD
cecdadd7cb288b781aa6e9920a90010d18af4b4a refs/heads/master
$ git show cecdad
commit cecdadd7cb288b781aa6e9920a90010d18af4b4a (HEAD -> master, origin/master)
Author: wood_glb <wood_glb@git.com>
Date: Sun Aug 14 10:42:01 2022 +0800
commit file1
diff --git a/file1 b/file1
new file mode 100644
index 0000000..e69de29
添加新开发人员
随着项目的推进,可能需要添加新的开发人员进行协同开发,这里引入tom:
$ mkdir tom
$ cd tom
$ git clone ../depot/rem.git/
Cloning into 'rem'...
done.
$ ls
rem/
$ cd rem/
$ ls
file file1
$ git branch
* master
$ git log -1
commit cecdadd7cb288b781aa6e9920a90010d18af4b4a (HEAD -> master, origin/master, origin/HEAD)
Author: wood_glb <wood_glb@git.com>
Date: Sun Aug 14 10:42:01 2022 +0800
commit file1
这里可以看出clone操作用版本控制下的所有文件填充了工作目录,即tom的克隆是一个开发版本库,而不是一个裸版本库。
而在tom的版本库中可以看到最新的提交信息,即是从父版本库克隆来的,因此它有一个默认的远程版本库origin。
$ git remote show origin
* remote origin
Fetch URL: C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
Push URL: C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
HEAD branch: master
Remote branch:
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
$ git show-branch -a
* [master] commit file1
! [origin/HEAD] commit file1
! [origin/master] commit file1
---
*++ [master] commit file1
其中master分支是tom的主要分支,这是常见的本地特性分支,也是与名为master的远程追踪分支相对应的本地追踪分支。
origin/master分支是一个远程追踪分支,追踪origin版本库中master分支的提交。
origin/HEAD应用通过符号名指出哪个分支是远程版本库认为的活动分支。
同样,tom也会进行本地开发:
$ echo abcdef > file2
$ git add file2
warning: LF will be replaced by CRLF in file2.
The file will have its original line endings in your working directory
$ git commit -m "commit file2"
[master 09c85b0] commit file2
1 file changed, 1 insertion(+)
create mode 100644 file2
$ git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 288 bytes | 288.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
cecdadd..09c85b0 master -> master
获取版本库更新
上面tom提交了自己的修改,此时远程版本库的状态为:
$ git show-branch -a
[master] commit file2
转回到本地版本库,并在本地版本库中更新tom的修改,并提交本地的修改:
$ git show-branch -a
* [master] commit file1
! [origin/master] commit file1
--
*+ [master] commit file1
$ git remote update
Fetching origin
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 268 bytes | 20.00 KiB/s, done.
From C:/Users/wood/Desktop/GIT/depot/rem
b39d3b7..a297df7 master -> origin/master
$ git show-branch -a
* [master] commit file1
! [origin/master] commit file2
--
+ [origin/master] commit file2
*+ [master] commit file1
$ git merge master origin/master
Updating b39d3b7..a297df7
Fast-forward
file2 | 1 +
1 file changed, 1 insertion(+)
create mode 100644 file2
$ git show-branch -a
* [master] commit file2
! [origin/master] commit file2
--
*+ [master] commit file2
$ echo abcdefg > file3
$ git add file3
warning: LF will be replaced by CRLF in file3.
The file will have its original line endings in your working directory
$ git commit -m "commit file3"
[master 6e5f98e] commit file3
1 file changed, 1 insertion(+)
create mode 100644 file3
$ git push
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin master
$ git push origin master
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 263 bytes | 263.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
To C:/Users/wood/Desktop/GIT/depot/rem.git
a297df7..6e5f98e master -> master
$ git show-branch -a
* [master] commit file3
! [origin/master] commit file3
--
*+ [master] commit file3
此时远程版本库的状态会发生改变:
$ git show-branch -a
[master] commit file3
而若tom想要更新其版本库,则需要使用git pull:
$ git show-branch -a
* [master] commit file2
! [origin/HEAD] commit file2
! [origin/master] commit file2
---
*++ [master] commit file2
$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 243 bytes | 20.00 KiB/s, done.
From C:/Users/wood/Desktop/GIT/tom/../depot/rem
a297df7..6e5f98e master -> origin/master
Updating a297df7..6e5f98e
Fast-forward
file3 | 1 +
1 file changed, 1 insertion(+)
create mode 100644 file3
$ git show-branch -a
* [master] commit file3
! [origin/HEAD] commit file3
! [origin/master] commit file3
---
*++ [master] commit file3
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
完整的git pull命令允许指定版本库和多个refspec:git pull repository refspecs。
如果不在命令行上指定版本库,则使用默认的origin远程版本库。如果没有在命令行上指定refspec,则使用远程版本库的抓取refspec。如果指定版本库,但没有指定refspec,Git会抓取远程版本库的HEAD引用。
git pull操作有两个操作:
- git fetch
- git merge或git rebase
git fetch
在开始的抓取步骤中,Git先定位远程版本库。因为在命令行中没有直接指定一个版本库的URL或远程版本库名,所以就假定默认的远程库名为origin:
[remote "origin"]
url = C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
fetch = +refs/heads/*:refs/remotes/origin/*
Git使用上面的URL作为源版本库,此外由于没有在命令行中指定refspec,Git会使用remote条目中所有fetch=的行。因此将抓取远程版本库中的每个refs/heads/*分支。
之后Git对源版本库进行协商,以确定哪些新提交是在远程版本库而不是在本地版本库中的,然后获取所有的refs/heads/*引用作为拉取符号引用给出的内容。
拉取操作输出中以remote:为前缀的内容表示协商,压缩和传输协议,以表示新提交正在传输到本地版本库中:
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Git把新提交放在本地版本库上合适的远程追踪分支上,然后显示其映射关系:
From C:/Users/wood/Desktop/GIT/tom/../depot/rem
a297df7..6e5f98e master -> origin/master
即Git查看了远程版本库depot/rem,取得了其master分支,然后将之取回到本地版本库的origin/master分支。
git merge/git rebase
这里,Git使用一种特殊类型的合并操作快进(fast-forward),合并远程追踪分支origin/master的内容到本地追踪分支master分支。
但是Git又是如何知道合并哪些特定分支呢?这个可以可以查看配置配置文件:
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = C:/Users/wood/Desktop/GIT/tom/../depot/rem.git/
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
上面的内容说明,当master分支是当前检出的分支时,使用origin作为fetch或pull操作过程中获取更新的默认远程版本库。此外在git pull的merge过程中,用远程版本库中的refs/heads/master作为默认分支合并到master分支。
配置文件中branch部分的merge字段值refs/heads/master被视为refspec的远程部分,其必须与从git pull命令过程中取出的源引用相匹配。
因为merge的配置值仅在执行git pull时使用,所以手动执行git merge时必须在命令行中指定合并的源分支,该分支可能是一个远程追踪分支:
git merge origin/master
而如果使用变基而不是合并,Git会将你的本地追踪特性分支上的变更向前移植到对应的远程追踪分支新抓取的HEAD。
命令git pull --rebase会使Git只在这次pull过程中变基(而不是合并)本地追踪分支到远程追踪分支。要将变基设置为分支的正常操作,需要把rebase变量设置为true:
[branch "master"]
remote = origin
merge = refs/heads/master
rebase = true
git merge vs git rebase
合并操作可能产生额外的合并提交来记录更新同时存在于每一个分支的变更,每个分支上的每个提交序列都基于原来的提交。当推送到上游时,任何合并提交都将继续存在。
变基操作从根本上改变了一系列提交是在何时何地开发的概念,开发历史记录的某些方面会丢失。
实际开发时,可以根据个人开发习惯自由决定。
远程版本库配置
手动追踪远程版本库的所有信息是十分烦琐和麻烦的,Git为建立和维护远程版本库信息提供了三种机制:
- git remote
- git config
- .git/config
git remote
该命令是专门的接口,特别适用于远程版本库,用于操作配置文件数据和远程版本库引用,其语法形式为:
git remote [-v | --verbose]
git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <url>
git remote rename <old> <new>
git remote remove <name>
git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
git remote set-branches [--add] <name> <branch>…
git remote get-url [--push] [--all] <name>
git remote set-url [--push] <name> <newurl> [<oldurl>]
git remote set-url --add [--push] <name> <newurl>
git remote set-url --delete [--push] <name> <url>
git remote [-v | --verbose] show [-n] <name>…
git remote prune [-n | --dry-run] <name>…
git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)…]
git remote add origin命令添加一个名为origin的新远程版本库到仓库中新创建的父版本库,然后运行git remote show origin命令提取关于origin远程版本库的所有信息,最后使用git remote update命令抓取远程版本库中的所有可用更新到本地版本库中。
git remote rm命令会从本地版本库中删除给定的远程版本库及其关联的远程追踪分支。而要只从本地版本库删除一个远程追踪分支,可使用:
git remote -r -d origin/dev
而远程版本库中可能已经有分支被其它开发人员删除了,git remote prune命令可以用来删除本地版本库中相对于实际远程版本库较旧的远程追踪分支。
而要重命名一个远程版本库及其所有引用,可以使用:
git remote rename old_name new_name
而更新或更改远程版本库的URL,可以使用:
git remote set-url origin new_url
git config
该命令可以直接操纵配置文件中的条目。
添加一个名为publish的新远程版本库,并带有想发布的所有分支的push refspec,可以使用:
git config remote.publish.url 'new_url'
git config remote.publish.push 'src_refspec:dest_refspec'
上面的命令如果publish部分不存在,则第一条命令就会在该文件中创建。
.git/config
这部分就简单多了,可以直接手动配置该文件。
使用追踪分支
创建追踪分支
本地的master分支可以认为是origin/master分支引进的开发的扩展,用同样的方式,用户可以在任何远程追踪分支的基础上创建新分支,并用其来扩展该开发线。
之前提到,在克隆操作或把远程版本库添加到版本库中时会引入远程追踪分支。而使用远程追踪分支名的检出请求会导致创建一个新的本地追踪分支,将与该远程追踪分支相关联,但要满足本地分支名只与所有远程版本库中的一个远程分支匹配。
比如这里尝试创建一个版本库:
$ git clone https://github.com/gitster/git.git
Cloning into 'git'...
remote: Enumerating objects: 325661, done.
remote: Counting objects: 100% (9/9), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 325661 (delta 3), reused 2 (delta 0), pack-reused 325652
Receiving objects: 100% (325661/325661), 191.82 MiB | 10.25 MiB/s, done.
Resolving deltas: 100% (244572/244572), done.
Updating files: 100% (4182/4182), done.
$ git remote add korg git://git.kernel.org/pub/scm/git/git.git
$ git remote update
Fetching origin
Fetching korg
remote: Enumerating objects: 6924, done.
remote: Total 6924 (delta 0), reused 0 (delta 0), pack-reused 6924
Receiving objects: 100% (6924/6924), 10.35 MiB | 975.00 KiB/s, done.
Resolving deltas: 100% (3254/3254), done.
From git://git.kernel.org/pub/scm/git/git
* [new branch] main -> korg/main
* [new branch] maint -> korg/maint
* [new branch] master -> korg/master
* [new branch] next -> korg/next
* [new branch] seen -> korg/seen
* [new branch] todo -> korg/todo
检出一个独特的分支:
$ git branch -a | grep tmp-objdir
remotes/origin/ns/tmp-objdir
$ git branch
* master
$ git checkout ns/tmp-objdir
Updating files: 100% (1631/1631), done.
Switched to a new branch 'ns/tmp-objdir'
Branch 'ns/tmp-objdir' set up to track remote branch 'ns/tmp-objdir' from 'origin'.
$ git branch
master
* ns/tmp-objdir
之后,Git会自动添加一个branch条目到.git/config中,指出该远程分支应该合并到新的本地追踪分支中:
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = https://github.com/gitster/git.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[remote "korg"]
url = git://git.kernel.org/pub/scm/git/git.git
fetch = +refs/heads/*:refs/remotes/korg/*
[branch "ns/tmp-objdir"]
remote = origin
merge = refs/heads/ns/tmp-objdir
当创建本地追踪分支时,如果不想检出,可以使用下述命令创建本地追踪分支,并在.git/config文件中记录本地和远程分支的关联。
git branch --track local_branch remote_branch
而如果已经存在以特性分支,用户决定其应该与上游版本库的远程追踪分支相关联,用户可以通过使用--upstream选项来建立该关系。通常情况下,在添加新的远程版本库后会这么做:
git remote add branch_name url
git branch --set-upstream mydev_name branch_name/dev_name
领先和落后
随着本地和远程追踪分支对的创建,可以对两个分支之间进行相对比较。除了正常的diff,log和其它基于内容的比较外,Git提供了每个分支提交数目的快速摘要和判断一个分支比另一个分支领先还是落后的方法。
如果在本地追踪分支上引入新提交,就认为其领先相应的远程追踪分支。而如果在远程追踪分支上获取新提交,并且其不存在于本地追踪分支中,Git就会认为本地追踪分支落后于对应的远程追踪分支。
$ echo abcd > file
$ git add file
warning: LF will be replaced by CRLF in file.
The file will have its original line endings in your working directory
$ git commit -m "commit file"
[ns/tmp-objdir bb986ece11] commit file
1 file changed, 1 insertion(+)
create mode 100644 file
$ git fetch
$ git status
On branch ns/tmp-objdir
Your branch is ahead of 'origin/ns/tmp-objdir' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
可以看出,此时本地追踪分支是领先于远程追踪分支的。
而要查看哪些提交在本地而不在远程分支上,可以使用:
git log origin/master..master
添加和删除远程分支
在本地克隆分支上创建的任何新开发,在父版本库中都是不可见的,除非直接请求将之传过去。同样在本地版本库中删除一个分支仍然是一个本地变化,其不会从夫版本库中删除,除非请求从远程版本库中删除。
要在远程版本库中执行类似的分支添加和删除操作,需要在git push命令中指定不同形式的refspec,即推送使用只有源引用的refspec(即没有目标引用)在远程版本库中创建新分支:
git push origin :branch_name
推送使用只有目标引用的refspec(即没有源引用)导致目标引用从远程版本库中删除,为了指明该引用是目标,冒号分隔符必须显式指定,同时上边的语法还有等价的形式:
git push origin --delete branch_name
而重命名远程分支则可以使用新名称创建新上游分支,然后删除旧分支:
git branch new_name origin/old_name
git push origin new_name
git push origin :old_name
裸版本库和git推送
上面的例子中,我们同时使用了裸版本库和本地版本库(开发),而在Git中,所有版本库的地位是平等的,即可以对开发于裸版本库进行推送和抓取,因为两者在实现上没有什么区别。这种对称涉及对于Git是非常重要的。
git push命令在接收版本库中不检出文件,只是简单地将对象从源版本库推送到接收版本库,并在接收端更新相应的引用。如果开发人员在本地版本库(开发)上工作,但此时存在异步推送,那么此时对于在本地版本库上开发的人员来说将会出现不必要的麻烦。因此上述的对称设计可以避免这种麻烦,即推送到裸版本库。