上一篇:Git 详解(一) 本地操作
一次远程仓库推送
远程仓库使用 github 提供的仓库。注册 github 账号,创建远程仓库 testgit :
这一步操作相当于在本地创建 testgit 文件夹,然后在文件夹中执行 git init 指令生成 Git 仓库。
将 github 上的远程仓库添加到本地,并为该远程仓库设置别名(别名通常使用 origin ):
git remote add origin https://github.com/BWHN/testgit.git
此时 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 <url>
之后进行推送操作时就不需要再输入 github 的账号和密码了:
remote
远程仓库指的是不在你计算机上的 Git 仓库,也就是你无法直接操作到的 Git 仓库。
在一次远程仓库推送小节中,提到了两个 remote 相关的指令。首先是添加远程仓库,添加远程仓库需要为远程仓库地址设置一个别名,因为我们不可能每次操作远程仓库时都输入它的 url,那太难记住了。该指令格式如下:
git remote add <shortname> <url>
其次是修改别名记录的远程仓库地址:
git remote set-url <shortname> <url>
当然除了修改别名记录的远程仓库地址,你也可以只远程仓库的别名而不修改远程仓库的地址:
git remote rename <shortname> <newname>
执行下面的指令就可以查看所有的远程仓库:
git remote
加上 -v 参数还可以显示远程仓库的地址:
git remote -v
关于上图中为什么同一个远程仓库的别名和地址会出现两次,看完后续小节后你的疑问将会得到解答。
查看远程仓库详细信息:
git remote show <shortname>
执行该指令输出的信息十分有用:
从上图中可以看到远程仓库的地址、远程仓库的分支、执行 git pull 指令(见 pull 小节)哪个本地分支会和哪个远程分支会合并、执行 git push 指令(见 push 小节)哪个本地分支会被推送到哪个远程分支。
删除本地的远程仓库(并不会删除 github 上的远程仓库,见 fetch 小节):
git remote rm <shortname>
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> <branch>
若省略本地分支,那么该指令默认将当前所在分支关联到指定的远程分支。--set-upstream-to 参数比较长,可以使用 -u 来代替该参数:
git branch -u <shortname/branch> <branch>
将本地 newb 分支也关联远程 master 分支:
很明显本地 newb 分支不应该关联远程 master 分支,此时我们可以通过 --unset-upstream 参数取消本地分支和远程分支的关联:
git branch --unset-upstream <branch>
若省略本地分支,默认取消当前分支与远程分支的关联:
2、使用 git branch --set-upstream-to=<shortname/branch> <branch> 指令关联分支,需要本地存在指定的分支用于和远程分支关联。若本地没有用于和远程分支关联的分支,那么可以使用下面的指令进行分支关联:
git branch <branch> <shortname/branch>
执行上面的指令就可以在创建本地分支的同时,将创建的分支关联远程分支:
需要注意的是,使用该指令创建本地分支指向的提交记录和本地的远程分支相同。从截图中可以看到,本地 remote_master 分支的提交记录既没有领先远程分支,也没有落后远程分支。
如果想在分支创建、关联操作完成后,直接切换到该分支,就可以使用下面的指令:
git checkout -b <branch> <shortname/branch>
由于该指令是常用的操作,所以 Git 提供了 --track 参数以便快捷操作:
git checkout --track <shortname/branch>
该指令和 git checkout -b <branch> <shortname/branch> 指令的不同在于:它不能指定创建的本地分支的名字,创建的本地分支和远程分支同名:
push
前面三个小节我多次提到了 git push 指令,该指令的作用是将指定本地分支推送到对应远程分支。该指令的完整形式如下:
git push <shortname> <branch>:<remote_branch>
变式一
若省略远程分支名(同时省略“:”),则表示将本地分支推送到与之同名的远程分支。如果不存在与该本地分支同名的远程分支,则在远程仓库新建与本地分支同名的远程分支。
上图中,将本地 newb 分支推送到远程仓库的 newb 分支,但是远程仓库并没有 newb 分支,因此在远程仓库新建 newb 分支,然后将本地 newb 分支推送过去。
在 github 上查看是否推送成功:
需要注意的是,虽然我们推送成功了,但是本地 newb 分支并没有关联远程 newb 分支:
变式二
若省略本地分支名(不省略“:”),表示推送一个空的分支到指定的远程分支。即删除指定的远程分支:
git push origin :master
当然你也可以使用 --delete 参数删除远程分支,二者是等价的:
git push origin --delete master
上一篇文章提到可以使用 git branch -m <branch> 指令重命名本地分支,但是远程分支是无法重命名的。只能先删除指定的远程分支,然后将本地分支推送到远程仓库中。
变式三
如果当前所在分支存在关联的远程分支,则远程仓库别名、本地分支和远程分支名都可以省略:
git push
因此建议在第一次将本地分支推送到远程仓库的时候,就将指定本地分支和指定远程分支关联。该指令在前文已经介绍过了:
git push -u origin <branch>
该指令等价于下面两条指令的组合:
1、git push origin <branch>
2、git branch -u <shortname/branch> <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 推送模式,本地分支需要存在关联远程分支才可以进行推送。
除了 matching 和 simple,还有 3 种推送模式。Git 的全部 5 种推送模式如下:
nothing:需要显式指出推送的远程分支。例如:git push origin master
current:推送当前所在的分支到远程同名分支,如果远程分支不存在相应的同名分支,则在远程仓库创建同名分支
upstream:推送当前所在分支到它的关联的远程分支上。这个模式只适用于推送到与拉取数据相同的仓库
simple:只能推送本地分支到关联的远程分支上,如果推送的远程仓库和拉取数据的远程仓库不一致,那么该模式会像current 模式一样进行操作。(Git 2.0 默认模式)
matching:推送本地分支到远程同名分支。(Git 2.0 之前默认模式)
fetch
到现在为止,我们进行的操作都是单向的将本地仓库的提交记录推送到远程仓库。在多人团队开发中,团队中的成员都会向远程仓库推送本地的提交记录,因此我们需要将远程仓库中其他人所做的提交抓取到本地仓库。
下面的指令可以访问指定的远程仓库,从中抓取本地仓库没有的数据:
git fetch <shortname>
该指令通常用来查看团队其他人的开发进度,因为它取回的数据存放在本地的远程仓库(相当于远程仓库的镜像),不会影响到本地仓库中的数据。
remote 小节中提到执行 git remote rm <shortname> 指令删除本地的远程仓库,删除的就是远程仓库的镜像。
若远程仓库新增 newb_copy 分支,使用 git fetch 指令可以将该分支从远程仓库抓取到本地:
上图中我没有输入仓库别名, 却依然能从远程仓库抓取更新。这是因为如果省略仓库别名,Git 默认取回 origin 记录的远程仓库的更新。这也就是默认将远程仓库命名为 origin 的原因。
那么抓取的远程分支被放到哪里了呢?答案是本地的远程分支。前文提到本地的远程仓库是远程仓库的镜像,而本地的远程分支也就是远程分支的镜像。
branch 小节中提到 git branch -r 指令可以列出所有本地的远程分支。若团队中有其他人向远程仓库推送了新的分支,而我们没有执行 git fetch 指令从远程仓库抓取更新,那么此时执行 git branch -r 指令查看到本地的远程分支数量就会比远程仓库中的分支少。
将新远程分支抓取到本地后,我们可以使用 git checkout --track <shortname/branch> 指令创建本地分支并关联远程分支:
当然我们也可以直接将新远程分支抓取到本地分支:
git fetch <shortname> <remote_branch>:<branch>
需要注意的是,使用该指令创建的本地分支并不会关联远程分支:
执行该指令时,如果本地分支已经存在并且能以 fast-forward 的方式进行合并,那么远程分支的更新会被合并到本地分支上,否则该指令会拒绝执行:
若省略指令中的本地分支名,则表示抓取远程分支的更新到本地远程分支,而不会影响到本地分支:
git fetch <shortname> <remote_branch>
当然如果你随便写一个不存在的远程分支,就会得到这样的提示信息:
抓取到远程分支的更新后,我们就可以将本地的远程分支合并到在本地分支上:
git merge <shortname/branch>
prune
前文提到可能我们在本地仓库开发的时候,团队中的其他人向远程仓库推送了新的分支。那么自然也有可能出现这种情况:团队中的其他人删除了远程仓库中的某个分支,但是此时我们本地远程分支依然存在,如下图:
可以看到截图中,Git 推荐使用 git remote prune <shortname> 清除远程仓库中已经删除而本地仓库依然存在的本地远程分支。在执行该指令之前,可以先执行下面的指令,查看哪些本地远程分支会被清除:
git remote prune --dry-run <shortname>
--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 <shortname> <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 <shortname> <remote_branch>:<branch>
该指令的作用是抓取远程分支的更新到本地的远程分支,然后将本地的远程分支与本地分支合并。
若省略远程分支名(同时省略“:”),表示抓取指定的远程分支和当前所在分支合并:
git pull <shortname> <remote_branch>
从上图中可以看到合并操作是自动进行的,即使远程分支和本地分支之间存在冲突,合并操作也不会拒绝执行。
而在 fetch 小节学习中我们知道,执行 git fetch <shortname> <remote_branch>:<branch> 指令,如果远程分支和本地分支的合并不能以 fast-forward 方式进行,该指令会拒绝执行。另一条指令:git fetch <shortname> <remote_branch>,则只是将远程分支的更新抓取到本地的远程分支,不会影响到本地分支。
这就是 pull 操作和 fetch 操作的不同之处。简而言之:pull = fetch + merge 。
若当前所在分支存在关联的远程分支,则远程仓别名、本地分支名和远程分支名都可以省略:
git pull
tag(remote)
执行 git fetch 指令可以将远程仓库中的标签抓取到本地远程仓库,并在本地仓库创建该标签:
但是使用 git push 指令却无法将本地的标签推送到远程仓库。要将本地标签推送到远程仓库,需要使用下面的指令:
git push <shortname> --tags
上面的指令会将所有的本地标签推送到远程仓库。如果你只想推送指定的标签,可以使用下面的指令:
git push <shortname> <tag1> <tag2> ...
该指令可以将任意个标签推送到远程仓库,多个标签之间用空格隔开。推送完成后,可以在 github 远程仓库的 Tags 目录下看到推送的结果:
使用上面两条指令推送标签,在远程仓库中创建的标签和本地标签同名。若不想远程标签和本地标签同名,可以使用如下指令:
git push <shortname> <tag>:<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 <shortname> --delete <tag>
git push <shortname> :refs/tags/<tag>
第二条指令之所以要将远程标签的路径写的如此详细,你可以设想这样一种情况:如果远程标签名和远程分支名是一样的,那么 Git 就不知道你要执行的是 git push <shortname> :<remote_branch> 还是 git push <shortname> :<tag> 指令:
这种情况下只有将要删除的远程标签的路径写详细一些,才能完成删除操作:
若只想将从远程仓库抓取标签更新可以使用如下指令:
git fetch <shortname> --tags
若只需要抓取指定的标签可以使用下面的指令:
git fetch <shortname> tag <tag>
refspec
refspec 是 Reference Sepcification 的简称,翻译过来就是“引用规则”。它并非一个指令,而是一种规则,定义如何将远程仓库的引用映射到本地。可能这样的描述还是让你觉得云里雾里,下面通过实际操作进一步详解。
执行 git remote add origin <url> 指令后,.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 的格式可以概括为 <src>:<dst>,<src> 代表远程仓库的引用格式,<dst> 代表本地仓库的引用格式。你肯定注意到 “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 和添加远程仓库用到的 url 是一样的,你可以用远程仓库的 HTTPS 地址(之后推送时每次需要输入账号和密码),也可以使用 SSH 地址。
执行该指令可以将远程仓库整个克隆到本地。克隆仓库使用 origin 作为远程仓库别名,在克隆仓库中只有一个和远程仓库默认分支同名的本地分支,并且本地分支和远程分支是关联的:
查看 github 远程仓库,可以看到默认分支为 master 分支:
默认情况下,克隆仓库和远程仓库同名。若不想将远程仓库名用作克隆仓库名,可以在克隆仓库时指定本地仓库名:
git clone <url> <dir>
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 提供了两个可视化工具:gitk 和 git-gui。
官方对于 gitk 给出的建议如下:
gitk 是一个历史记录的图形化查看器。 你可以把它当作是基于 git log 和 git grep 命令的一个强大的图形操作界面。 当你需要查找过去发生的某次记录,或是可视化查看项目历史的时候,你将会用到这个工具。
只需要在 Git 工作目录下,输入 gitk 即可打开该工具:
在 gitk 界面中查看提交记录,可以看到基本上每次提交会有两个属性:Parent 和 Child。通过这两个属性,每一次提交可以记录上一次提交和下一次提交的 SHA-1 值,从而组合成一个提交链。
当然有的提交记录可能没有 Parent ,例如 Git 仓库的第一次提交;有的提交记录会没有 Child,比如 Git 仓库的最新一次提交;也有的提交记录存在两个 Parent,这就是合并之后的提交记录;还有的提交记录存在两个 Child,因为这次提交上分出了两个分支。
与 gitk 相比,git-gui 则主要是一个用来制作提交的工具。同样的,在 Git 工作目录下输入 git gui 即可打开工具:
关于两个图形化工具的使用,我就不再多做介绍。GUI 本质上也是执行命令,相信你应该可以很快上手这两款工具。
下一篇:Git详解(三) 项目协作
参考: