手把手教你学Git

引言

这是一篇简单的入门级文章,旨在让从没有使用过git的人群能够明白git的初步用法,以及git用于工作的基本思想。看完本文后,希望大家能够上手实现基本功能的操作。只要入了门,再进阶就有章可循了。

思虑再三,决定采用轻小说的形式。以一个虚拟项目从初始化构建到代码上传、合并、解决冲突等实现开发协作。

学习目标与设定

git本身就是一个很复杂的工具了,它每个命令中都有多项参数。如果要穷尽学习所有命令与参数之后再来工作,那显然时间成本太高。所以本文以工作实用为目标,尽力做到一边学习一边就能上手用于实际工作。

  • 掌握核心用法;
  • 能够上手实际工作;
  • 通过内置帮助查找命令说明。

人物设定

老马:资深码农,作风沉稳,五年以上工作经验,技术负责人。

小蔡:新晋码农,求知欲强,基础扎实。

起步:安装与配置

公司最近决定将源码管理系统从SVN迁移到git上,于是就由老马来牵头整这个事。老马自己找资料、试用,又琢磨了一番,觉得差不多了,就招呼道:“小蔡,你过来一下,咱合计个事。”

小蔡立马应道:“来了,马哥,啥事?”

小蔡刚毕业不久,在公司已经结束实习期,签了正式合同。他一直跟着老马做事,因为小蔡专业基础扎实,反应机敏,老马带着不累,因此也越来越倚重他。

老马慢条斯理地说道:“听说过git这个工具吗?”

“知道,是个分布式源码管理系统。我同学他们公司就在用这个呢。马哥,咱公司也要用上这个了?”

“嘿,你小子脑子转得倒快,省得我跟你废话了。现在咱们要趟条路出来,才好给其他兄弟们做演示。那你自己玩过没?”

“这个真没有,马哥。要不我现在就去搭个demo环境试试看?”

“行,最简单的就可以,纯演示用。那些个安全性、管理界面的都不用考虑。”

“明白,马哥。”

小蔡回到工位,通过ssh终端连接上公司内网centos linux服务器,他决定使用yum工具安装git。

 $ yum search git
 $ yum install git.x86_64
 $ git version
 git version 1.8.3.1

至此,系统里已经有了git命令,接下来他又搜索了一下初始配置方法。于是执行以下命令:

 $ git config --global user.email "macai@vision.com"
 $ git config --global user.name "macai"
 $ git config -l
 user.email=macai@vision.com
 user.name=macai

做完这些,小蔡就跟老马碰了一次头。老马表示基本满意,接下来就是如何新建仓库并提交代码了,老马示意小蔡把椅子挪过来,他打算两个人一块看着弄。

上手:初始化与提交

老马:“小蔡你看,项目版本库所在目录通常称之为工作区。我们先初始化一个工作区吧。”

 $ mkdir vision_prj
 $ cd vision_prj/
 $ git init
 初始化空的 Git 版本库于 /home/macai/web/vision_prj/.git/

小蔡:“马哥,可我看目录下什么也没有啊。”

老马:“你仔细看,其实有一个.git隐藏文件夹。仓库相关的配置、版本管理文件都在那里面。”

小蔡:“果然如此。”

 $ ls .git/
 branches  config  description  HEAD  hooks  info  objects  refs

老马:“接下来我们放一个文件进去,再添加到仓库里。最后看下状态吧。”

 $ vim vision_main.c
 $ git add .
 $ git status
 # 位于分支 master
 #
 # 初始提交
 #
 # 要提交的变更:
 #   (使用 "git rm --cached <file>..." 撤出暂存区)
 #
 #   新文件:    vision_main.c
 #

文件内容vision_main.c:

 #include <stdio.h>
 
 int main(int argc, char *argv[]) 
 {
     printf("hello, vision\n");
 }

小蔡:“嘿,马哥,这就说我们刚创建的文件已经被纳入版本管理了吗?这跟svn一样啊。”

老马:“不对,这里就跟svn有差别了。在git里这一步只是将文件放入了暂存区,还要经过一次提交才会被正式纳入git版本管理。当然,在暂存区中的文件是可以被撤销的,这个我们以后再说。”

 $ git commit -m "create vision_main.c"
 [master(根提交) d4fd427] create vision_main.c
  1 file changed, 6 insertions(+)
  create mode 100644 vision_main.c

老马:“对于刚完成的操作,我们可以通过查看日志命令来回顾。”

 $ git log
 commit d4fd427ec364c14cb84efc41d646a3189ccac5e1
 Author: macai <macai@vision.com>
 Date:   Tue Jan 14 11:08:28 2020 +0800
 
         create vision_main.c

