likes
comments
collection
share

Git 学习笔记

作者站长头像
站长
· 阅读数 7

一个不错的场景式 Git 最佳实践手册(多语言):github.com/k88hudson/g…

Git 版本控制与存储模型

Git 是什么,为什么要使用 Git

想象一个场景:你写了一个文件,版本是 1.0,现在你需要修改,优化内容,又怕把原来的文件丢掉,于是将 1.0 版本的文件拷贝了一份,在这个副本上进行修改。

两个文件尚不能造成太多困扰,然而实际学习和工作中,经常遇到一个项目中有多个文件,每个文件的修改次数都不少,团队合作时,每个人都需要同步地更新进度,才能保证各自更新、沟通的部分不会出错。这导致版本管理非常难,造成了很大的脑力和人力成本。

Git 就是为解决这个问题而诞生的。Git 是一种版本控制系统,会帮助工作者,记录每一次提交的更新,而工作者不必手动地创建副本、合并更新、管理版本历史。在团队协作时,也不需要某个管理者主动分发历史记录和新版本给其他成员,项目成员可以直接从任意一个保存了完整版本记录的机器(比如一个专门的中央服务器)上,远程拷贝、拉取一份相同的记录,创建自己的工作分支,将自己的更新推送回去。每个成员完成各自的任务并推送后,管理者通过 Git 对这些分支进行检查,合并,就完成了总项目的版本更新。

分布式系统、本地化存储

Git 被设计为分布式的管理系统,参与项目的每台机器上都有一个完整的版本库,对于版本历史的提交与修改,也是在本地完成。对于该台机器的使用者而言,可以很快的切换版本与分支,而不必通过网络获取版本信息。

需要和其他机器的版本历史进行交互,比如克隆、远程推送,统一归纳合并时,才需要网络。

至于所谓【中央服务器】,只是为了方便,设置一个拥有干净、完整版本历史记录的服务器,便于发布、管理、标准化,不代表 Git 不是分布式系统。

存储项目随时间改变的快照(snapshot)

每次的更新提交/保存项目状态时,Git 会对当时的全部文件创建一个快照(snapshot)并保存索引/指针。当然,没有修改的文件就不必重新创建快照并存储,只要保留链接,指向之前的文件即可。因此在改变、查看不同版本的文件时,Git 通过改变索引/指针的方式,就可以进行替换,而不必重新计算差别,这是 Git 版本切换比较快的原因。

Git 学习笔记

三种状态

Git 将要进行版本控制的文件夹/文件,分成了工作区、暂存区、版本库三个部分。

  • 工作区,代表我们当前正在编写、修改,还没有提交到暂存区的文件
  • 暂存区,通过 git add 将工作区的当前状态保存下来,作为未来可能会提交到版本库的预备信息,可以多次添加覆盖
  • 版本库,或者说 Git 仓库,是通过 git commit 将暂存区的信息保存,作为最终的、正式的版本记录,添加进版本库中,版本库的内容不能随意修改(版本回退是索引/指针回退,并不会真的删除内容)。
Git 学习笔记

起步

安装 Git

git-scm.com/book/en/v2/…

安装后配置用户信息

设置用户名和邮件地址,这个设置是必要的。每一个 Git 提交都需要一个来源记录,这样才能正常工作。

$ git config --global user.name "John Doe"
$ git config --global user.email "johndoe@example.com"

--global 代表全局配置,默认情况下本机的提交都按该配置运行,如果要在特定项目下更改配置,就在该项目下运行没有 --global 的配置。

检查配置信息通过以下命令,其他配置参数查看官网 document 即可。

$ git config --list

查看帮助

$ git help config
$ git add -h

以上指令可以在控制台查看特定指令的帮助信息,help 指令看完整手册,示例中表示查看 git config 的说明。-h 则比较简明,用于查看命令可选参数的快速参考,示例语句表示查看 git add 的参数。后续学习的内容中,很多指令的参数可以通过以上命令来查阅。

此外,通过 Git 官网、技术论坛、搜索引擎等方法搜索使用说明也很方便。

Git 基础

获取 Git 仓库

在本地创建 Git 仓库不外乎两种方式,要么本地初始化创建,要么克隆别人的仓库:

在已存在的项目目录中初始化一个仓库

进入到你的项目目录中,然后输入以下命令,即可让 Git 接管当前目录及其子目录的文件。

$ git init

这一步会在目录中创建一个 .git 隐藏文件夹,用于 Git 工作,不要随意改动该目录中的内容。

从其他服务器克隆一个仓库

$ git clone https://github.com/libgit2/libgit2 mylibgit

通过 git clone,将 github 上的 libgit2 仓库,克隆到了我们本地,自定义名为 mylibgit 的目录中,我们现在拥有该仓库在 github 上保存的完整的版本信息,和初始化仓库一样拥有 .git 文件夹,可以直接在此基础上开发。

除了 https 协议以外,Git 还支持 git:// 协议, 或 SSH 传输协议,具体的链接,在 github、gitlab 等远程仓库上会有选项支持。为了明确用户和保证安全,这些网站一般都要求配置本地机器的 hash 认证密钥,用于通信时的安全校验,相关内容后续进行补充。

基本操作

下图展示了文件的状态变化周期:

Git 学习笔记
  • Untracked 未跟踪,一般发生在新建文件,或者文件被从暂存区和版本库记录里移除掉
  • Unmodified 未修改,字面意思,文件没有修改
  • Modified 已修改,同上,工作区文件修改后,还没有提交到暂存区或版本库
  • Staged 已暂存,还没有提交到版本库

查看文件状态

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

git status 可以查看哪些文件是新创建/未跟踪的(untracked files,需要通过 git add 进行跟踪),哪些文件正在修改还没有暂存,哪些文件在暂存区还没有提交等等,强烈建议每次提交前先查看一下

示例信息表示工作区是“干净的(clean)”,表示没有新建或修改文件。注意到单词 master,master 是指我们当前所处的分支,也是默认的主分支,分支的概念可以后续再看。

文件跟踪/添加到暂存区

$ git add *.c
$ git add LICENSE 

如果一个文件未被跟踪,那么在 Git 的 git status 提示信息里,是不会表示其是被修改的(modified),对于该新文件的写入,始终是 untracked。

通过 git add,将未跟踪或修改了的工作区文件添加到暂存区。一个方便的添加所有文件的方法是:

$ git add --all

提交到版本库

$ git commit -m 'initial project version'

-m 和后面的字符串,用于标记本次提交的描述。 通过以上指令,暂存区的内容就会被提交,一个新的记录就被添加到了 Git 版本库中,Git 会为每个提交自动生成一个 hash 校验值,作为一个提交的唯一标识符,用于后续的区分、切换等,可以通过 git log 查看(查看历史记录)。

如果想一步直接从工作区提交,可以使用 -a 选项,同时完成暂存和提交,但一般不建议这么用。

忽略文件 gitignore

有一些文件,比如临时文件、开发时的工具包、日志文件等等,一般无需纳入 Git 管理,此时就可以在项目根目录下,创建一个 .gitignore 文件,在里面列出要忽略的文件的模式(即正则),例如:

*.[oa]
*~

* 匹配所有字符串,第一行表示忽略所有以 .o.a 结尾的文件,第二行则忽略以 结尾的文件。更多的例子如下

# 忽略所有的 .a 文件
*.a

# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a

# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO

# 忽略任何目录下名为 build 的文件夹
build/

# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt

# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf

查看修改

$ git diff

以上命令比较当前工作区和当前暂存区快照之间的差异,即当前文件,和 git add 时文件的差异

$ git diff --staged

staged 参数,则对比当前暂存区和最新版本库之间的差异,即 git addgit commit 文件之间的差异。(也可以用 cached 参数,同义)

移除文件

$ git rm filename

以上命令会将文件从工作区和暂存区删除,git status 会显示文件已经 deleted,前提是该文件尚未修改或提交到暂存区。下一次提交后,Git 就不会再把该文件纳入版本管理了。

如果要删除的文件,修改过或已经放到暂存区,就给 git rm-f 参数,表示强制删除。

如果想把一个文件从 Git 管理中移除,但依然需要保留在当前工作目录,就添加 --cached 参数,后续通过 .gitignore 来忽略该文件

移动/重命名文件

