Git详解(二) 远程操作

127 阅读25分钟

​本文已参与「新人创作礼」活动,一起开启掘金创作之路。

上一篇:Git 详解(一) 本地操作

一次远程仓库推送

远程仓库使用 github 提供的仓库。注册 github 账号,创建远程仓库 testgit :

这一步操作相当于在本地创建 testgit 文件夹,然后在文件夹中执行 git init 指令生成 Git 仓库。

将 github 上的远程仓库添加到本地,并为该远程仓库设置别名(别名通常使用 origin ):

git remote add origin github.com/BWHN/testgi…

此时 github 上的远程仓库是一个空的仓库,将本地仓库的 master 分支推送到远程仓库。不过在此之前,先在本地的 master 分支上创建 README.md 文件(该文件用于项目描述):

提交记录后,将本地仓库的 master 分支推送到远程仓库的 master 分支,并关联本地仓库的 master 分支和远程仓库的 master 分支:

git push -u origin master

此时 Git 会提示你需要输入 github 的账户和密码,填写完成后即可成功推送:

在 github 上查看推送结果:

使用 HTTPS 方式推送,每次都需要输入 github 的账户和密码,十分繁琐。下面介绍使用 SSH 方式进行推送。

生成 ssh 公钥和私钥:

ssh-keygen

生成的公私钥默认存放在在 /user/.ssh 目录下。最好给公私钥取一个易记的名字,要不然公私钥一旦多起来就不容易找到了,我将远程仓库的名字用作公私钥名:

复制公钥 testgit.pub 中的内容,用其为 github 远程仓库创建公钥:

然后我们还需要将 origin 记录的远程仓库 HTTPS 地址替换为 SSH 地址:

git remote set-url origin

之后进行推送操作时就不需要再输入 github 的账号和密码了:

remote

远程仓库指的是不在你计算机上的 Git 仓库,也就是你无法直接操作到的 Git 仓库。

在一次远程仓库推送小节中,提到了两个 remote 相关的指令。首先是添加远程仓库,添加远程仓库需要为远程仓库地址设置一个别名,因为我们不可能每次操作远程仓库时都输入它的 url,那太难记住了。该指令格式如下:

git remote add

其次是修改别名记录的远程仓库地址:

git remote set-url

当然除了修改别名记录的远程仓库地址,你也可以只远程仓库的别名而不修改远程仓库的地址:

git remote rename

执行下面的指令就可以查看所有的远程仓库:

git remote

加上 -v 参数还可以显示远程仓库的地址:

git remote -v

关于上图中为什么同一个远程仓库的别名和地址会出现两次,看完后续小节后你的疑问将会得到解答。

查看远程仓库详细信息:

git remote show

执行该指令输出的信息十分有用:

从上图中可以看到远程仓库的地址、远程仓库的分支、执行 git pull 指令(见 pull 小节)哪个本地分支会和哪个远程分支会合并、执行 git push 指令(见 push 小节)哪个本地分支会被推送到哪个远程分支。

删除本地的远程仓库(并不会删除 github 上的远程仓库,见 fetch 小节):

git remote rm

branch(remote)

上一篇文章介绍了本地分支相关的操作,这里补充远程分支相关的操作。

执行 git branch 指令可以查看所有的本地分支,加上 -r 参数(remote)可以列出所有本地的远程分支(见 fetch 小节):

git branch -r

如果想要同时查看所有本地分支和本地的远程分支,可以使用 -a 参数:

git branch -a

执行 git branch -v 指令可以查看所有的本地分支以及每个分支的最新提交记录,而 -vv 参数不仅可以显示使用 -v 参数看到所有内容,还可以看到本地分支关联的远程分支状况:

git branch -vv

从上图中可以看到,本地 master 分支关联远程仓库的 master 分支,并且领先远程分支一次提交。而本地 newb 分支没有关联远程分支。

分支关联

上面两次提到“本地分支关联远程分支”,关联指的是什么呢?

git push 指令为例,执行该指令时,为什么 Git 会知道将本地 master 分支推送到远程 master 分支,而不是将本地 master 分支推送到远程其他分支呢?这正是因为本地 master 分支关联了远程 master 分支。因此将指定本地分支和指定远程分支关联后,执行 git push 指令 Git 就可以把指定本地分支推送到相应的远程分支上。

在一次远程仓库推送小节中,执行了 git push -u origin master 指令,该指令的作用之一就是将本地 master 分支关联远程 master 分支(该指令的另一个作用是将本地 master 分支推动到远程 master 分支)。

