这是一篇成为 git 高手的文章

6,015 阅读20分钟

引言

首先为什么要写这篇文章?因为最近总是遇到有人在问一些关于代码管理的问题,比如怎么从 仓库拉代码,上传代码,也有完全没用过代码管理系统的。因此这篇文章是结合这几年来使用 git 团队协作或个人的代码管理的使用心得。

那 git 是什么呢?git 是一个分布式代码管理系统,可能很多人之前用过 svn 这样的代码管理系统。不过 git 的特点是分布式,以及更加便捷的协作方式。git 的使用也是十分广泛,比如程序员交友圈 github, gitlab, gogs 等等代码托管平台都是使用 git 作为代码管理工具。

必须了解的知识

SSH加密认证原理: SSH(Secure Shell)是一种非对称加密与对称加密算法相结合的安全网络协议,用于计算机通信加密。一个SSH会话的建立过程分为两个阶段:第一阶段,双方沟通并同意建立一个加密连接通道以供后续信息传输用;第二阶段,对请求接入的用户进行身份验证以确定服务器端是否要给该用户开放访问权限。详细的文章请参考理解SSH的加密与连接过程

使用方式

首先,如果主机本地没有建立秘钥对,就需要生成。生成方式:

当然你可以使用默认的方式,一路回车下去即可。或者也可以指定名称。例如 test,一路回车即可。

在当前目录下会看到生成的秘钥对如下,其中带后缀.pub 为公钥,没有后缀的为私钥。

这样就生成好了秘钥对,切记不要将私钥随意上传到能被网络访问、下载。个人的做法是用一套密钥对来管理所有的终端,比如云主机的 ssh 登陆方式。

这里以 github 举例,你将你的公钥拷贝后,登入 github, 然后找到 Settings -> SSH and GPG keys。 如图的所示的地方新建并粘贴。

点击进入,如图所示:

当搞定这个了就可以愉快的在 github 代码托管仓库创建你的项目了,这里我就一个测试的项目讲解我的使用过程。第一步新建项目:

点击进入,填写项目名称以及项目的访问方式, 这里我以公开访问方式,也就是所有人都可以访问这个项目。

新建项目完成后第一个要做的就是将这个项目 clone(克隆) 到本地主机。

然后在终端中输入, 这是第一个命令。

git clone git@github.com:huangxiaojingCN/LearningGit.git

在终端命令行中输入: ls -al 查看, 可以看到有一个隐藏的目录文件 .git, 在这个文件下就是记录这个代码仓库的所有信息,其中也有很多功能可以用以我们对代码的自动化,代码检查等等。

接下来是第二个命令:git add 文件

// 创建一个新的空白文件,你可以使用文本编辑器打开并编辑内容,也可以使用 echo "This is a a.txt." >> a.txt
touch a.txt

// 将字符串 "hello world" 重定向到 a.txt
echo "This is a a.txt" >> a.txt

// 通过 git add a.txt 文件加入到本地代码管理
git add a.txt

当然你也可以使用 git add -p 命令来提交文件中的添加记录, 前提是这个文件以及在本地代码管理库中存在。稍后再说这个用法,非常实用。

第三个命令: git status, 查看当前代码库的状态

git status

不难看到,我们发现刚才执行了 git add a.txt 的命令被加入到了缓存区,即将要被提交 commit,当然你也可以删除它,执行 git rm --cached a.txt。

第四个命令: git commit -m "提交消息",只有在执行了 git add 将其加入到本地缓存区后才能使用 commit, 将其加入到本地代码仓库。

git commit -m "添加了 a.txt 文件,并加入了 hello world 文本内容."

从上面的结果显示中可以看到,a.txt 被正确创建并加入到了本地代码管理库中。接着通过前面的命令 git status 查看下当前的代码库状态, 可以发现已经没有文件要被提交。

经过上面的两个步骤:git add、git commit 就已经代码加入到了本地代码仓库中,通过 git status 辅助查看本地代码仓库的状态。 这三个命令构成另一次本地代码的的提交,比较基础但是非常常用。

第五个命令: git log, 从名字不难看出就是要看看我们本地代码仓库的日志。

