Git
作为分布式版本控制系统,每个人都可以在本地的版本库中随意创建分支和标签。如果多人协作时,问题就出现了。
思考以下几个问题
如何避免因用户把所有的本地分支都推送到共享版本库,从而造成共享版本库分支的混乱?
如何避免不同用户针对不同特性开发创建了相同名字的分支而造成分支名称的冲突?
如何避免用户随意在共享版本库中创建标签而导致标签名称上的混乱和冲突?
有没有办法在执行 fetch 和 push 操作时,不用输入长长的版本
URL
?如果不带任何其他参数执行
git fetch
、git pull
和git push
,到底是和哪个远程版本库的哪个分支进行交互?
一、 远程分支介绍
之前介绍分支时,每个版本库只和一个远程共享版本库进行交互,实际上 Git
允许一个版本库和任意多的远程共享版本库进行交互。首先执行下面的命令,基于 hello-world.git
版本库再创建两个新的版本库。
现在共有三个共享版本库: hello-world.git
、hello-user3.git
和 hello-user4.git
,先看看 hello-world
远程版本库中包含的分支有哪些:
共有三个分支,现在重新克隆一下该版本库到 user5/workspace
目录:
执行 git branch
命令查看一下本地分支:
发现只有一个 master
分支 ,远程版本库的其他分支在哪里呢?执行 git show-ref
命令可以看到全部的本地引用:
输出显示:有些引用以 refs/remotes/origin
为前缀,并且名称和远程版本库的分支名一一对应,这些引用实际上就是从远程版本库的分支复制过来的,称为远程分支。
通过 git branch
加上 -r
参数,能够查看远程分支,如果是 -a
参数,则能够查看本地和远程所有分支:
实际上,在从远程版本库中执行获取操作时,不是把远程版本库的分支原封不动地复制到本地版本库的分支中,而是复制到另外的命名空间 .git/refs/remotes/origin/
下,这样从不同的远程版本库执行获取操作,因为命名空间的相互隔离,从而避免了分支在本地的相互覆盖。
至于为什么远程分支都有一个名为 origin/
的前缀,原因就在于配置文件:
说明 :
第 1 行表示以
origin
为名注册了一个远程版本库。第 2 行表示该远程版本库的地址。
第 3 行表示执行
git fetch orgin
操作时使用的默认引用表达式。该表达式以+
号开头,含义是强制进行引用的替换,即使将进行的替换是非快进式的。引用表达式中使用了通配符,冒号左侧表示匹配通配符规则的所有远程版本库分支,右侧表示复制到本地的远程分支(refs/remotes/origin/
)目录。执行
git fetch origin
命令时,相当于执行了:git fetch origin +refs/heads/*:refs/remotes/origin/*
,将远程版本库所有的分支复制到本地的远程分支中。
注意:远程分支不是真正意义上的分支,是类似于标签一样的引用。如果针对远程分支执行 checkout
命令,会看到大段的警告,告诉我们处于分离头状态。实际上,除了以 refs/heads
为前缀的引用之外,检出任何其他引用,都将使工作区处于分离头状态。如果要对远程分支进行修改,就需要创建新的本地分支。
二、分支追踪
如果想在分支 helper/v1.x
上进行工作,那么需要基于该远程分支创建本地分支,远程分支可简写为: origin/helper/v1.x
。在 Git
较高版本(高于 1.6.6)时,可直接使用下面命令同时完成本地分支的创建和切换:git checkout helper/v1.x
。在低版本中或注册了多个远程版本库(可能存在多个同名的远程分支),就不能使用上面简洁的分支创建和切换命令了,需要通过如下命令来创建:
输出显示,已经切换到对应分支了,并且本地分支和远程分支建立跟踪了。此时的本地分支有下列特征:
检查工作区状态时,会显示本地分支和关联的远程分支提交之间的关系。
当执行
git pull
时,如果两者出现版本偏离的话,会和关联的远程分支进行合并(或者变基)。当执行
git push
时,会推送到远程版本库的同名分支中。
下面进行操作演示:
- 先将本地
helper/v1.x
分支回退两个提交版本,再查看一下状态:
输出显示:本地分支落后于远程分支 origin/helper/v1.x
两个提交,并提示可以通过 git pull
来更新到最新的提交状态。
- 执行
git pull
命令,会自动与关联的远程分支进行合并,相当于找回了最新的两个提交:
三、基于本地分支创建另一个本地分支
基于本地分支创建另一个本地分支,是没有分支跟踪功能的,来演示一下:
- 从本地分支
helper/v1.x
创建新的本地分支helper/v1.y
:
只有一行输出,看不到分支间建立跟踪的提示。
- 将
helper/v1.y
分支回退两个版本,再查看一下状态:
- 执行
git pull
命令,会报错:
输出表示:当前分支并没有跟踪信息,需要指定要和哪个分支进行合并操作。可通过 git pull {remote} {branch}
的方式指定拉取远程的指定分支,或者通过 git branch --set-upstream-to=origin/{branch} helper/v1.y
命令进行远程分支的跟踪,再进行拉取操作。
- 再查看一下配置文件:
会发现 master
和 helper/v1.x
都有配置信息,但是 helper/v1.y
并没有相关的配置。
四、基于本地分支创建另一个本地分支(具备跟踪)
如果希望在基于一个本地分支创建另一个本地分支时,建立分支间的跟踪功能,需要加上 --track
参数,具体演示一下:
- 先检出
helper/v1.x
,再删除之前的helper/v1.y
分支。
- 通过
--track
参数重新创建分支。
- 查看一下配置文件,因为这里跟踪的是本地分支,所以
remote
中的远程版本库的名字为一个点。
五、注册新的版本库
名为 origin
的远程版本库是在克隆时就已经自动注册了,下面再注册一个新的远程版本库:
将
e/git_study/repos/hello-user3.git
以new-remote
为名进行注册:git remote add new-remote file:///e/git_study/repos/hello-user3.git
查看一下配置文件:
可以看到配置的分支信息 , remote
默认都是 origin
,所以不加参数,直接运行 git fetch
命令,并不会从新注册的 new-remote
远程版本库中获取,需要指定远程版本库。
- 执行
git remote -v
命令,可以查看已经注册的远程版本库:
- 从
new-remote
远程版本库进行获取,并查看一下远程分支:
可以看到在 new-remote
目录下的三个远程分支。
六、更改远程版本库的地址
如果远程版本库的地址改变了,怎么修改呢?第一种:直接打开 .git/config
文件修改。第二种:使用 git config
命令进行更改。第三种:用 git remote
命令:git remote set-url new-remote file:///e/git_study/repos/hello-user4.git
。
再观察一下远程版本库信息:
输出显示:每个远程版本库都有两个 URL
地址,分别是执行 git fetch
和 git push
命令时用到的,这就意味着两个地址是可以不一样的,通过加上 --push
参数可以单独设置推送地址,如下图:
当单独为远程推送设置 URL
后,配置文件的 remote
节点也会增加一条名为 pushurl
的配置,如下:
七、更改远程版本库的名称
将远程 new-remote
的名称修改为 user4
:
重命名之后,相应的远程分支的名称也会自动更改。
八、远程版本库更新
当注册了多个远程版本库并希望所有远程版本库一起更新时,可通过如下命令:git remote update 。
如果某个版本库不想通过 git remote update 自动更新,可以使用如下命令进行关闭:git config remote.user4.skipDefaultUpdate true
九、删除远程版本库
将 user4
远程版本库进行删除,命令为:git remote rm user4
。
十、push 和 pull 操作远程版本库
push
和 pull
操作的重点在于配置文件,配置文件指定了合并的远程版本库名称、远程推送和获取的地址、引用表达式、远程分支信息等等,根据如下的配置图,我们来分析一下执行过程:
不带参数执行 git push 命令的执行过程:
如果为当前分支设置了
{remote}
,则不带参数执行git push
相当于执行了git push {remote}
。如果没有为当前分支设置{remote}
,则相当于执行了git push origin
。要推送的远程版本库的
URL
由{remote}.pushurl
指定,如果没有配置,则使用{remote}.url
配置的URL
地址。如果为注册的远程版本库设置了
push
参数,即通过{remote}.push
配置了一个引用表达式,则使用该引用表达式执行推送。否则,使用 ":" 作为引用表达式。该表达式的含义是同名分支推送,即对所有在远程版本库中有同名分支的本地分支执行推送。
总结:在一个本地新建分支中执行 git push
推送操作,是不会推送也不会报错的,因为远程分支不存在同名分支,所以根本就没有对该分支执行推送。如果需要在远程版本库中创建分支,可以执行 git push {remote} {new_branch}
。通过将本地分支推送到远程版本库的方式在远程版本库中创建分支。但是,不能通过 git pull
(不带参数)将远程版本库中其他人推送的提交获取到本地,因为没有建立本地分支和远程分支的追踪。
不带参数执行 git pull
命令的执行过程:
跟
git push
一样,如果配置了{remote}
,则使用配置的{remote}
名称,否则使用origin
,即不带参数的git pull
相当于执行了git pull origin
。要获取的远程版本库的
URL
由地址{remote}.url
指定。如果为注册的远程版本库设置了
fetch
参数,即通过{remote}.fetch
配置了一个引用表达式,则使用该引用表达式执行获取操作。如果配置了
branch.merge
,则对其设定的分支执行合并操作,否则报错。
总结:在执行 git pull
操作时,可以通过使用参数 --rebase
设置使用变基而非合并操作。可通过使用: git config branch.{branchname}.rebase true
命令设置使用变基操作,而不是默认的合并操作。
十一、标签和远程版本库
远程版本库中的 tag
同步到本地版本库时,会使用相同的名称,不会像分支那样复制到另外的命令空间(远程分支)中,这可能会给本地版本库的标签带来混乱,特别是和多个远程版本库交互时。
之前讲过,执行 git fetch
命令时,如果有新建的 tag
,这些 tag
会被获取到本地版本库中。当删除远程版本库时,远程分支会被删除,但是已经引入到本地的 tag
并不会被删除(删除远程不影响本地),这也使本地版本库中的 tag
变得混乱。如果不想管理 tag
,可以进行如下操作:
在执行
git fetch
命令时,可以加上-n
或--no-tags
参数不获取tag
而只获取分支和提交。在注册远程版本库的时候,也可以使用
--no-tags
参数,避免将远程版本库的tag
引入到本地版本库。
实际上 Git
版本库本身也提供了一些安全机制避免对版本库的破坏:
- 用
reflog
对分支的操作历史进行记录。
默认创建的带工作区的版本库都会包含 corelogallrefupdates
为 true
的配置,这样在版本库中建立的每个分支都会创建对应的 reflog
。但是创建的裸版本库默认不包含这个设置,也就不会为每个分支设置 reflog
。如果团队的规模较小,可能因为分支误操作导致数据丢失,可以考虑为裸版本库添加corelogallrefupdates
的相关配置。
- 关闭非快进式推送。
如果将配置 receivedenyNonFastForwards
设置为 true
,则禁止一切非快进式推送。但这个配置有些矫枉过正,更好的方法是搭建基于 SSH
协议的 Git
服务器,通过钩子脚本更灵活地进行配置。允许来自某些用户的强制提交,而其他用户不能执行非快进式推送。
- 关闭分支删除功能。
如果将配置 receivedenyDeletes
设置为 true
,则禁止删除分支。同样更好的方法是通过架设基于 SSH
协议的 Git
服务器,配置分支删除的用户权限。