两个仓库之间的联系

在之前的笔记中讲过 Git 的分支操作,无论简单还是深奥,那些都是一个仓库内的事情。今天这篇笔记重点描述两个仓库之间的联系,涉及到的 git 命令有 pull、push、clone 等,难点围绕着两个概念: remote-tracking branch 和 tracing branch。

如果接触过 github,或者国内的 OSChina,那么必然用过 git clone 命令。我一般是这么用的:

  1. 先在 github 上创建一个仓库(有时候还会捎带着使用 README.md 文件);
  2. 然后使用 git clone 克隆到本地;
  3. 在本地进行文件添加、修改等操作后 commit;
  4. 在本地执行 push 推送到远程仓库(有时候还会将本地新增的 branch 也 push 上去)。

针对本地原本存在的仓库,如何上传到远程版本仓库?——这是我一直摸索的,至今也没有弄明白的事情。好在我确定等我整理完这篇笔记,我就会有答案了!结论见第 5 章。

要解开上述问题,我们需要先弄清楚两个问题:

  • 在 github(还有 OSChina)上创建仓库时做了哪些工作?
  • git clone 究竟做了哪些工作?

OSChina 新建项目

我们新建项目时对话框如下,我在图中共标注了 6 个序号:

OSChina new proj

  1. 默认为空,不选择任何语言;我一般都是选择 C++。
  2. 默认为空,不添加 .gitignore 文件;可选条目很丰富,后期只需要我们自己微调。
  3. 默认为空,不添加任何许可证;我貌似一直没用过。

这六个选项中只有第一个不影响仓库本身,也就是除此之外的五个选项,只要你选择了任意一个——或者你添加了 .gitignore 文件,或者你添加了许可证,或者你使用了 Readme 文件,或者你使用了模板——那么仓库都不再是一个空仓库,会默认生成 master 分支,并产生第一次提交!

如果你想创建一个空仓库,2、3 选项需要保持默认,4、5、6 选项去掉勾选复选框。

如果你创建了一个空仓库,就会看到“快速设置——xxxx”的页面,其中会提到“强烈建议所有的git仓库都有一个README, LICENSE, .gitignore文件”,并且会给出简易的命令行入门教程:

1
2
3
……
git remote add origin https://git.oschina.net/mazha/test.git
git push -u origin master

如果不是空仓库,那么你就会看到 master 分支,看到首次 commit 记录,看到你让 OSChina 自动生成的那些文件。

空仓库和非空仓库有什么影响呢?稍后就会看到,我们先认识一下入门教程中提到的两个命令。

git remote && git push

即便是在 OSChina 的 新手帮助手册 中,我们看到出现频率更多的是 git push,而非 git clone。看来 git clone 真不是一个好命令……

我们将 A 仓库的修改推送到 B 仓库,首先得建立两者之间的联系。通过在 A 仓库添加远程仓库实现:

运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库,同时指定一个你可以轻松引用的简写。

执行 git remote 可以查看已经配置的远程仓库服务器,也可以指定选项 -v,看到略微详细一些的内容。

如果想要查看某一个远程仓库的更多信息,可以使用 git remote show <remote-name> 命令

添加远程仓库之后,就可以从远程仓库拉取数据(这个稍后介绍),也可以推送数据到远程仓库。

执行 git push <remote-name> <branch-name>:<remote-branch-name> 会将本地 <branch-name> 分支的数据推送到 <remote-name> 服务器的 <remote-branch-name> 分支。

git push 的默认行为

我们一般不会使用上述命令的完整形式,在实际业务中多见到省略远程分支名、省略远程分支名 & 本地分支名、省略远程分支名 & 本地分支名 & 省略远程主机名的形式。我曾误以为:

  1. 省略远程分支名,默认与本地分支名相同;
  2. 在省略远程分支名的前提下,省略本地分支名,默认为当前分支;
  3. 在省略远程分支名、本地分支名的前提下,省略远程主机名:如果只存在唯一远程主机,则使用;否则报错;

反复阅读 git-push 的英文手册之后,发现事实要复杂得多,上述的理解根本就是错的。

git push [<repository> [<refspec>…​]]

When the command line does not specify what to push with <refspec>... arguments, the command finds the default <refspec> by consulting remote.*.push configuration, and if it is not found, honors push.default configuration to decide what to push.

When neither the command-line nor the configuration specify what to push, the default behavior is used, which corresponds to the simple value for push.default: the current branch is pushed to the corresponding upstream branch, but as a safety measure, the push is aborted if the upstream branch does not have the same name as the local one.

ps:Git 1.x 的默认策略(即 push.default 属性)是 matching;在Git 2.0 之后 simple 成为新的默认策略。simple 策略要求存在上游分支 && 本地分支和上游分支同名。引用来源

<refspec>…​ 引用来源

Specify what destination ref to update with what source object. The format of a <refspec> parameter is an optional plus +, followed by the source object <src>, followed by a colon :, followed by the destination ref <dst>.