git log

看到上面的截图是不是就发现,我们刚才提交的信息也被展现出来啦!没错,这就是告诉你文件被正确提交并加入到了本地代码仓库中。可能大家发现了有一个 HEAD-> master,你只需要记住,当前 HEAD 所在的地方就是你的代码的版本。如果你发现你的截图跟我不一致,没有 Author 那是因为还没配置,这个后面再说,先说完整个 git 使用流程。

现在再来修改下 a.txt 的内容: 比如我们加入其他文本内容如下, 笔者使用的 vim 编辑方式,当然你可以使用你顺手的,比如编辑器等等。

编辑完后引入新的 git add 带参数的方式:

git add -p

请你仔细看, 从第一行开始: diff --git a/a.txt b/a.txt 就是比较原来的 a.txt 和现在更改后的 a.txt 有啥区别。 绿色的 "+" 号表示新添加的,红色的 "-" 号代表删除的内容。最后一行方框中有选择[y, n,q,a,d, /, e, ?],然后让你填写一种,一般我们使用最多的就是: y(yes)、n(no)、q(quit)。 这里我们写入 y 后回车。

不知道还记不记得前面提到的 git status, 用来查看当前本地代码仓库的状态。你看出了什么变化吗?😄😄 前面添加的文件如果是新的, 那么看到的就是 new file, 如果是修改的是 modified。这也很好理解嘛!

那下一步我们是不是又要提交啦!继续 git commit -m "提交信息"

然后查看一下 git log 日志吧!嗯嗯,是不是又看到了新修改的 a.txt 文件啦!

为了巩固前面的学到的知识,我们将继续新建一个文件, 巩固 add、commit、status、log 的用法, 来来来

touch b.txt

echo "This is a b.txt" >> b.txt

git add b.txt

按照管理,看看本地代码库的状态: git status

ok! 翻译一下吧!说的就是告诉你这个文件未被追踪,也就是未被加入本地代码仓库中。你可以使用 git add file 的方式去添加。那就执行 git add b.txt, 然后通过 git status 看看。 是不是很熟悉啦!但是我写到想吐😄

到了这人我们是不是就要提交了,从它的结果中也是可以看出的。继续执行:

git commit -m "添加了 a.txt 文件,加入了 This is a b.txt 文本内容."

这个结果也是前面见过的,你应该有感觉了吧!这里告诉我们的是 b.txt 也被加入到本地代码仓库中。

快快使用 git status 和 git log 来瞅瞅吧!大佬!

又一个文件被添加到本地代码库管理了。但是这里你看看 log 的方式是不不够清晰,我们能不能看看它的生长过程。这个时候我们就要为 log 加上一些参数啦!

// --graph 图形化展示
// --decorate 查看当前提交的 commit 信息和其他的描述信息
// --all 查看所有的分支,分支的概率后面再说。
git log --graph --decorate --all 

看到区别了吧! 是不是旁边多了一条线,这个就好比我们物流时间轴。后面的肯定是包含前面的过程,在这里就是后面会包含前面提交的文件。比如当前 HEAD 所在的地方,执行 ls,可以看到即包含了 a.txt 又包含了 b.txt。

梳理一下前面的过程: 其无非核心的就是: add -> commit, 辅助 git status, git log 来完成提交到本地代码库的操作。

接下来就开始往远程仓库推代码和拉取代码啦!首先先来解释下前面总是提到的本地代码仓库,这个含义就是你的服务端代码仓库可能不包含你本地新建的文件,很简单嘛!你没有把信息同步给别人,别人是不知情你的本地的变化的。在这里我就引入远程代码仓库。接下来就是和远程仓库的交互。

再回顾下前面 log 日志打印内容, 你会发现有一串很长的数字。它代表了当前的提交的唯一标示,即我们可以通过它找到任意一次提交的代码。

第七个命令: git push。 push 的单词意思就是推送,即客户端向远程仓库推送代码。那 origin 又是什么? 这个其实是远程仓库的名字而已,它等效于前面克隆的地址。你可以通过 git remote show origin 来查看。

git push origin HEAD:分支