除上述方式外,还有两种方式可以关联本地分支和远程分支。

1、如果只需要关联本地分支和远程分支,不需要进行分支推送,那么可以使用下面的指令:

git branch --set-upstream-to=<shortname/branch>

若省略本地分支,那么该指令默认将当前所在分支关联到指定的远程分支。--set-upstream-to 参数比较长,可以使用 -u 来代替该参数:

git branch -u <shortname/branch>

将本地 newb 分支也关联远程 master 分支:

很明显本地 newb 分支不应该关联远程 master 分支,此时我们可以通过 --unset-upstream 参数取消本地分支和远程分支的关联:

git branch --unset-upstream

若省略本地分支,默认取消当前分支与远程分支的关联:

2、使用 git branch --set-upstream-to=<shortname/branch**> ** 指令关联分支,需要本地存在指定的分支用于和远程分支关联。若本地没有用于和远程分支关联的分支,那么可以使用下面的指令进行分支关联:

git branch <shortname/branch>

执行上面的指令就可以在创建本地分支的同时,将创建的分支关联远程分支:

需要注意的是,使用该指令创建本地分支指向的提交记录和本地的远程分支相同。从截图中可以看到,本地 remote_master 分支的提交记录既没有领先远程分支,也没有落后远程分支。

如果想在分支创建、关联操作完成后,直接切换到该分支,就可以使用下面的指令:

git checkout -b <shortname/branch>

由于该指令是常用的操作,所以 Git 提供了 --track 参数以便快捷操作:

git checkout --track <shortname/branch>

该指令和 git checkout -b <shortname/branch> 指令的不同在于:它不能指定创建的本地分支的名字,创建的本地分支和远程分支同名:

push

前面三个小节我多次提到了 git push 指令,该指令的作用是将指定本地分支推送到对应远程分支。该指令的完整形式如下:

git push :<remote_branch>

变式一

若省略远程分支名(同时省略“:”),则表示将本地分支推送到与之同名的远程分支**。如果不存在与该本地分支同名的远程分支,则在远程仓库新建与本地分支同名的**远程分支。

上图中,将本地 newb 分支推送到远程仓库的 newb 分支,但是远程仓库并没有 newb 分支,因此在远程仓库新建 newb 分支,然后将本地 newb 分支推送过去。

在 github 上查看是否推送成功:

需要注意的是,虽然我们推送成功了,但是本地 newb 分支并没有关联远程 newb 分支:

变式二

若省略本地分支名(不省略“:”),表示推送一个空的分支到指定的远程分支。即删除指定的远程分支:

git push origin :master

当然你也可以使用 --delete 参数删除远程分支,二者是等价的:

git push origin --delete master

上一篇文章提到可以使用 git branch -m 指令重命名本地分支,但是远程分支是无法重命名的。只能先删除指定的远程分支,然后将本地分支推送到远程仓库中。

变式三

如果当前所在分支存在关联的远程分支,则远程仓库别名、本地分支和远程分支名都可以省略:

git push

因此建议在第一次将本地分支推送到远程仓库的时候,就将指定本地分支和指定远程分支关联。该指令在前文已经介绍过了:

git push -u origin

该指令等价于下面两条指令的组合:

1、git push origin

2、git branch -u <shortname/branch>

推送模式

在 Git 2.0 版本之前,默认的推送模式为 matching 模式 —— 将本地分支推送到同名的远程分支。也就是说,Git 2.0 之前的版本(本文所用 Git 版本为 1.8),即使当前所在的本地分支没有关联远程分支,但是存在同名的远程分支,直接执行 git push 指令也是可以成功的。

例如,github 远程仓库上存在 newb 分支:

而本地 newb 分支没有关联的远程分支,此时在本地 newb 分支上直接执行 git push 指令,也可以将新的提交记录推送到远程 newb 分支:

从上图中可以看到 Git 给出的警告信息为:推送模式没有设置,Git 2.0 版本的默认推送模式将会从 matching 修改为 simple。那么 simple 推送模式是怎样的呢?

将推送模式修改为 simple

git config --global push.default simple

再次执行 git push 指令:

可以看到使用 simple 推送模式,本地分支需要存在关联远程分支才可以进行推送。

除了 matchingsimple,还有 3 种推送模式。Git 的全部 5 种推送模式如下:

nothing:需要显式指出推送的远程分支。例如:git push origin master