老马:“小蔡你看,在commit后面跟着的那一长串值就是本次提交的哈希值。有了这个值,我们可以通过它做很多事情,例如比较各个提交之间的差异、打标签。甚至你不小心执行了误删除命令,都可以通过这个哈希值把内容恢复出来。”

小蔡:“这么神奇。”

老马:“那是当然。接下来我们要模拟一下分布式操作了。”

协作:分布式操作

老马:“我们先来实现第一个目标,就是你能够在自己的电脑上获取工程。这需要我先建立一个裸仓库。”

“什么是裸仓库?” 小蔡很自然地问了出来。

老马:“简单理解,就是不包含工作区的源码仓库。它可以追踪我们做出的提交变更,一般用来作为远程中心仓库。”

小蔡又冒出了另一个问题:“马哥,咱不是说要去中心化吗?怎么还是弄了一个中心出来呢?”

老马这回笑了:“哈,傻小子,你仔细想想。这个中心对源码管理来说是必须的吗?它相比于SVN式的中心服务已经大大弱化了,这个中心最大的用途是为了方便开发者远程协作,以及源码备份。假如这个中心出了故障,那从任意一位开发者手中的本地仓库就能快速重建一个。你说SVN能这么轻易地做到吗?”

小蔡茅塞顿开:“啊,是这个意思,我明白了。”

老马不再多话,在服务端运行了如下命令:

 $ git clone --bare ./vision_prj/ ./vision_prj_repo.git
 克隆到裸版本库 './vision_prj_repo.git'...
 完成。

老马:“好了,小蔡你回去把这个工程clone到本地吧。”

小蔡回到工位以后,按照老马的吩咐,在自己的电脑上运行如下命令:

 $ git clone macai@172.16.127.43:/home/macai/web/vision_prj_repo.git
 正克隆到 'vision_prj_repo'...
 The authenticity of host '172.16.127.43 (172.16.127.43)' can't be established.
 ECDSA key fingerprint is SHA256:d2UJyLYxdTOsdoHqubiNgWwbUHRcKwCKo9wyHx2MNMo.
 ECDSA key fingerprint is MD5:01:d6:df:44:01:c1:bc:46:d8:89:7f:06:bf:ab:28:bd.
 Are you sure you want to continue connecting (yes/no)? yes
 Warning: Permanently added '172.16.127.43' (ECDSA) to the list of known hosts.
 macai@172.16.127.43's password: 
 remote: Counting objects: 3, done.
 remote: Compressing objects: 100% (2/2), done.
 remote: Total 3 (delta 0), reused 0 (delta 0)
 接收对象中: 100% (3/3), done.

因为小蔡是第一次连接这台内网服务器,所以经过了一次权限确认的步骤。他一看本地果然有了一个vision_prj_repo目录,他迫不及待地进入目录,一看vision_main.c赫然在目,并且内容也分毫不差。小蔡笑着向老马打了个胜利的V字手势。

老马又埋头操作了一番,他添加了一些新代码,并执行了向远端仓库提交修改的push命令,操作如下:

 $ git push
 macai@172.16.127.43's password: 
 Counting objects: 5, done.
 Compressing objects: 100% (2/2), done.
 Writing objects: 100% (3/3), 322 bytes | 0 bytes/s, done.
 Total 3 (delta 1), reused 0 (delta 0)
 To macai@172.16.127.43:/home/macai/web/vision_prj_repo.git
    6c2473b..9162fd1  master -> master

然后老马向小蔡说:“你再运行一下git pull命令看看。这条命令的作用是从远端仓库同步最新数据至本地,它实际包含了两个步骤,fetch和merge。即拉取数据并且和本地修改提交进行合并。”

“另外,”老马想了想还是补充说:“diff命令可以查看前后版本的差异,这个你自己研究一下吧。”

小蔡依言操作,果然有了变化。

 $ git pull
 macai@172.16.127.43's password: 
 remote: Counting objects: 5, done.
 remote: Compressing objects: 100% (2/2), done.
 remote: Total 3 (delta 0), reused 0 (delta 0)
 Unpacking objects: 100% (3/3), done.
 来自 172.16.127.43:/home/macai/web/vision_prj_repo
    d4fd427..0dd7e62  master     -> origin/master
 更新 d4fd427..0dd7e62
 Fast-forward
  vision_main.c | 7 ++++++-
  1 file changed, 6 insertions(+), 1 deletion(-)