从打印可以看到,Push URL: 即我们要推送的地址,而 Fetch URL 就是我们要拉取代码的地址。

好吧! 那我们执行提交代码的命令看看效果。

// HEAD 即当前所在的分支
// : 冒号左边是本地分支, 右边是远程分支, 那这里的 huangsanyang/add_a_and_b 即为远程的分支名称
// 完整的理解就是: 将本地 HEAD 的分支推向远程仓库名为 huangsanyang/add_a_and_b 的分支
HEAD:huangsanyang/add_a_and_b

使用 git log --graph --decorate --all 查看日志,这下多了点区别,可以看到当前 HEAD 所在地方,右侧还有一个红色的 origin/huangsanyang/add_a_and_b, 这就是告诉我们这是一个远程分支, 从前面的 origin 就可以看出。

再次回顾: 一次完整的推送代码到远程仓库的流程为: add -> commit -> push。 再次 看看 github 代码仓库,是不是已经包含了我们的提交啦!说明已经正确将本地的代码仓库提交到远程代码仓库。相当于做了一次远程备份。

到了这儿,我们就将从零新建项目到推送代码到远程服务器仓库的流程啦!

拉取代码和多人协作一起讲解,现在假定你是项目组长,你拥有这个代码仓库的所有权,你可以删除、指定谁能协助项目开发、审核代码、合并代码。

这里我掏出了我自己使用的电脑,作为一个项目管理者的方式参与到协作开发中。因为我也是第一次克隆,各位请注意这是新的人员。所以你不是一个人在战斗....

老生常谈:查看一下 log 日志吧! 是不是和前面的 push 后一样。

接着我开始开发我的功能,比如我新建了 123.txt

touch 123.txt

echo "123456" >> 123.txt

git add 123.txt

现在你还会忘记要干嘛吗?如果忘记就该撞墙去。 git commit -m "添加了 123.txt "

再来看看 log, 是不是当前的 HEAD 指向了 huangsanyang/add_a_and_b, 这个也就是当前的本地分支啦!你还会看到后面一个分支有一个 origin/HEAD, 这个告诉我们远程服务器的 HEAD 分支比我们本地刚才提交分支少了一个 123.txt 文件。也就是说前面的开发者是没法拿到 123.txt, 那如果要拿到该怎么办呢?

继续提交吧!这一次提交是项目管理员,也是另一个协作的开发者提交的代码。

别慌,苟住。继续来看 log 日志。这里我提交另一个新的文件 123.txt, 推向了远程服务器分支 huangxiaojing/add_123。

OK, 让我们回到前面的开发者,看他怎么获取最新的代码。这就很自然的过多到了 git fetch

git fetch --all 

sorry,可能仔细的你发现,我是用的是 git fetchall 而不是 git fetch --all, 那是因为我使用别名的方式简化了拉取指令。别名在后面的 git 补充篇讲解。

来看 log 日志,是不是拿到最新的代码了呢? 也就是管理员作者提交的代码 123.txt。

可以看到分支确实能看到了吧!不过你通过 ls 查看并没有发现我们想要的 123.txt。 那是因为你没有留意的你本地的 HEAD 指向的是哪里。可以看到当前的 HEAD 是要早于 123.txt 之前提交的。所以肯定没有的。那怎么办?

啊哈!我们就来看看怎么把分支切换到最新的版本上去。引入了新的命令:


// checkout 切换分支
// 分支名: huangsanyang/fetch_123_txt,当然也可以不指定,最好还是带上。这样更好,更清晰
// 指定分支的哈希值: 这个前面说过,用来确定唯一的提交记录。
git checkout -b 分支名 指定分支的哈希值

来看 log 日志, OK! 切换过来了吧!通过 ls 看看

经过上面的过程,你已经可以和同事进行代码交换啦!你可以拿到同事的代码,通过 fetch -> checkout。但是这样其实也不是很好,我们的代码仓库应该要有一个好的规范,在所有知名的 git 代码管理的项目中,都会通过维护一个 master 分支来保证稳定的版本。 master 分支应该遵循的原则是,一定要保证能编译通过,能正常部署运行。根据实际情况也可以增加 developer 分支来表示开发的过程的分支。那这里是不是就出现了两条分支了,一个是开发分支,一个是发布分支。 这个时候项目管理员的作用就来了,他务必保证 master 分支的代码一定是所有开发者的代码合体、保证代码稳定运行、代码质量审查。