current:推送当前所在的分支远程同名分支,如果远程分支不存在相应的同名分支,则在远程仓库创建同名分支

upstream:推送当前所在分支到它的关联的远程分支上。这个模式只适用于推送到与拉取数据相同的仓库

simple:只能推送本地分支到关联的远程分支上,如果推送的远程仓库和拉取数据的远程仓库不一致,那么该模式会像current 模式一样进行操作。(Git 2.0 默认模式)

matching:推送本地分支远程同名分支。(Git 2.0 之前默认模式)

fetch

到现在为止,我们进行的操作都是单向的将本地仓库的提交记录推送到远程仓库。在多人团队开发中,团队中的成员都会向远程仓库推送本地的提交记录,因此我们需要将远程仓库中其他人所做的提交抓取到本地仓库。

下面的指令可以访问指定的远程仓库,从中抓取本地仓库没有的数据:

git fetch

该指令通常用来查看团队其他人的开发进度,因为它取回的数据存放在本地的远程仓库(相当于远程仓库的镜像),不会影响到本地仓库中的数据。

remote 小节中提到执行 git remote rm 指令删除本地的远程仓库,删除的就是远程仓库的镜像。

若远程仓库新增 newb_copy 分支,使用 git fetch 指令可以将该分支从远程仓库抓取到本地:

上图中我没有输入仓库别名, 却依然能从远程仓库抓取更新。这是因为如果省略仓库别名,Git 默认取回 origin 记录的远程仓库的更新。这也就是默认将远程仓库命名为 origin 的原因。

那么抓取的远程分支被放到哪里了呢?答案是本地的远程分支。前文提到本地的远程仓库是远程仓库的镜像,而本地的远程分支也就是远程分支的镜像。

branch 小节中提到 git branch -r 指令可以列出所有本地的远程分支。若团队中有其他人向远程仓库推送了新的分支,而我们没有执行 git fetch 指令从远程仓库抓取更新,那么此时执行 git branch -r 指令查看到本地的远程分支数量就会比远程仓库中的分支少。

将新远程分支抓取到本地后,我们可以使用 git checkout --track <shortname/branch> 指令创建本地分支并关联远程分支:

当然我们也可以直接将新远程分支抓取到本地分支:

git fetch <remote_branch>:

需要注意的是,使用该指令创建的本地分支并不会关联远程分支:

执行该指令时,如果本地分支已经存在并且能以 fast-forward 的方式进行合并,那么远程分支的更新会被合并到本地分支上,否则该指令会拒绝执行:

若省略指令中的本地分支名,则表示抓取远程分支的更新到本地远程分支,而不会影响到本地分支:

git fetch <remote_branch>

当然如果你随便写一个不存在的远程分支,就会得到这样的提示信息:

抓取到远程分支的更新后,我们就可以将本地的远程分支合并到在本地分支上:

git merge <shortname/branch>

prune

前文提到可能我们在本地仓库开发的时候,团队中的其他人向远程仓库推送了新的分支。那么自然也有可能出现这种情况:团队中的其他人删除了远程仓库中的某个分支,但是此时我们本地远程分支依然存在,如下图:

可以看到截图中,Git 推荐使用 git remote prune 清除远程仓库中已经删除而本地仓库依然存在的本地远程分支。在执行该指令之前,可以先执行下面的指令,查看哪些本地远程分支会被清除:

git remote prune --dry-run

--dry-run 参数也可以使用 -n 参数代替,这应该会让你想起一个熟悉的指令:git clean -n

确认无误后,执行 git remote prune origin 指令即可:

FETCH_HEAD

仓库的 .git 目录下存在 FETCH_HEAD 文件,当我们进行 fetch 操作时就会影响到该文件。FETCH_HEAD 文件负责记录某个远程分支最新提交。

这么一说你可能会想到另一个文件:HEAD。HEAD 文件中存放分支文件的引用,表示当前所在的分支。那么 FETCH_HEAD 和 HEAD 一样,也是一个引用么?口说无凭,眼见为实。

执行 git fetch <remote_branch> 指令之后,查看 FETCH_HEAD 文件:

可以看到 FETCH_HEAD 文件中存放远程分支最新提交的 SHA-1 值和提交所在分支。试着切换到 FETCH_HEAD:

成功切换了 FETCH_HEAD 所记录的那一次提交。在上一篇文章的学习中,我们知道 checkout 操作切换分支本质上是通过 SHA-1 值来定位那一次提交,而 FETCH_HEAD 可以配合 checkout 使用,这就说明 FETCH_HEAD 文件本质上和分支文件是一样的——都是指针,指向最新一次提交记录,只是文件形式上略有不同。