Git 并不能直接跟踪文件移动,单纯重命名文件会被当作新文件,要在 Git 中对文件改名,可以这么做:

$ git mv README.md README

相当于以下三条命令

$ mv README.md README
$ git rm README.md
$ git add README

通过这种 git mv,Git 就可以推断这是一次重命名,在 git status 中,会显示:

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

查看历史记录

通过 git log 进行查看:

$ git log
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

按时间先后次序,新提交在上面,列出每次提交的哈希校验和,作者信息,以及提交说明(git commit -m xxx

如果要使提交信息在一行中显示,可以使用 --pretty=oneline

$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing

更常用的方式,是使用 --pretty 参数和 --graph 参数,可以更方便的查看历史校验和、分支合并图等

$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
*  5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
*  11d191e Merge branch 'defunkt' into local

--pretty=format:是一种制定输出格式的语法,%h 表示输出简写的哈希,%s表示输出提交说明。

具体参数可以查看官方文档:git-scm.com/book/en/v2/…

撤销操作

修改提交

如果有次因为失误,提交后发现有些文件有错误,或有些文件忘了提交到暂存区,这时候可以在修改并加入到暂存区后,执行以下命令:

$ git commit --amend

使用 --amend 参数,可以重新提交,所以可以在进行正确的修改以后,使用该命令修改上一次的错误提交,例如:

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
取消暂存的文件

假设你通过 git add * 或其他操作,已经将一些文件提交到了暂存区,现在你想从暂存区,撤回某个文件的操作,比如 README.md,可以通过以下方式:

$ git reset HEAD README.md

这里使用了 resetHEAD 两个标识符,前者代表重置,后者是一个指针,代表当前最新的一次提交。意思就是将当前文件状态变为上次提交的样子,这样暂存区就会恢复,工作区的修改并未被提交。关于 HEAD 可以查看 HEAD 指针

现在使用 git status 查看,会发现 README.md 已经处于 Changes not staged for commit 分类下。

撤销工作区的修改

假如要通过 Git 命令撤销对工作区文件的修改,可以用最近提交的版本来覆盖它,相当于重置了修改,命令如下。

$ git checkout -- README.md
版本回退

假如我们进行了一次错误的 commit,或者有一些方案被放弃,我们需要回到以前的某个版本重新开发,此时就需要进行版本回退:

$ git reset --hard HEAD^

这里有一个 --hard 参数,代表直接将所有工作内容,重置为某个指针对应的提交。HEAD^ 代表最新提交的上一次提交,一个 ^ 就是一次提前,比如 HEAD^^ 就代表回退两次.....。此外,也可以通过 git log 查看不同提交对应的 hash 校验和来指定。比如某次提交的 hash 值前 6 位为 a721c9 ,那就通过以下指令就可以回到该次提交。

$ git reset --hard a721c9

这里注意,不一定非要是 6 位 hash,Git 会自动根据我们输入的值,查找开头一样的 hash。比如,如果没有重复的话,输入前 4 位也是可以的,有重复的话,就该多输入几位了。

给提交打标签(Tag)

标签是什么?

一般来讲,标签(即 Tag),代表某次提交的标记、名称、版本号等内容,比如 v1.0,v2.0-beta 这种。和 git commit -m 不同,后者一般是对本次提交内容的概括描述,比如修改了什么,新增了什么。这个区别要分清楚。

查看标签历史

很简单,示例如下:

$ git tag
v0.1
v1.0
v1.1
创建标签

Git 有两种标签,轻量标签(lightweight)和附注标签(annotated)。

前者是某个提交的引用,后者则拥有比前者更多的信息,比如打标签者的名字、邮件地址、日期时间等。官网建议创建附注标签,因为这样有更多可追溯的信息。

  • 创建附注标签的示例如下:
$ git tag -a v1.4 -m "my version 1.4"

以上指令通过 -a 创建了 v1.4 的 tag,通过 -m 添加了标签说明。

通过 git show v1.4 就可以看到该标签的提交信息,和对应 commit 的提交信息。

  • 创建轻量标签的示例如下
$ git tag v1.4

不用任何参数即为轻量标签,在 git show v1.4 中,只有 commit 的提交信息,而没有 tag 的提交信息。

  • 对历史提交打标签的示例如下
$ git tag -a v1.2 9fceb02

后面加上某次提交的 hash 即可。

推送标签

默认情况下标签不会传送到远程仓库服务器上,所以需要手动推送标签,推送标签的指令如下:

$ git push origin v1.4

以上指令推送了 v1.4 的标签到远程的 origin 仓库中(origin 概念会在后面的远程服务器中进行描述)。

如果要推送多个标签,可以使用如下命令:

$ git push origin --tags

以上指令将所有不在远程服务器上的标签推送过去

删除标签

删除本地标签:

$ git tag -d v1.4

删除远程标签

$ git push origin --delete 1.4

Git 分支

感性认知

Git 的分支功能,是需要重点掌握的核心。

在文档开头的理论部分,我们说到了,Git 为不同版本的文件创建了快照,通过索引/指针指向不同的快照,来实现版本的快速切换。

那么【分支】这个概念,其实就很简单了。假设我们有一个文件的原始版本,叫做 V0,采用默认指针 master 指向该版本,当我们提交更新到 V1 时,V0 -> V1 就串成了一条有序的引用关系,master 作为一个指针,也从 V0 指向了 V1。现在访问 master,就会显示 V1 版本的文件。

这时候又来了一个成员,于是两个人决定各自开发一部分再合并,于是各自从中央服务器上克隆了一份项目。假设你将 V1 版本的文件更新为 V2-1,用指针 dev1 来表示,他将文件更新为 V2-2,用指针 dev2 来表示。此时 V1 和 V2 之间,就出现了分叉关系,即 V1 的 master 指针,有两条路径可以前进,但这两条路的结果都是不完整的,如何合并为真正的 V2,然后让 master 指向 V2 呢?

现在两人采用了如下方案:新建了一个叫做 pre 的指针,指向 master,也就是 V1,用来表示预发布的意思。现在两人分别将各自的部分,合并到 pre 上去,pre 现在就变成了 V2。这时我们回顾一下所有指针的变化情况:

指针变化路径
masterV0 -> V1
dev1master -> V2-1
dev2master -> V2-2
premaster -> V2 (合并 dev1 和 dev2 的文件内容)

可以看到,整个文件的版本变化,被称为【分支】是非常形象的。经过检查没有问题后,我们让 master 指针,按照 pre 指针的路径前进,现在 master 就指向 V2 了!如果要继续更新版本,就重新拉取 master 文件,然后重复之前的思路。

以上的内容,涉及了分支的创建、切换、合并等操作。

基本操作

HEAD 指针

HEAD 指针是一个概念,可以直观的理解为,HEAD 总是指向当前分支下最新的一次提交。

在之前的操作中,我们要在不同的分支之间进行切换,我们【看到】了不同分支下的不同版本文件,这里要【看到】master、dev1、dev2,就需要一个【眼睛】,这个【眼睛】就是 HEAD 指针,也就是我们在写一些数据结构算法,比如树和链表时,经常用到的活动指针。

比如,当 HEAD 指向 dev1 时,我们看到的就是 dev1 最新的文件快照,指向 master 时,看到的就是 master 的文件快照。

创建分支

$ git branch dev1

以上指令创建一个名为 dev1 的分支,其指针指向当前文件,也就是 HEAD。

查看分支

$ git branch
  dev1
* master

* 表示当前处于哪一个分支。

切换分支

$ git switch dev1

如果要创建的同时切换分支,可以使用 -c 参数。

当你在 dev1 分支下提交新版本时,dev1 和 HEAD 指针都会前进一步,而原来的 master 分支并不会前进。

无分歧的分支合并

现在假设 dev1 分支下的开发已经完成并提交,现在要在 master 的基础上进行第 2 个需求的修改,我们先切换回 master 分支:

$ git switch master

此时 HEAD 指针回到了 master 的位置,工作区内容也恢复到 master 的状态。我们创建 dev2 分支进行修改并提交,步骤和 dev1 是一样的。

现在我们将 dev2 新增的内容合并到 master 上:

$ git switch master
$ git merge dev2

Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

merge 即为合并命令,将目标分支的内容,合并到当前分支上。我们发现输出信息有 Fast-forward 这个单词,代表快进。这是由于 dev2 和 master 之间的变化关系,是一条直线,直接让 master 指针移动到 dev2 即可完成更新。

删除分支

现在 dev2 没用了,我们删除它。

$ git branch -d dev2

有分歧的分支合并

现在我们要将 dev1 的工作内容,也加入到 master 中。显然,这里和 dev2 的合并有一些不同:dev1 的内容来源于更早的历史,并没有 dev2 修改后的部分,直接移动现在的 master 指针到 dev1,会丢失掉 dev2 的内容。此时就会遇到一些额外的状况:

不同分支改动的文件内容有交叉冲突部分

例如两个分支改动了同一个文件的同一个部分,这种情况很常见,比如 dev1 将一个函数放在了顶部,而 dev2 也将一个函数加到了顶部,我们的想法是要这两个函数都有,在 git merge 时,会发生如下情况:

$ git switch master
$ git merge dev1
Auto-merging Test1.txt
CONFLICT (content): Merge conflict in Test1.txt
Automatic merge failed; fix conflicts and then commit the result.

这些提示说明合并发生了冲突,此时打开文件,会有如下状况:

Git 学习笔记

Git 自动对文件进行了特殊处理,同时出现了 dev1 和 dev2 的修改,<<<<<< HEAD====== 之间表示当前 master 对于历史文件的更改,即 dev2 的结果(我们先 merge 了 dev2 到 master 上)。而 >>>>>> dev1====== 之间表示现在要合并来的 dev1 的改动。

此时我们可以手动对两部分进行处理,比如删除 <= 这些冲突提示信息,然后整理一下格式,进行 git addgit commit 即可。

不同分支的改动没有交叉冲突部分

这种情况也很常见,多人编写代码总会分文件模块,由不同的人员编写不同的文件。此时的 merge 就会正常进行,将两个分支的快照,及两者最近的共有根节点合并,变成一个新的提交

比如 dev2 的改动是新建文件,而不是修改源文件。此时我们重复之前的过程,将指向 dev2 的 master 和 dev1 合并:

$ git switch master
$ git merge dev1
Merge made by the 'recursive' strategy.
Test1.txt | 1 +
1 file changed, 1 insertion(+)

虽然此时的 master 和 dev1 处于不同的分支路线中,但此时并不会发生之前的冲突,而是将各自的部分直接叠加,形成一个新的 commit。

分支管理之 Rebase

之前我们提到,合并分支常用的方法是使用 git merge,其原理是将 2 个分支和其最近的公共根节点的内容进行合并,变为一个新的提交:

Git 学习笔记

那么 Rebase 是什么意思呢?

常见 Rebase 示例

按照上图的情况,逻辑上我们也可以不进行暴力合并,而是先对比 C4 和 C2 之间的区别,把其中的变化记录下来,然后添加到 C3 上,形成一个新的提交,experiment 重定向为这个提交(Rebase 这个单词的含义,就是重新更改基准)确认无误后,我们让 master 前进一步即可。这就是 Rebase 的功能:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
Git 学习笔记

可以看到,experiment 从原来的 C4 转移到了 C4',文件内容等价于原来的 C5,这时候通过 git merge,让 master 向前一步即可。

$ git checkout master
$ git merge experiment
Git 学习笔记

有人会问,这样的操作看起来有什么用呢?事实上,从结果的角度,自然和 git merge 的最终结果是一样的,但其好处是提交历史更加整洁美观,从 git log --graph 可以看到多个分叉变为一条直线。开发者可以先在本地将其 dev 开发者分支,rebase 到 master 后面,此时的 dev 就已经是合并后的状态,而 master 并没有改变。这样项目管理者可以直接快速合并代码,减少整合的压力,也符合代码洁癖和强迫症需求,尤其是对于有多个成员,多个分支的 Git 仓库。

更复杂的 Rebase 示例

Git 学习笔记

图中的示例,可以假设为以下情况:在 C2 处,项目被拆分为两个部分,其中一部分是 master 底层主干代码,另一部分为 C3 开始的具体应用开发,而 C3 又被分为两部分,即 server 服务端和 client 客户端。

假如此时客户端已经开发完成,想加入到 master 中,而服务端还没有完成,无法和客户端、C3 根节点合并。这时候就可以使用 git rebase,让 C8 和 C9 的变化,先添加到 master 所在文件快照上,图就变成了以下效果:

Git 学习笔记

具体代码如下:

$ git rebase --onto master server client

该命令的意思是,以 client 和 server 开始分歧时的根节点为基准(C3),和 client 进行对比,获取到 C8 和 C9 的变化,然后添加到 master 所在分支上,形成新的路径,然后 client 指针指向最终结果。

接下来只要和之前一样,移动 master 指针即可:

$ git checkout master
$ git merge client

新的提交历史如下:

Git 学习笔记

现在,服务端 server 也已经开发完成,我们要将其更新的内容 rebase 到 master 分支上。之前我们的步骤,都是先切换到要 rebase 的分支,比如 client,然后指定目标分支。现在我们可以使用更完整的语法,在不切换分支的情况下,直接 rebase 分支:

$ git rebase master server

以上指令的含义非常明了,表示将 server 分支的变化,提交到 master 分支的快照上。提交历史变为如下图片:

Git 学习笔记

然后让 master 分支继续前进,并删除 server 和 client 分支即可:

$ git checkout master
$ git merge server
$ git branch -d client
$ git branch -d server

提交历史最终变为一条干净的直线,amazing!

Git 学习笔记

不要随意使用 Rebase

Rebase 的缺点也很明显,稍微留意一下就会意识到,Rebase 会破坏之前的分支历史,比如上述例子中的 client 和 server 的原路径,所以如果别人可能基于某些分支进行开发,那么就不要用 Rebase 破坏这些分支!

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

事实上原来的例子中,销毁 client 和 server 分支是非常不明智的。因为实际项目中 client 和 server 也会是一个相对独立的主题分支,往往会在中央 Git 仓库进行版本控制,前后端人员会分别基于 client 和 server 上开发,并且项目是不断迭代的,这些主题分支应该被保留。

更多令人难解、痛苦的反面例子,可以自主查阅 Git 官网 Rebase 章节。

Rebase VS Merge 最佳实践

在得到答案之前,我们需要重新思考一下 Merge 和 Rebase 对提交历史的意义。

Merge 操作,本身是对历史的记录,可以对各种分支的变化进行追溯,即使提交历史可能看起来比较复杂、混乱,但追溯历史的价值和意义是永恒的。

Rebase 则销毁了变化的过程,只留下被判定为 “正式出版” 的版本,至于中间的 “草稿” 则被丢弃,好处是作为用户而言,我们能够专注在最有必要研究的地方,而不必浪费时间精力在其他地方。

Git 官网给出了一个原则,我认为比较精准:

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。

需要推送至别处的 Git 提交,意味着会被别人继续开发和研究,这就符合之前说到的不乱用 Rebase 的原因。

远程 Git 仓库

现在我们已经掌握了 Git 的大多数常见本地操作,足以应对版本控制中的一般问题,管理好自己的项目。现在就来到了最后一步,即如何与他人协作,如何使用远程仓库来进行开发,认识诸如 clonepushpullremote等概念,以及 GitHub、GitLab 等著名远程 Git 仓库托管服务。

此外,远程 Git 仓库也可以自己搭建,但绝大多数情况下,开发者都会使用现有的托管服务,所以不会着重描述这一部分,如有需要还请查阅官方文档。

传输协议

网络通信的第一步是需要确定一个通信协议,在这里我们不讲太原理性的部分,我们以 git clone 命令为入口,来说明实际开发时,协议扮演的角色。

$ git clone git@github.com:xxxx/xxxx.git
$ git clone https://github.com/xxxx/xxxx.git

以上两条语句是我们常见的,克隆保存在 GitHub 的远程项目的示例,第一条是 SSH 协议下的 url,第二条是 HTTPS 协议下的 url,他们克隆的实际内容是一样的,只是协议不一样。其他远程 Git 服务如 GitLab 也类似,可能 url 的表示方法不同,但本质一样。

生成 SSH 公钥

为了保证安全,许多 Git 服务器都会用 SSH 密钥来进行认证,来区分、认证、追溯具体的开发机,控制权限等。以 GitHub 为例,你需要做两件事:

  • 在本地生成本机的 SSH 密钥对,一般要通过控制台输入系统命令来生成
  • 找到本地密钥文件,将密钥对中的公钥,加入 GitHub 的信任列表中

完成以上工作,才可以正常下载、上传项目,具体操作可以参考 GitHub 的 SSH 密钥指南:help.github.com/articles/ge…

远程交互基本操作

和远程仓库关联

这里的【关联】有两种情况,一种是本地没有现成的项目,需要通过 git clone 来下载一份项目到本地,这种情况下本地仓库和远程仓库会自动关联。另一种情况是,本地有现成的项目,需要添加或更改远程的关联仓库。现在假设我们有一个名为 learngit 的本地仓库,现在我们在 GitHub 上建立一个同名仓库,然后本地执行以下指令:

$ git remote add origin git@github.com:yourname/learngit.git

以上指令我们可以这么分析:remote 代表远程,add 是增加命令,origin 是远程仓库的名称,惯例上多用 origin 这个单词,后面的 url 就是你的 git 仓库的地址,这里 yourname 只是示例,实际是你的 GitHub 账户名称,这个很好查阅,当你创建好 Github 仓库后,它的 url 地址是可以查看的,SSH 和 HTTPS 版的 url 都可以。

如果你要修改远程仓库的 url,只需要执行如下指令:

$ git remote set-url origin git@github.com:yourname/learngit.git

这里使用 set-url 来进行更改。

要查看远程仓库信息,可以使用如下指令:

$ git remote -v
origin  git@github.com:yourname/learngit.git (fetch)
origin  git@github.com:yourname/learngit.git (push)

-v 参数执行远程库的信息查看,后面的 fetchpush 表示该远程仓库可以执行获取或推送行为。

如果要解除和远程仓库的关联,可以使用 rm 指令:

$ git remote rm origin

查看/跟踪其他远程分支

通过 git clone 下载到本地的仓库,使用 git branch 时会发现本地只有一个 master 分支,如果需要基于远程的其他分支进行开发,此时需要使用 -a 参数查看:

% git branch -a

-a 参数会列出本地和全部远程 remote/origin/XXX 分支,此时我们需要在本地创建一个新的分支并与远程某个分支建立关联:

$ git checkout -b myDev origin/myDev

在这里创建了 myDev 分支并与远程的同名仓库进行内容的同步,此时就可以进行开发并推送到对应的远程分支上了。同理,本地的 master 分支默认与 origin/master 分支对应。

要查看本地某个分支对应的远程分支,可以使用以下指令:

$ git branch -vv

推送到远程仓库

$ git push -u origin master

以上命令将本地 master 分支推送到远程 origin 仓库,其中使用了 push 命令和 -u 参数,前者是远程推送的意思,后者表示将本地 master 和远程 master 关联起来,在以后推送时,直接使用 git push 简化命令即可。当然一般情况下,为了防止失误,还是建议写上本地分支名和远程仓库名,提醒自己不要提交错分支。

如果像之前描述的,基于其他远程分支进行开发,只要按照以下步骤即可:

$ git checkout -b dev origin/dev

令本地分支和远程分支关联,这样就可以基于 dev 分支进行开发,然后推送:

$ git push origin dev

拉取远程分支更新到本地

实际开发是多人协作的,当你在本地开发时,可能远程仓库已经有一些更新了,所以需要先把最新的内容合并进来,整理以后再提交上传。

$ git pull

以上命令会尝试获取 origin 远程仓库的分支到本地,并进行 merge 操作。有时候可能会因为本地分支和远程并不完全一致,导致失败,这时需要显示的指定本地分支和远程要拉取的分支的关系。比如,当前本地处于 dev 分支,没有与远程的 origin/dev 分支关联,那就无法识别并拉取,需要手动设置:

$ git branch --set-upstream-to=origin/dev dev

再进行 git pull 即可。

刚才已经提到,git pull 的第 2 步是 merge,此时需要的操作和之前的分支管理是一样的。在处理好合并内容,更新,提交到本地版本库以后,进行 git push 即可:

$ git commit -m "fix env conflict"
$ git push origin dev