接下来就要引入稍微高阶的内容:代码合并、审查。

现在回到项目管理者,他首要的就是要保证代码的 master 主干分支是最新的。

很显然不是,因为我们并没有看到 origin/master 是否在最后一次提交的分支。 这里先执行 git push origin HEAD:master 创建出远程 master 分支。

由于没有新的内容需要条,所以可以看到 Total 为 0。接着看看 log 日志,各位再次强调一下,善于使用 log 能让我们清楚知道当前代码库的状态。

现在看到 origin/master, 我们已经创建出这样一个用于项目发布的分支。因此作为管理员就要维护这个分支。接下来演示一次合并请求。也就是其他的开发者,编写了代码提交到远程非 master 的分支。开发者可以设置远程 master 分支为保护分支,那么别的开发者就不能往 master 推送代码。

现在切换到普通开发者,编写一个文件 utils.txt

touch utils.txt

echo "这是一个描述项目中使用工具的文件" >> utils.txt

git add utils.txt

这里就不再提供 git status 打印, 读者自行使用查看。通过 commit -> push 如下:

可以看到,开发者已经提交了新代码并且提交了指定分支 origin/huangsanyang/push_utils。 可以看出这样的提交代码风格非常好。因为让项目管理者,可以快速清晰的知道他干了什么。

让我们切回到项目管理者,来看看他是如何合并代码的。请务必记住,一定要先执行 git fetch --all

git fetch -all

再看 log 日志

到了这一步其实也是前面都用到的知识,知识不断的软磨硬泡,旨在让小白变成一个真正的高手。当然如果你看完不练习,当我没说。

在合并的时候,开发者应该要将分支切换到远程 master 所在的地方,然后再合并别的开发的代码。为什么要这样,因为是将别的开发者代码往 master 合并,而不是把 master 往开发者合并。要分清那个分支才是最重要的分支。因为开发者写的功能也许有问题。所以一定要注意。

合并的命令:

// merge 合并分支
// --no-ff 不要强制解决冲突,由合并者自行处理
git merge --no-ff 指定合并的分支

执行如下的合并指令:

git merge --no-ff c17c8f7e46b666ab84f3ba5b99efc57c1711baed

最终的 log 截图, 请你仔细看看,分支走向发生了变化。可以看到有一个分支合并到了 5df2ffb37f131f93fc1db775e1a7dd36674814c9 分支。

如果确认无误,经过严格测试以后符合功能需求,再将其推送到远程 master 分支。

再查看 log

好了,正常合并流程已经讲完。但是你想想,既然要成为一个高手,如果不去解决点冲突,这能符合高手的常规操作吗?

营造一个冲突环境,比如管理员开发的某个文件的一些代码被另一个开发者改掉,那么开发者最终提交给管理员的代码和原来管理员的代码肯定不同的。这样是不是就要处理冲突了。

回到开发者,记得 fetch, 记得 checkout 切换分支。 然后修改一下 123.txt 文件, 可以看到他将新增了一些文件,并且修改了原来 12345 改为 1234 sdf6。然后提交。

1234 sdf6


故人西辞富士康
为学技术到蓝翔
蓝翔毕业包分配
尼玛还是富士康


来看看 log 日志,可以看到已经正确提交啦!

这个时候项目管理员就来合并代码啦!记得 fetch, 记得切换到 master 分支。

接着再来执行

git merge --no-ff 348a5e9987981d34ceea031c9cf442bd0aaa7c3c

😄😄,你会发现并没有冲突,哈哈这是不是很尴尬呀。 那是因为我们是在 master 上将开发提交的分支往 master 合并。 而开发者其实是包含了 master 的所有文件和记录。因此这次合并是正常的。其实我就是为了再一次巩固一下合并代码的流程。