疑问:上述文件中只提到了省略远程分支名 & 本地分支名时的行为,但只省略远程分支名时的行为呢?省略远程分支名 & 本地分支名 & 省略远程主机名的行为呢?

我只找到这样一句:

$ git push origin serverfix - This is a bit of a shortcut. Git automatically expands the serverfix branchname out to refs/heads/serverfix:refs/heads/serverfix, which means, “Take my serverfix local branch and push it to update the remote’s serverfix branch.” 引用来源

尽信书不如无书

关于 Git 远程操作的命令的具体用法,可以去官网查阅,如果觉得手册中的内容细致得太花费精力,可以学习阮一峰老师的 Git远程操作详解,其中对于 git clone、git remote、git fetch、git pull 和 git push 的介绍完全能够满足我们的日常使用。ps:有些地方,因为翻译不恰当(其实很不负责)的问题很是头疼,花费了一下午时间。

阮老师在介绍 git push 命令的时候提到:

如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。$ git push <remote-name>(我反复读了 git push 的英文手册,此“追踪关系”是从 remote.<repository>.push configuration variable 来的,不是下文中提到的 tracking)

如果当前分支只有一个追踪分支,那么主机名都可以省略。$ git push想来此处的“追踪分支”也是指 remote.<repository>.push configuration variable,或者是指只有一个远程仓库见鬼,这个“追踪分支”指的是什么?如果不加任何参数使用 $ git push 似乎只有下面一种情况:指定了上游分支)

如果当前分支与多个主机存在追踪关系(1),则可以使用 [-u | --set-upstream] 选项指定一个默认主机(2),这样后面就可以不加任何参数使用 git push(3)。(1. “多个”到底指的是什么?什么时候会存在多个?remote.<repository>.push configuration variable 2.“默认主机”的表述明面上似乎更容易理解,但本质上是错的:此选项就是指定 upstream branches,建立追踪关系 3. 事实:存在 remote.<repository>.push 时不会使用 push.default,所以也就不会解析上游分支……)

类似观点见博客评论 过客 说,上述引用文字本身一团糟,我备注的文字也是一团糟,官方手册中我也没找到任何依据。

查看配置

remote.<repository>.push configuration variable 可以通过 $ git config --add remote.origin.push fixbug 设置。

  • 查看仓库级的 config,命令:git config –local -l
  • 查看全局级的 config,命令:git config –global -l
  • 查看系统级的 config,命令:git config –system -l
  • 查看当前生效的配置, 命令:git config -l

命令参考来源 Git Config 命令查看配置文件

追踪(tracking)关系

我们先来区分三个概念:remote branches、 remote-tracking branches、 tracking branches

remote branches 远程分支:远程仓库中的分支

remote-tracking branches 远程跟踪分支:

Remote-tracking branches are references to the state of remote branches. They’re local references that you can’t move; they’re moved automatically for you whenever you do any network communication. Remote-tracking branches act as bookmarks to remind you where the branches in your remote repositories were the last time you connected to them. 引用来源

远程跟踪分支,但真实的意思应该是,远程分支在本地仓库的缓存,不执行git fetch命令,不会获取到远程分支的更新。千万不要将这些分支当做远程分支,以为是它们是自动更新的。引用来源

tracking branches 跟踪分支:

Checking out a local branch from a remote-tracking branch automatically creates what is called a “tracking branch” (and the branch it tracks is called an “upstream branch”). Tracking branches are local branches that have a direct relationship to a remote branch.

When you clone a repository, it generally automatically creates a master branch that tracks origin/master.

综上:

  1. remote-tracking branches 和 tracking branches 都是本地分支,可以认为 remote-tracking branches 是 remote branches 在本地的镜像,在这个“镜像”上不能进行修改。

    这个镜像 tracks 远程分支,所以称为 remote-tracking branches。

  2. 分支的概念本质上是指向某次 commit 的指针,remote-tracking branches 指针是死的,不能移动的;好在我们可以以它为基础建立 tracking branches,检出到工作区进行工作。

    tracking branches 追踪 remote-tracking branches。在这个子场景里,后者又称为前者的 upstream branch。

参考 Git Branching - Remote Branches

指定上游分支

remote-tracking branches 对 remote branches 的跟踪是固定的。和 git 操作有紧密联系的是另外一个 tracking!本节也只讨论这个 tracking。

通过上面几个概念,我们知道了 tracking 关系的存在,那么我们怎么查看分支之间的追踪关系呢?怎么建立分支之间的追踪关系呢?

查看 $ git branch -vv。ps 啰里啰嗦多说点:参数 -vv 用来查看追踪关系/上游分支的,如果只是单纯的用 $ git push <remote-name> <branch-name>[:<remote-branch-name>] 推送过,或者在设置了 remote.<repository>.push configuration variable 的基础上使用 $ git push <remote-name> [<branch-name>[:<remote-branch-name>]] 推送过,而未设置 upstream branch,这个命令是看不到多余信息的,毕竟它本意也就是用来查看 upstream branch 而已。

