背景

本文旨在让0基础的人快速掌握Git的操作,方便快速进入工作的状态。

我个人的观点是:单独抽出时间研究Git是不高效的并且也是脱离实际的,经历过实际使用才能更好更快的理解Git。所以,本篇文章主要涉及实际使用中的场景,并不会系统的介绍Git或者说讲解Git的原理。

Git单分支开发

1.从拥有一个git管理的仓库开启

Android开发的同学一定很熟悉Android项目,那我们就用它来做为我们操作的对象。

我的操作是在桌面创建一个新的Android项目,然后在终端中进入该项目目录中,使用如下命令初始化仓库。

1
git init

此时,我们只是做了一个初始化的操作,当前目录下所有的文件都是没有被跟踪的,我们可以使用如下的命令查看文件状态。

1
git status

img

我们逐一解读 git status 后出现的输出:

  • On branch master:当前分支处于master分支

  • No commits yet:还没有依次提交过

  • Untracked files:工作目录中没有跟踪过的文件,红色的部分就是我们没有被追踪的文件

2.尝试第一次追踪文件

我们初始化仓库后,所有的文件都是未跟踪的,那么如何跟踪文件呢?不知道你们有没有注意到使用git status后输出信息中的一些提示呢?

1
(use "git add <file>..." to include in what will be committed)

根据Git的提示我们知道了跟踪文件的命令如下:

1
git add <file>

比如说我想追踪 app/ 下所有文件,那么我们就可以这样写命令:

1
git add app/

一个一个的添加命令看起来有点蠢,如果我们要追踪所有的文件,要一次一次的add吗?大可不必,我们可以使用以下的命令将所有的文件设置为追踪状态:

1
git add --all

如果我们将所有的文件设置为追踪状态,使用git status查看文件状态又会出现什么呢?

img

类比之前提示追踪文件的命令的结果,我们可以得出如何取消追踪一个文件的命令:

1
git rm --cached <file>

3.尝试第一次提交文件

我们追踪过文件后,Git提示我们 Changes to be committed,那我们该如何提交呢?

我们使用如下的命令进行提交:

1
git commit -m "本次提交内容的总结"

我们commit的时候可以看到具体的改变情况: 41 files changed, 939 insertions(+)

img

我们使用git status再次查看文件状态发现,nothing to commit, working tree clean表明我们什么东西是可以提交的,working tree也是干净的。

4.尝试此时修改文件后会发生什么

我的修改内容如下:

img

img

此时我们使用git status查看文件状态会发生什么呢?

img

Git提示我们有两个文件被修改了,它们还没有被暂存。并且Git告诉了我们如何更新我们修改的文件:

1
2
//此时我们又知道了git add的一个功能
git add <file>

以及如何放弃工作目录中的修改:

1
2
3
4
//新版本的命令
git restore <file>
//旧版本的命令,使用新版本是为了分担git checkout承担的压力
git checkout -- <file>

如果我们想要仔细查看具体的改变,该怎么做呢?我们可以使用下面的命令查看非暂存区相对于暂存区的改变(按任意键退出查询状态):

1
git diff

img

我们此时将修改的文件更新到暂存区,查看文件状态如下:

img

通过Git的提示,我们看到了如何取消我们刚才暂存的文件

1
2
3
4
//新版本命令,为了分担git reset命令过多功能的压力
git restore --staged <file>
//旧版本命令
git reset HEAD <file>

那,如果我们此时再修改文件会发生什么呢,比如我们将text.text = “你好”修改为text.text = “你不好”,此时查看文件状态如下:

img

我们看到MainActivity同时出现在暂存区和非暂存区,这是怎么回事?我们暂存区的文件实际上是你执行git add的时候文件的状态,由于有了新的改变所以又出现在了非暂存区。我们执行git commit是提交暂存区中的数据。

5.理解Git的管理过程

首先我们要知道,我们的工作目录中有很多文件,每个文件又会有很多种状态。所以,我们第一个需要理清的东西就是文件的状态。