但是当我们执行 git fetch 指令后,再次查看 FETCH_HEAD 文件:

从上图中可以看到 FETCH_HEAD 文件中记录所有远程分支的最新提交,这是因为该指令会将远程仓库中的所有分支更新全部抓取回本地。在这种情况下,FETCH_HEAD 依然是一个指针,默认指向远程 master 分支的最新一次提交:

pull

通过 fetch 小节的学习,我们知道 fetch 操作可以将远程分支的更新抓取到本地的远程分支上,然后我们将本地的远程分支上的更新合并到本地分支上即可。那么抓取更新和合并分支,能不能简化成一步操作呢?答案是肯定的,这个操作就是 pull 操作。

pull 操作的指令完整形式如下:

git pull <remote_branch>:

该指令的作用是抓取远程分支的更新到本地的远程分支,然后将本地的远程分支与本地分支合并。

若省略远程分支名(同时省略“:”),表示抓取指定的远程分支和当前所在分支合并:

git pull <remote_branch>

从上图中可以看到合并操作是自动进行的,即使远程分支和本地分支之间存在冲突,合并操作也不会拒绝执行。

而在 fetch 小节学习中我们知道,执行 git fetch <remote_branch>: 指令,如果远程分支和本地分支的合并不能以 fast-forward 方式进行,该指令会拒绝执行。另一条指令:git fetch <remote_branch>,则只是将远程分支的更新抓取到本地的远程分支,不会影响到本地分支。

这就是 pull 操作和 fetch 操作的不同之处。简而言之:pull = fetch + merge

若当前所在分支存在关联的远程分支,则远程仓别名、本地分支名和远程分支名都可以省略:

git pull

tag(remote)

执行 git fetch 指令可以将远程仓库中的标签抓取到本地远程仓库,并在本地仓库创建该标签:

但是使用 git push 指令却无法将本地的标签推送到远程仓库。要将本地标签推送到远程仓库,需要使用下面的指令:

git push --tags

上面的指令会将所有的本地标签推送到远程仓库。如果你只想推送指定的标签,可以使用下面的指令:

git push ...

该指令可以将任意个标签推送到远程仓库,多个标签之间用空格隔开。推送完成后,可以在 github 远程仓库的 Tags 目录下看到推送的结果:

使用上面两条指令推送标签,在远程仓库中创建的标签和本地标签同名。若不想远程标签和本地标签同名,可以使用如下指令:

git push :<remote_tag>

推送完成后,可以使用下面的指令查看远程仓库中的标签:

git ls-remote --tags

上图中 v1.0 是一个轻量标签,v2.0 是一个附注标签。上一篇文章中提到,附注标签是一个完整的对象。因此除了提交记录的 SHA-1 值,标签 v2.0 还有自己的 SHA-1 值。refs/tags/v2.0 代表该标签自身的 SHA-1 值,refs/tags/v2.0^{} 代表该标签指向提交记录的 SHA-1 值。

和删除远程分支一样,删除远程标签可以使用下面两条指令:

git push --delete

git push :refs/tags/

第二条指令之所以要将远程标签的路径写的如此详细,你可以设想这样一种情况:如果远程标签名和远程分支名是一样的,那么 Git 就不知道你要执行的是 git push :<remote_branch> 还是 git push : 指令:

这种情况下只有将要删除的远程标签的路径写详细一些,才能完成删除操作:

若只想将从远程仓库抓取标签更新可以使用如下指令:

git fetch --tags

若只需要抓取指定的标签可以使用下面的指令:

git fetch tag

refspec

refspec 是 Reference Sepcification 的简称,翻译过来就是“引用规则”。它并非一个指令,而是一种规则,定义如何将远程仓库的引用映射到本地。可能这样的描述还是让你觉得云里雾里,下面通过实际操作进一步详解。

执行 git remote add origin 指令后,.git/config 文件中会增加这样的记录:

[remote "origin"]
url = git@github.com:BWHN/testgit.git
fetch = +refs/heads/*:refs/remotes/origin/*

这就表示 origin 记录的远程仓库地址为 git@github.com:BWHN/testgit.git,当执行 git fetch origin 指令时,origin 记录的远程仓库中 refs/heads 目录下的内容都会抓取到本地仓库的 refs/remotes/origin 目录下。

本地仓库中的 refs/remotes/origin 目录也就是 fetch 小节中提及的本地的远程仓库,查看该目录下的文件:

该目录下的文件正是 fetch 小节中介绍的本地的远程分支

refspec 的格式可以概括为 :, 代表远程仓库的引用格式, 代表本地仓库的引用格式。你肯定注意到 “fetch =” 后面还有一个 +,这代表从远程仓库抓取更新到本地仓库时,即使不能快进(fast-forward)也要强制更新它。

如果你想在执行 git fetch 指令时只拉取远程 master 分支的更新,而不是远程仓库所有分支的更新,你就可以把 fetch 这一行修改成这样:

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

当然你也可以在配置文件中指定多个 refspec 。例如执行 git fetch 指令时获取 master 和 master_copy 两个分支的更新:

[remote "origin"]
url = git@github.com:BWHN/testgit.git
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/master_copy:refs/remotes/origin/master_copy

clone

至此为止,我们学会了为本地仓库添加远程仓库,然后一点一点的将本地仓库中的内容推送到远程仓库。也就是一个从零开始的过程。如果远程仓库中已经是一个完整的项目,而我是中途加入项目的呢?

此时我们就应该先整个远程仓库抓取到本地,然后在该仓库中进行开发、推送。这里需要使用 clone 操作:

git clone

该指令中用到的 url 和添加远程仓库用到的 url 是一样的,你可以用远程仓库的 HTTPS 地址(之后推送时每次需要输入账号和密码),也可以使用 SSH 地址。

执行该指令可以将远程仓库整个克隆到本地。克隆仓库使用 origin 作为远程仓库别名,在克隆仓库中只有一个和远程仓库默认分支同名的本地分支,并且本地分支和远程分支是关联的:

查看 github 远程仓库,可以看到默认分支为 master 分支:

默认情况下,克隆仓库和远程仓库同名。若不想将远程仓库名用作克隆仓库名,可以在克隆仓库时指定本地仓库名:

git clone

alias

别名的设置非常简单,完全可以在上一篇文章中就讲完。之所以放到这里才介绍,主要还是想让各位多熟悉每条指令。

我想下面的这种情况,在你使用 Git 的过程中肯定遇到过:

有些指令我们已经非常熟悉了,但是使用时可能由于手滑拼错单词,而且 Git 并不会在我们输入部分命令时自动推断出你想要的命令。因此对于一些常用且很熟悉的指令,完全可以为它们设置一个别名。例如:

git config --global alias.co checkout

git config --global alias.br branch

git config --global alias.ci commit

git config --global alias.st status

设置别名之后,输入指令就简单多了:

若指令之间存在空格,可以用引号将指令括起来:

git config --global alias.unstage 'reset HEAD'

甚至还有人定义这种丧心病狂的别名:

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

显示效果如下:

GUI(图形用户界面)

首先需要声明:非桌面环境下的 Linux 系统,无法使用该功能。下面演示的图片,出自 Windows 系统上的 Git Bash 。

诚然在终端我们可以发挥 Git 的全部能力,但是在某些场景下我们需要图形化界面。安装 Git 的同时,Git 提供了两个可视化工具:gitkgit-gui

官方对于 gitk 给出的建议如下:

gitk 是一个历史记录的图形化查看器。 你可以把它当作是基于 git loggit grep 命令的一个强大的图形操作界面。 当你需要查找过去发生的某次记录,或是可视化查看项目历史的时候,你将会用到这个工具。

只需要在 Git 工作目录下,输入 gitk 即可打开该工具:

gitk 界面中查看提交记录,可以看到基本上每次提交会有两个属性:Parent 和 Child。通过这两个属性,每一次提交可以记录上一次提交和下一次提交的 SHA-1 值,从而组合成一个提交链

当然有的提交记录可能没有 Parent ,例如 Git 仓库的第一次提交;有的提交记录会没有 Child,比如 Git 仓库的最新一次提交;也有的提交记录存在两个 Parent,这就是合并之后的提交记录;还有的提交记录存在两个 Child,因为这次提交上分出了两个分支。

gitk 相比,git-gui 则主要是一个用来制作提交的工具。同样的,在 Git 工作目录下输入 git gui 即可打开工具:

关于两个图形化工具的使用,我就不再多做介绍。GUI 本质上也是执行命令,相信你应该可以很快上手这两款工具。

下一篇:Git详解(三) 项目协作

参考:

Git中的Reference及其refspec概述

Git 官方文档

Pro Git 中文版

Git 之术与道 -- 对象