其实冲突是多个开发者之间提交的代码中可能都修改同一个文件,且各自的代码都不一致。这个时候项目管理者就要做权衡,选择最好的结果。举个例子,现在代码管理者对 123.txt 进行修改后提交,普通开发者也修改了 123.txt 并提交。

先来看看普通开发者的修改,请注意一定要拉取代码 fetch, 然后 checkout。再次巩固这个两个命令的使用:

// 拉取远程代码仓库的所有分支
git fetch --all

// checkout 切换, branc_name 有意义的分支名, hash: 当前分支的唯一标示
git checkout -b branch_name  hash

然后使用文本编辑修改内容为:

111111111111111

asdfasdfsa;dlf
故人西辞富士康
为学技术到蓝翔
蓝翔毕业包分配
尼玛还是富士康

然后提交和推送代码到远程仓库。再次回顾命令的使用:

// 将其加入到本地缓存
git add 123.txt

// 提交代码到本地仓库
git commit -m "修改了 123.txt 内容"

// 将代码推送到远程仓库
git push origin HEAD

使用 log 查看,命令回顾:

git log --graph --decorate --all

不难看出,这一次提交要晚于远程代码仓库的 master 分支的。

接下来看项目管理者的修改, 请注意,这里并没有将分支切换到开发者提交的分支上,而是在原来的老的分支上进行直接的修改。

看到下面的 log, 我没有骗你,我确实是在老的分支上,而开发者新提交的代码并没有合并到 master

来看项目管理者修改内容变成了什么,是不是和之前的完全不一样,并且将之前的全部改掉啦!

接着又是熟悉的步骤, add、commit、push

OK, 提交成功后,尝试合并。并将最终分支合并到 master。

合并的命令回顾:

git merge --no-ff 被合并的分支

看到执行 merge 后的打印了吗?

// 告诉我们尝试自动合并失败,需要手动合并解决冲突
Automatic merge failed; fix conflicts and then commit the result.

OK, 这里你应该选择你最使用方便的编辑器来进行代码的合并操作,比如你可以使用 AS 自带的 git 插件。笔者这里使用的是 Emacs 的 git 插件。下面是本次合并信息,可以看出来是分段的,第1、2 行表示当前的分支,3、4两行表示要被合并的分支; 剩下的几行表示有哪些文件修改需要被合并。

我们进入到未合并的文件 123.txt, 可以发现当前 HEAD 就是管理员提交的代码,而下面的就是要被合并的开发者提交的代码。

这里我就直接选择管理员的代码作为最终的代码,在实际开发中,审核着就是要承担这样的责任,需要合理的选择谁提交的代码可用,或者抽取各自好的代码进行合并。

最终选择的结果,可以看到我删除了开发者的代码,并将辅助的信息全部删除。比如有:

>> 开头的

<< 开头的

==== 开头的

合并完成后又进入到熟悉的步骤, commit、push。看到下面的图,可以看到分支又被何在一起,你可以自己好好观察下分支走向。那些分支合到那个分支上,只要看懂了,遇到再复杂的分支也不会晕头转向。

好啦, 写到这儿就将整个 git 的常规操作全部讲完了。让我们来再一次回顾一下,因为内容太长,为了让读者能熟悉命令,看完后能记住大部分。我给你再总结一次。

写在前面: 如果你的仓库已经存在提交,你第一步就需要 fetch, 然后切换到最新的版本上去,一般是 master 所在的分支
git fetch --all

2. 切换到指定 hash 的分支
git checkout -b branch_name hash

3. 新建一个文件后,需要将其加入到缓冲区,称为追踪文件
git add xxx

4. 将缓存去的文件加入到本地代码仓库
git commit -m "提交消息内容"
 
5. 提交到远程仓库
git push origin HEAD:branch_name
 
辅助命令:
1. 查看当前提交的状态
git status
 
2. 查看日志,分支树
git log --graph --decorate --all

管理者使用:
1. merge 合并代码,将指定分支合并到某一个分支,一般是 master
git merge --no-ff hash

其实上面的是最常见的几个命令啦!基本上熟练使用这几个指令就能完成大部分工作。笔者用的最多也是这几个命令。下一篇作为补充,再增加几个命令,虽然不常用,但是也要知道。