仓库刚初始化的时候,文件是“没有跟踪的-Untracked”,使用“git add ”命令会成为“跟踪的-Tracked”。只有跟踪的文件才是受Git管理的。

对于暂存区,非暂存区的理解,就是以 Git 的思维框架(将其作为内容管理器)来管理三棵不同的树。 “树” 在我们这里的实际意思是 “文件的集合”

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。

用途
Head 上一次提交的快照,下一次提交的父节点
Index 暂存区,预期下一次提交的快照
Working Directory 我们工作目录中跟踪的文件

我们以Pro Git的讲解来理解:

假设我们进入到一个新目录,其中有一个文件。 我们称其为该文件的 v1 版本,将它标记为蓝色。 现在运行 git init,这会创建一个 Git 仓库,其中的 HEAD 引用指向未创建的分支(master 还不存在)。

img

此时,只有工作目录有内容。现在我们想要提交这个文件,所以用 git add 来获取工作目录中的内容,并将其复制到索引中。

img

接着运行 git commit,它首先会将索引中的内容保存为一个永久的快照,然后创建一个指向该快照的提交对象,最后更新 master 来指向本次提交。

img

此时如果我们运行 git status,会发现没有任何改动,因为现在三棵树完全相同。现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本,并将它标记为红色。

img

如果现在运行 git status,我们会看到文件显示在 “Changes not staged for commit,” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add 来将它暂存到索引中。

img

此时,由于索引和 HEAD 不同,若运行 git status 的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交与上一次提交不同。 最后,我们运行 git commit 来完成提交。

img

现在运行 git status 会没有输出,因为三棵树又变得相同了。

切换分支或克隆的过程也类似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照,然后将 索引 的内容复制到 工作目录 中。

6.查看我们的提交

现在让我们回到我们之前的GitTest项目,当前的文件状态如下,我们添加到暂存区后然后提交。

img

可以用下面的命令查看提交:

1
git log

上面的命令还有各种各样的花样,但是查看提交我们可以直接用SourceTree可视化工具查看:

img

7.小小的总结

我们用一张图总结一下我们学习的过程:

img

8.关于reset命令的奥秘

通过总结我们发现,我们似乎少了什么东西,我们好像没有讲到如何放弃一次提交?这就需要讲解reset命令了。

在讲解reset命令之前,我们先弄懂一个小知识点,git中HEAD^和HEAD~的区别?

“^”代表父提交, 如果一个提交有多个父提交,可以通过在后面追加数字,表示第几个父提交。

“^”默认是“^1”表示第一个父提交。

”代表连续的父提交,“”默认是“1”表示父提交,“2”则是父提交的父提交。

首先我们需要明白撤销提交命令什么:

1
git reset --[soft/mixed/hard] HEAD~

有三种参数供选择,soft,mixed,hard,那我们接下里就依次讲解。

为了演示这些例子,假设我们再次修改了 file.txt 文件并第三次提交它。 现在的历史看起来是这样的:

img

让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。

① 移动 HEAD

reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上),运行 git reset 9e5e64a 将会使 master 指向 9e5e64a。

img

无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset –soft,就只会做到这一步。

本质上是撤销了上一次 git commit 命令。

② 更新索引

如果指定 –mixed 选项,我们不仅会移动 HEAD 的指向,而且会取消取消暂存所有的东西。

img

本质上是撤销了上一次 git commit 命令 + git add命令。

③ 更新目录

如果指定 –hard 选项,我们不仅会移动 HEAD 的指向,取消取消暂存所有的东西,而且会改变工作目录中的内容。–hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。

img

9.单分支开发总结

在掌握了上面这么多内容的学习后,我想单分支开发已经难不倒我们了。

Git多分支开发

1.远程仓库的引入

直接在Github上创建仓库,主要是获得SSH(例如:git@github.com:xiaoshitounen/GitTest.git)

img

给我们的本地仓库,添加远程仓库,可以使用下面的命令:

1
git remote add <远程仓库的简称> <SSH>

想查看我们本地仓库和多少个远程仓库有联系,可以使用下面的命令:

1
git remote -vv

从下面的图片中我们可以看到,我们添加的远程仓库命令是:

1
git remote add origin git@github.com:xiaoshitounen/GitTest.git

通过添加远程仓库的时候给与远程仓库一个简称,这样我们之后想要指定某一个远程仓库的时候,可以直接使用origin来代替 “git@github.com:xiaoshitounen/GitTest.git”。

直接git push将本地仓库的内容推送到远程仓库会失败,因为本地仓库的分支没有和远程仓库的分支建立映射关系。Git不知道你想要将当前所在分支的内容推送到远程仓库的哪一个分支。

如何将本地分支和远程分支建立关系呢?可以使用下面的命令建立联系(如果没有对应的远程分支,会创建出来):

1
git push --set-upstream <远程仓库简称> <本地仓库的分支>:<远程仓库的分支>

如果是将当前本地所在的分支和远程某一个分支建立关系,可以使用下面更加简洁的命令:

1
git push --set-upstream <远程仓库简称> <远程仓库的分支>

从下面的图片中我们可以看出,我们使用了下面的命令将本地master分支和我们远程仓库master分支建立了联系,之后使用get pull就可以将本地master分支的内容同步到远程仓库的master分支。

1
git push --set-upstream origin master

img

同步代码成功之后,就可以在Github上面的仓库中看到下面的内容了。

img

如果我们想知道我们本地仓库添加了多少个远程仓库,我们可以使用下面的命令查看我们添加的所有远程仓库的列表

1
git remote -vv

2.建立映射关系的好处

本地分支和远程仓库的分支建立映射关系是很方便的。

首先我们先明确几个命令:

  • git push:推送本地分支的内容到远程仓库的分支

  • git fetch:拉取远程仓库某个分支的内容到本地

  • git pull:拉取远程仓库的某个分支的内容到本地,并且将内容合并到本店仓库分支

通过比较这三个命令我们可以知道,我们在本地使用这三个命令的时候,如果不建立映射关系,我们每次使用都需要指定具体是哪一个远程仓库,是这个远程仓库的哪一个分支。

但是,一旦我们建立了联系之后,我们就可以直接使用git push,git fetch,get pull,因为我们已经知道了是和远程仓库的哪一个分支进行交流的。

3.多分支

我们首先肯定是想知道我们现在有多少分支?我们可以使用下面的命令查看本地的命令:

1
git branch

继续使用上面的GitTest项目,我们可以查看我们的分支如下,我们可以看到我们只有一个名为master的分支,并且我们的分支前面有一个*,表示这是我们当前所处的分支。

img

不仅可以查看本地分支,我们还可以查看远程分支,使用下面的命令,我们就可以查看所有的分支列表:

1
git branch -a

img

那么,我们该如何创建一个分支呢?我们可以使用下面的命令创建分支:

1
git branch <name>

比如我们使用命令git branch test创建分支后,查看分支列表就可以看到下面的内容:

img

这个命令是基于当前分支的内容进行创建的,所以我们如果想要在某一个分支基础上开发,我们需要切换到对应的分支,我们可以使用下面的命令切换分支:

1
git checkout <name>

比如我们使用命令git checkout test切换分支,查看分支列表发现*对应的分支果然改变了

img

有时候我们想创建新分支的同时切换到对应的分支,可以使用下面的命令:

1
git checkout -b <name>

既然我们可以创建分支,那我们肯定也想删除分支,使用下面的命令就可以删除分支(注意:不能删除当前所在分支):

1
git branch -d <name>

4.分支之间的合并

1> merge

我们先来看一次简单的merge情况,我们新开一个分支test,修改内容如下。

img

我们修改后,先查看文件状态,发现我们修改的文件果然变成了“modified”状态,我们使用git操作依次添加到暂存区,然后将当前的修改做一次提交。

之后,我们切回到master分支,我们现在将test分支的内容同步到我们master分支中,可以使用命令:

1
git merge test

上面的命令是将test分支的内容同步到当前分支。

img