从上文中,我们知道将 remote-tracking branches 检出 git checkout -b <branch-name> <remote-tracking branches> 时会建立追踪关系,其他的情况呢?

将本地分支与远程某分支建立追踪关系分为几种情况:

  1. git branch (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]

    Set up <branchname>‘s tracking information so <upstream> is considered <branchname>‘s upstream branch. If no <branchname> is specified, then it defaults to the current branch.

    此命令用于给已存在的分支指定上游分支。一般使用 remote-tracking branches 作为 <upstream>,但也可以指定本地的其他分支作 [<branchname>]<upstream>——语法上允许这么操作,但这么配置没有意义,两个完全相同的本地分支。

    相关命令:git branch --unset-upstream [<branchname>]

  2. git branch [--set-upstream | --track] <branchname> [<start-point>]

    When creating a new branch, set up branch.<name>.remote and branch.<name>.merge configuration entries to mark the start-point branch as “upstream” from the new branch. 引用来源

    此命令为创建分支命令,在创建新分支的同时指定上游分支。如果未指定 <start-point> 值,默认为当前分支。

  3. git push [-u | --set-upstream] [<repository> [<refspec>…​]]

    For every branch that is up to date or successfully pushed, add upstream (tracking) reference,……引用来源

  4. git clone

我目前习惯使用的 git pull、git push 其实都是使用了很多默认参数的,而我却忽视了默认参数的存在。

结论:追踪关系的本质是 remote-tracking branches 和 local branches 的映射(前者成为后者的 upstream branch),建立映射后的 local branches 称为 tracking branches。

上游分支相关的两个变量:branch.<name>.remote 配置项branch.<name>.merge 配置项

本地仓库与远程仓库建立联系

远程仓库如果是空的,可以直接向其 push 任意分支;

如果不是空的,就不能(在已存在分支上)直接向其 push 内容,需要先 pull 合并后再进行 push 操作(pull 有限制条件:要么本地仓库为空,要么两者有共同祖先)。

OSChina 空仓库

  1. 本地无仓库,git init 建立仓库,一般来说也会进行 git commit 提交;
  2. 本地存在仓库,git remote add 添加远程仓库后,使用 git push -u <remote-name> <branch-name> 在远程仓库创建分支、建立追踪关系、并将本地分支数据推送到远程仓库;
  3. 拉取怎么做呢?

OSChina 存在初始提交

本地无仓库,使用 git clone 简单省事;本地存在仓库,一般来说远程仓库的初始提交默认都是 mater 分支,所以:

  • 如果本地分支不是和远程仓库 master 分支(或已存在的其他分支)建立追踪关系,直接使用 git push -u <remote-name> <branch-name> 即可;
  • 如果本地分支要与远程仓库已存在的分支建立追踪关系,首先使用 git branch –set-upstream 建立追踪关系,然后 git pull 拉取远程分支数据,再将本地更新推送上去;这是错误的,拉取时会直接报错 “fatal: refusing to merge unrelated histories”

强调:两个没有任何关系的分支(即,没有共同祖先)无论在理论还是实际操作上都是不可以进行合并的。所以,在远程仓库存在数据且保有本地仓库的前提下,在两者之间强加联系,只能在远程仓库另建分支(此分支和远程仓库原有数据无关),远程仓库会存在两个毫无关系的、相互独立的分支!还不如在最初直接使用空的远程仓库!

如果远程仓库存在工作区?

这一小节描述的是一个问题,此问题出现在向远程仓库推送的过程中。当远程仓库由 git init 创建(无 --bare 参数),远程仓库存在工作区,且 push 的目的分支恰好是远程仓库的检出分支时,就会报错!

我们使用 OSChina、github 等版本库服务时不会出现这个错误,我们自己搭建 git 服务器(使用 git init –bare)时一般也会不会出现这个问题。出现此问题最普遍的场景是,我们 clone 电脑上版本库作为 bak,在 bak 中修改后 push 回原仓库。

git clone 做了哪些工作?

我们看官方文档的描述:

  1. Clones a repository into a newly created directory,
  2. creates remote-tracking branches for each branch in the cloned repository (visible using git branch -r),
  3. and creates and checks out an initial branch that is forked from the cloned repository’s currently active branch.
  4. After the clone, a plain git fetch without arguments will update all the remote-tracking branches,
  5. and a git pull without arguments will in addition merge the remote master branch into the current master branch, if any (this is untrue when “–single-branch” is given; see below).
  6. This default configuration is achieved by creating references to the remote branch heads under refs/remotes/origin and by initializing remote.origin.url and remote.origin.fetch configuration variables.

参考链接:Git远程02:git clone都做了什么

如果有更多疑惑,请参考 Git 分支 - 远程分支,查看英文原著更容易理解 Git Branching - Remote Branches