小蔡再一查看文件内容,原来是添加了一个新方法showMessage。

 #include <stdio.h>
 
 void showMessage()
 {
     printf("hello, vision\n");
 }
 
 int main(int argc, char *argv[]) 
 {
     showMessage();
 }

小蔡再查看一下日志,果然有了上次提交的内容:

 commit 0dd7e62b481bac323c5e1e4d2c8bf6c86458381d
 Author: macai <macai@vision.com>
 Date:   Thu Aug 6 15:09:34 2020 +0800
 
     add(vision_main): add showMessage

小蔡又在自己的本地代码中进行修改,然后提交。老马那边执行git pull一看,内容也都同步过来了。这一来一回,小蔡算是搞清楚怎么进行分布式协作了。

自动合并

老马招手又将小蔡叫了过去,开口问道:“小蔡你想过一个问题没有,就是假如我们同时修改这个文件并且提交,会发生什么事情?”

小蔡没有马上开口,而是思索了一会儿,说:“那就会产生代码冲突,要做合并的工作,对吧?”

老马笑了笑,说:“git在这方面还是比较智能的,它会根据时间线来判定先后关系。有些合并工作就自动完成了,不过也有一些更复杂的情况是需要我们手工处理的。废话不多说,我们先试一把,你回去改动一下做个提交吧。”

小蔡再次回到工位,修改了showMessage方法的显示字符串printf("hello, vision world\n");,然后提交。老马没有立即执行git pull,而是对vision_main.c文件进行编辑,增加了两行代码并提交。这时他再执行git pull命令时,他的终端就进入了一个文本编辑提示界面:

 $ git pull
 自动合并 vision_main.c
 
 Merge branch 'master' of /home/macai/web/vision_prj_repo
 
 # 请输入一个提交信息以解释此合并的必要性,尤其是将一个更新后的上游分支
 # 合并到主题分支。
 #
 # 以 '#' 开头的行将被忽略,而且空提交说明将会终止提交。

小蔡这时也来到老马身后看着,老马在文本编辑器里输入了更改原因然后退出。老马笑道:“合并完成。” 小蔡有些诧异地问:“这就行了?”

老马:“不信你看。” 说着他就把代码显示了出来:

 #include <stdio.h>
 
 void showMessage()
 {
     printf("hello, vision world\n");
 }
 
 int main(int argc, char *argv[]) 
 {
     int i;
 
     i = 0;
 
     showMessage();
 }

“哟嗬,果然是自动合并呢。” 小蔡频频点头。

老马:“你刚才第一次执行git pull命令的时候,我就提示过你,pull包含了两个步骤,对吧?”

小蔡再次点头:“对,拉取和合并。所以pull命令会将远程的修改和本地的提交进行合并,这个过程我明白了。”

老马总结道:“所以每次开始工作前执行一次git pull是个好习惯,可以避免潜在的代码冲突。但代码冲突那些更复杂的情况,就留给你小蔡自己去琢磨吧。好了,目前我们只有一个分支,就是master。但你也知道只在一个分支上做开发是会出问题的,下面我们看看分支操作怎么弄吧。”

分支管理

老马:“先让我们来考虑简单一些的情形。例如master分支就作为产品线的稳定发布分支,就是最终上线部署的程序,就从master分支编译得到。但master分支的代码不允许直接编辑,它必须来自于另一个分支,我们称它为develop分支吧。”

小蔡:“这个我理解。我们开发就在develop分支上进行,测试没问题了再合并到master分支上,对吧?”

老马:“孺子可教。那我们就来创建一个新分支吧。” 说完,老马在终端上执行git branch develop命令,但执行完后没有任何提示,老马又执行查看分支的命令:

$ git branch develop
$ git branch -l
   develop
 * master

老马一边敲键盘一边解说:“你看,分支已经有了吧。接下来我们再切换到它上面去。”

 $ git checkout develop
 切换到分支 'develop'

老马:“切换成功。小蔡,我这是分步演示的,其实还有更简便的办法,就是git checkout -b develop,一条命令就包含了创建并切换的动作。回头你自己去试试啊,我们现在看看怎么合并分支。”

老马打开vision_main.c文件进行编辑,提交之后就切换回主分支并进行合并,执行如下命令:

 $ git checkout master
 $ git merge develop 
 更新 6c2473b..f7d9d9a
 Fast-forward
  vision_main.c | 6 +++---
  1 file changed, 3 insertions(+), 3 deletions(-)

再一查看文件内容,果然和develop分支保持一致了。老马指点着屏幕上的“Fast-forward”,说:“看到这个提示了吗?这就是说git判断develop是相对于master的增量变化,没有其他差异,git就简单地将master分支置于develop处,这样实现快速合并的功能。”

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java