由于,test分支是从master分支切出来的,所以我们的test分支修改后我们本地仓库的内容大致如下:

img

如果我们把这三次提交拉成一条线,那么master分支到test分支就被拉成一条线可以直达,所以这种类型的合并就是直接移动msater指针到test指针的位置。merge的时候,我们也会看到Fast-forwad这样的提示。

此时,远程仓库不知道我们本地仓库master分支的内容发生了变化,而我们的本地分支master和远程仓库分支的master分支是建立过映关系的,所以我们直接使用git push将本地master分支的内容推送到远程仓库的master分支中。(注意:你本地分支一定要在master分支再执行git push操作)

推送前:

img

推送后:

img

但是,大部分的合并都不会如此的轻松。我们试着先在master分支做一些修改,然后到test分支做一些修改,最后切回到master分支,将test分支的内容同步到master分支中。

master分支的修改内容如下:

img

这个时候,我们提交一次,具体操作情况如下:(某些文件的修改不是我们做的,所以我们放弃修改.idea/gradle.xml文件)

img

img

之后,我们切换到test分支,修改的内容如下:

img

这个时候,我们提交一次,具体操作情况如下:

img

img

这个时候,如果我们将test分支的内容合并到master分支,发现无法像上面合并的过程一样直接Fast-forwad。这个时候,git的操作是将test分支和master分支以及它们的共同祖先做一个简单的三方合并。

由于这里我们修改的是同一个地方,会出现合并冲突,具体解决冲突的情况请往下看会有视频操作。

2>rebase

rebase部分我还没有实际使用过,这里参考公司文档的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git rebase <BRANCH_NAME>
rebase作用如下图所示:
--------------------------rebase示例图-----------------------------
A--B topic(*) A'--B' topic(*)
/ --git rebase master--> /
D--F--G master D--F--G master
-------------------------------------------------------------------
简述:rebase的作用
如rebase示例图,在topic上对master做rebase后,topic分支就成为master分支的完全超集了。这样,再在master上merge分支topic时,就会出现fast-forward(ff),如下图
------------------------------ff示例图-------------------------------
A'--B' topic A'--B' topic
/ --git merge topic--> /
D--F--G master(**) D--F--G--A'--B' master(**)
---------------------------------------------------------------------
保证主分支在合并时ff的好处显而易见:得到一个线性增长的主分支。

5.不想直面的冲突

首先安利一个工具:p4merge,下载地址:https://www.perforce.com/downloads

下载之后需要配置一下,参考配置地址是这个:https://gist.github.com/tony4d/3454372,直接复制下面的四条命令,依次执行下去。

1
2
3
4
5
6
7
8
$ git config --global merge.tool p4mergetool

$ git config --global mergetool.p4mergetool.cmd \
"/Applications/p4merge.app/Contents/Resources/launchp4merge \$PWD/\$BASE \$PWD/\$REMOTE \$PWD/\$LOCAL \$PWD/\$MERGED"

$ git config --global mergetool.p4mergetool.trustExitCode false

$ git config --global mergetool.keepBackup false

首先我们在上面的过程中了解到了merge过程产生冲突的情况,接下来我们在装了p4merge的情况,就可以使用下面的命令调用可视化工具p4merge来解决冲突:

1
git meregtool

刚才操作的视频演示:

显示三个框框的内容,左边的内容是我们想要同步的分支的内容也就是我们这里的test分支,右边的内容是我们的本地分支的内容也就是我们这里的master分支,中间的内容是它们的共同内容。

下面框框的内容就是我们最终合并的内容,合并之后,保存即可。我们这里合并就是保留master方法去除fun方法。你可以再次运行 git status 来确认所有的合并冲突都已被解决。如果你对结果感到满意,并且确定之前有冲突的的文件都已经暂存了,这时你可以输入 git commit 来完成合并提交。

我们可以使用SourceTree可视化工具清楚的看到,我们的确是合并了。

img

如果,你正在合并中,那么你可以使用下面的方法来结束合并:

1
git reset --hard HEAD