从零开始学习Git

@[TOC](0 目录)

1 简介

  • Git是什么
    Git是一个免费开源的分布式版本管理系统。所谓版本管理系统,就是让我们能够在开发的时候更好地进行产品的迭代升级,以及回滚纠错,同时不需要冗长的命名,试想如果按照下图这样进行版本迭代该多头疼。也很容易发生错误。
    抖个机灵

  • Git的特点

    1. 分布式
      Git的一大特点是其分布式的结构,所有版本信息仓库全部同步到本地的每个用户,这样就可以在本地查看所有版本历史,可以离线在本地提交,只需在连网时push到相应的服务器或其他用户那里。由于每个用户那里保存的都是所有的版本数据,只要有一个用户的设备没有问题就可以恢复所有的数据,但这也增加了本地存储空间的占用。
    2. 程序员导向
      Git毕竟是一个面向程序员开发的软件,随处都可见程序员的思维习惯,黑色的命令行框框、晦涩难懂的正则表达式,导致普通人很难上手。虽然现在已经有不少带GUI界面的Git操作软件,但从原始Git中继承来的名词和繁杂的操作还是容易让人望而生畏。
    3. 较陡峭的学习曲线
      上面提到Git程序员导向的整体气质,对许多刚接触Git的程序员来说,其学习曲线也是十分陡峭的,稍不留神就容易导致各种警告和冲突的出现,特别是如果在开发的过程中碰到问题则更让人抓狂,最后可能就会变成下面的漫画那样。
      在这里插入图片描述
      确实这种操作也是Git新手很容易出现的,毕竟我只是想拿来进行版本控制结果跳出了一大堆看不懂的错误,最简单粗暴的办法就是删了重装,但还是希望读者在看完这篇博客回去实践的时候,出现问题多想办法,总结错误,在这个过程中也能加深对Git的认识。当然最后如果实在时间紧急那还是删了重装吧😂。
  • Git的故事
    关于Git的来源还有一个小故事。在Linus大神组织全世界的热心志愿者开发Linux系统的时候,同样面临着版本迭代控制的问题,而在Git发明之前,全世界的志愿者都是通过电子邮件将自己开发后的代码的diff,也就是修改的部分发给Linus,然后再由Linus手动进行代码合并,这样的效率显然是十分低下的,而在当时网络带宽的限制下,免费的分布式版本控制系统CVS、SVN之流速度非常缓慢,也遭到了Linus的弃用。手动合并的方式虽然繁琐,但是在Linux系统初期代码量还比较小的情况下还是能用的,但到了Linux系统十周年的时候,整个系统的代码量已经非常庞大了,再继续使用这种办法显然不是可行之举。恰逢此时有一家商用的版本控制系统公司BitMover愿意免费授权他们的产品Bitkeeper这些开发者使用,于是Linux社区开始使用这个产品进行版本控制。但是在Linux这个免费开源的系统的开发过程中竟然使用的是一个商用的版本控制系统,这在社区当时也引起了很大的争议,也有部分开发者对其进行逆向工程希望破解。最终这一行为被BitMover公司发现并取消了其授权,而Linus大神则在两个星期的时间里就重新用C语言开发出了Git这个传奇的版本控制系统,这就是Git的由来故事。点击查看更加详细的故事

2 Git基本概念

在对Git有了一个简单的认识之后让我们来先了解一些Git的基本概念,方便后续的讲述。

  • 三个区域
    在一个Git工作目录下,总共有3个区域,分别是工作区(working directory)、缓存区(stage)、版本库(repository),工作顾名思义就是当前你看到的文件夹中的文件以及正在修改的文件,而缓存区则是一个隐藏起来的区域,你可以通过Git命令将某个经过修改的文件从工作区转移到缓存区,而版本库则是记录之前版本的区域,你可以通过Git命令来将当前缓存区中的文件打包保存成一个新的版本,注意不在缓存区中的文件则是保持与上一个版本相同。
  • 五种文件状态
    在一个Git工作目录下,每个文件都会有四种状态,分别是未追踪(untracked)、未修改(unmodified)、修改(modified)、暂存(staged)、提交(committed),而后面四种可以被认为是已追踪的状态(tracked)。在Git的工作目录中,我们需要手动将新的文件“告诉”Git,也就是让Git能够去“追踪”这个文件,关注其是否被修改提交等。而列入追踪的文件如果没有发生修改则处于未修改状态,一旦这个文件发生了变化,则状态变为修改,此时这个文件可以被存入缓存区,进入暂存状态,而一旦我们将缓存区中的内容进行提交,则文件进入暂存区称为一个历史文件,直到下一次其发生变化。详情可见下图
  • HEAD
    HEAD是一个常见的GIt概念,它其实就是一个指针,指向某一次提交,一般而言它会随着一条提交链而移动指向最新的一次提交。
  • 常见符号及命令行操作
    Git中有几个常见的符号:
    1. .指所有可操作的文件,一般我们在将某一个版本的文件提交称为一个新的版本的时候会用这个可以省去一个个敲入需要更新的文件。
    2. ^一般和HEAD配合使用,HEAD^指HEAD的前一个版本,HEAD^^指HEAD前两个版本,而如果需要更多的版本则可以用HEAD~n来指向。
  • Git与Github
    Github是一个大型代码托管平台,其实是许多远程仓库的所在网站,我们也可以把自己的远程仓库建立在上面,更详细的情况可以看下文远程仓库的部分。

    3 Git安装

    3.1 Linux系统

    如果是在Linux系统下可以直接通过命令行进行安装,例如Debain系下的可以通过sudo apt-get install git-all来实现安装。

    3.2 Windows系统

    Windows系统下可以在Git的官网下安装,下载完成之后一路next即可完成安装。

    4 Git基本操作

    下面介绍Git的基本操作,首先介绍本地Git仓库的操作,再介绍与远程仓库的连接与交互。

    4.1 基础操作

  1. 初始化仓库
    git init
    这个命令没有太多可说的,直接使用即可,之后在文件夹中会生成一个.git的隐藏文件夹,这个文件夹就是用于保存这个Git工作目录的相关信息的,随着开发的进行这个文件夹的大小会逐渐膨胀。

  2. 将文件保存到缓存区
    在初始化完成之后,如果当前文件夹中原来有部分文件,则需要手动将已有文件添加到缓存区当中(同时也是将这些文件标记为追踪状态),可以通过下面的命令:
    git add [filename|dir|.]
    来实现,其中如果使用.作为最后的参数则会把当前文件夹中的所有文件添加进去。而如果有多个文件或文件夹则文件与文件之间直接使用空格隔开即可。

    如果想把文件从缓存区中删除则可以使用下面的两条命令:
    git rm --cached [filename] or git rm -r --cached [dir|.]
    git reset <HEAD|log> [filename]这条命令的意思是用版本库中的对应文件来替换当前的缓存区,从而实现相同的效果。

  3. 查看文件状态
    如果想要查看当前文件夹中的文件状态,可以通过
    git status [|filename]
    实现。如果参数为空则会列出当前目录下所有的文件状态。

  4. 查看同一文件不同版本之间的不同
    通过以下命令可以查看同一文件不同版本之间的不同:

    1
    2
    3
    4
    git diff [filename] 查看当前工作区文件和暂存区的不同
    git diff HEAD [filename] 查看工作区和之前已提交的不同
    git diff [HEAD|log] [HEAD|log] [filename] 查看两个不同版本之间的不同
    git diff --cached [HEAD|log] [filename] 查看暂存区和已提交文件的不同

    敲入命令后会出现以下这样的界面:
    在这里插入图片描述
    红色和绿色表示的就是两个文件之间的差异,其中— a表示的是修改前的文件,而+++ b表示修改之后的文件。下面的红绿字体同样对应,红色字前有一个-表示这是修改前的内容,绿色字体前+表示这是修改后的内容。

  5. 签出命令
    签出命令对应的是git checkout,这个命令有多个用途。

    1. 切换分支
      通过git checkout [branch]可以在不同的分支之间切换,详情见下文分支操作部分。
    2. 取出文件进行覆盖
      签出命令可以将文件从缓存区取出到工作区,也可以从版本库中取出来覆盖缓存区和和工作区,命令如下:
      1
      2
      git checkout --filename 用缓存区filename文件覆盖工作区filename
      git checkout branch --filename 用branch中的filename文件覆盖工作区和缓存区的filename
  6. 建立一个提交
    终于在完成一个功能的开发之后可以进行版本迭代了,这个时候我们需要进行一次提交来将新写的代码从缓存区转移到版本库,这个过程就称为提交。提交的命令如下:

    1
    2
    3
    4
    git commit -m [message] 普通提交,可以直接commit,之后在vim中填写message
    git commit [file1] [file2] 指定部分文件进行提交
    git commit -a 相当于git add . + git commit
    git commit -v 提交时显示所有diff信息

    其中如果不加-m选项,则会跳出这么一个界面:
    在这里插入图片描述
    对于新手来说可能不知道这个界面是怎么回事,同时还发现无法操作。这个界面要求我们输入提交的信息(commit message),也就是带有-m选项中的后面的[message]。而当前处于的是一个叫做Vim的编辑器,这里只介绍最基础的操作,按下i即可开始编辑,编辑完成之后按下ESC结束编辑,再依次按下:wq回车结束即可。

    当然能够提交也要能够撤销提交,如果在一次提交之后发现有错误,那这时我们可以通过两种方式进行补救:

    1. 覆盖法
      将需要修改的文件修改完成之后,重新添加到暂存区,之后通过
      git commit --amend即可用一次新的提交来覆盖上一次提交,其中暂存区中同名的文件会被替换,而其他文件则保留在这个版本当中。
    2. 撤回法
      如果错误一时半会儿解决不了,则可以通过下面的命令撤回提交
      git reset --hard HEAD~1
      其中1也可以替换为任意数值,表示当前HEAD指向前的n个版本。
  7. 总结
    以上的操作可以通过一张图简单的概括一下:
    在这里插入图片描述
    当然Git当中有许多相似的操作,他们可以针对不同的情况来使用,这里就不做过多介绍,感兴趣的话可以自行上网搜索。

4.2 分支操作

在掌握了上面的基本操作之后,我们基本上就能够用Git来进行版本的控制管理了,但是在实际开发中,我们还会遇到多分支开发的情况,这时候就需要学习分支操作了。

  1. 创建一个分支
    在创建Git仓库之后会默认生成一个mastermain分支,而如果想要创建一个新的分支,可以通过
    git branch [branchname]
    实现。

  2. 切换分支
    在创建出新的分支之后,想要切换到这个分支去,可以用上面提到的checkout命令:
    git checkout [branchname]

    而创建切换两步其实可以通过一步来完成
    git checkout -b [branchname]这样如果原来不存在branchname分支则会自动创建并切换过去。

    注意:切换分支之后缓存区和工作区都会被新分支的最新提交所覆盖,所以在切换之前需要先把缓存区与工作区中刚刚完成的内容提交到分支当中,然后再进行切换,防止内容丢失。

  3. 分支合并

     当你和你的小伙伴分别都完成了各自的开发内容后,现在需要将你们两个负责的分支合并起来变成完整的版本,这个时候就需要分支合并了。分支合并可以通过以下命令实现:
    

    git merge [branchname]
    这条命令会把branchname分支合并到当前所在分支当中,所以在合并之前要记得先切换分支哦。

当然了,分支的合并没有那么简单,加入你和你的小伙伴在开发过程中都对某一个文件做出了修改,合并的时候就会提示分支之间存在冲突(conflicts),冲突显示的方式与上文`diff`的样式相同,这个时候就需要你们进入文件,对冲突的部分进行协商然后进行修改了。 通常冲突会保留在文件当中,如下图所示
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021022611515863.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTX05ld2VyMTk4ODU=,size_16,color_FFFFFF,t_70)
图中使用`<<<<<<<< branchname``=========``>>>>>>>>>>> branchname`分隔开两个不同的分支中同一个区域的不同内容,我们在修改的时候可以选择保留其中一个,也可以两个都保留,最后记得删去分隔线,在冲突解决好之后,这时**不使用`merge`命令**,而是需要将这个修改后的文件添加到缓冲区,之后提交即可:
1
2
git add [冲突文件]
git commit -m [message]
在冲突解决之后右侧的状态会发生变化

在这里插入图片描述
在这里插入图片描述
可以看到MERGING消失。

当然除了`merge`命令之外,Git还提供了一个`rebase`命令也可以用于分支合并,`rebase`命令会将另一个分支的所有提交全部复制到新的分支上,从而在新的分支上产生许多新的提交,具体入下图所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210227104222198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTX05ld2VyMTk4ODU=,size_16,color_FFFFFF,t_70)
关于二者的区别,[点击查看详情](https://www.atlassian.com/git/tutorials/merging-vs-rebasing)
  1. 删除分支
    在分支合并完成之后,原有的分支就不再需要了,这时可以通过:
    git branch -d [branchname]删除。

    4.3 应用操作

    在介绍完基本的操作之后,我们聚焦于应用类的操作,主要设计日常开发过程中的一些常见操作。

    4.3.1 多分支开发

    在日常的开发过程当中,我们通常不会直接在master分支上直接进行迭代更新,而是会另外设置一些分支,常见的分支如下:
  • master分支:主分支,仅用于记录关键版本节点;
  • develop分支:开发分支,从master分支中分出,用于记录每个小的版本更新。
  • feature分支:从develop分支分出,用于开发某个小功能,开发完成后合并回develop分支。
  • hotfix分支:用于紧急修复,从master分支分出,修复完成后合并回master分支,然后再从把master分支合并到develop分支从而将修复内容也转移到develop分支。
  • release分支:用于版本发布,从develop分支分出,通常需要在这个分支上进行代码注释,相关文件的版本号修改,完成之后合并到master分支,再在master分支上打上tag,一次版本更新就完成了。
    总结起来就是下图这样:
    在这里插入图片描述

4.3.2 代码回滚

代码回滚总共有3中类似但不同的命令,下面一一进行介绍:

  • checkout:这个命令我们之前有提及过,这里进一步解释以下,这个命令主要是用于查看之前的某个提交,会将HEAD转移到之前的某个提交上,保持原来的版本库不变,具体可见下图:
    在这里插入图片描述
    在这里插入图片描述
    我们输入git checkout HEAD^^,看到此时HEAD指针指向了前两个版本节点,但是整个版本库的树形结构没有发生改变。

    值得一提的是,这种方式虽然没有对树形结构进行破坏,但也正是如此,如果想要在这种情况下做出修改,我们不能简单地直接修改后提交,而是要另外再在这个节点上分出一个分支,修改提交之后再将这个分支合并回去,如下图所示。所以checkout命令才更主要用于查看而非修改。
    在这里插入图片描述

  • reset:与checkout相对,reset命令则会破坏原来的树状结构,会将该分支返回后的节点以后的所有节点删除掉,如下图所示。
    在这里插入图片描述
    在这里插入图片描述
    reset命令的作用就是更加纯粹的用于撤销之前的修改重新对之前的代码进行改动,可以看到相比checkoutreset在修改之后重新生成一个新的提交会方便很多。但是这样做的危害则是抛弃之前所做的一些修改,同时如果这个分支已经共享给了其他人,这么做很有可能会造成大量的冲突,甚至也有可能抹除他人所做的一些修改。

  • revert:revert命令算是随着Git的更新后来跟上的一个命令,这个命令主要是为了解决reset命令中存在的痛点,即无法在多人协作的项目中使用。revert相比reset的不同点在于它并不会“返回”之前的那个节点,而是把之前的一个节点挪到当前节点的下一个节点位置,然后再由你进行修改,详情可见下图:
    在这里插入图片描述
    在这里插入图片描述
    这么做在多人协作的项目中会有更好的效果,但是如果分支不需要和别人共享,那么这么做则会使Git历史记录变得冗余繁杂,同时也增大的存储占用。

    4.4 远程仓库

    当我们需要一个团队一起合作完成任务的时候,我们就需要有一个共同的远程仓库来存放整个团队的代码了,下面我们就来介绍远程仓库的相关操作。

  1. 远程仓库建立
    通常我们会将远程仓库建立在代码托管平台,例如GitHub,Gitee等平台,这里以GitHub为例简单展示如何建立。

    左上角找到如下界面,点击New开始建立一个仓库
    在这里插入图片描述
    然后填写上仓库名称,描述等内容,然后可以选择是公有(public)还是私有(private),注意个人仓库私有是免费的,但是团队仓库私有需要收费,如果不想付费的话可以考虑Gitee等其他平台。最后点击最下方create即可。
    在这里插入图片描述
    之后会进入这样一个界面,将仓库的SSH地址复制下来,在命令行通过git clone URL按下Insert键即可粘贴
    在这里插入图片描述
    但是也有可能出现下面这样的情况。这是因为GitHub网站还不认识你这台电脑,需要使用一个SSH密钥来指定你这台电脑,在你的GitHub网站中设置一下即可,点击查看详情
    在这里插入图片描述
    当然如果你的电脑上安装了GitHub Desktop,则可以直接点击Set up in Desktop一键完成。

  2. 克隆
    上面其实我们已经介绍了如何把自己建立的GitHub仓库克隆到自己的电脑上,其实就是通过
    git clone URL
    来实现的,这里再介绍一下如何克隆别人的仓库。
    在这里插入图片描述
    进入别人的仓库后会是这样的一个界面,点击绿色的按钮Code会出现如下界面。
    在这里插入图片描述
    点击右边的按钮复制即可,其他同上。

  3. 抓取
    当我们想要和远程仓库取得同步的时候,我们就需要使用抓取(fetch)和拉取(pull)两个命令。抓取的作用是从远程仓库获取它的所有版本库中的内容并用于更新本机的版本库,但是保留当前缓存区和工作区的内容,具体命令如下。其中name是远程仓库的名字,通常是origin,当然也可以自己进行设置。

    1
    2
    git fetch [name] [branch] 只抓取特定分支
    git fetch [name] 抓取所有分支

    抓取到的分支会被命名为name/branch,之后如果要将当前的分支与远程分支进行合并,则可以通过mergerebase命令实现。

  4. 拉取
    拉取相当于将抓取与合并两步合起来,具体命令如下:
    git pull <远程主机名> <远程分支名>:<本地分支名>
    如果省略冒号后面的内容,则会与当前分支进行合并,同时在克隆的时候会自动建立和远程分支的跟踪关系,从而可以只保留远程主机名。当然跟踪关系也可以手动建立,通过
    git branch --set-upstream [local_branch] [远程主机名/分支名]实现。

    同时如果我们希望直接用远程分支来覆盖本地对应的追踪分支,可以通过如下命令:
    git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

    注意:可能有人会有这样的疑惑,如果我在拉取的时候远程分支已经被删除了,一经同步岂不是也会把我的分支给删除吗?这里大家可以放心,一般情况下是不会被删除的,如果想删除,需要在命令中添加-p选项。

  5. 推送
    在完成了自己的任务之后,需要将自己的进度推送到远程主机上,这个时候就需要进行推送(push)了。命令如下:
    git push <远程主机名> <本地分支名>:<远程分支名>
    注意分支顺序和pull相反。同上如果省略远程分支名则会自动推送到存在追踪关系的分支,如果没有则会新建一个该名字的分支。如果省略本地分支名,则相当于删除某个远程分支。

    而如果只保留远程主机名,则自动将当前分支推送到与之有追踪关系的分支。

    5 Git团队开发规范Gitflow

    Git这项工具在开发的时候并没有设定什么规范,但是我们在开发的过程中如果不遵守一定的规范则很容易导致错误,甚至会破坏整个团队的成果,所有就有人逐渐总结Git的使用方法,发明出了工作流的说法,这里我们就简单介绍一下Gitflow这一工作流的基本原则。

  6. master分支

    master分支应该是稳定的,可以轻松地被获取到然后编译使用,同时也是hotfix(热修复)分支的出发点。

  7. develop分支

    develop分支是开发分支,代表开发的最前沿(bleeding edge),在这个分支中分出feature分支用以开发新的功能。

  8. 不允许直接在developmaster分支上进行开发

  9. 分支名应该具有概括性,例如修补分支fix-xxxbug,release-X.X.X

  10. 在开发完一个feature分支之后,想要并入develop分支,首先要通过pull获取最新的develop分支并消除conflicts

    如果还没有将当前的这个feature分支传到远程主机(feature完全在本地),则首先将远程端的develop分支rebase到本地(相当于更新本地的develop分支),然后将feature分支mergedevelop分支并解决冲突(这个步骤在本地完成),最后将develop分支push到远程主机,然后本地的feature分支就可以删除了。

    而如果已经将这个feature分支传到远程主机(feature同时在本地和远程主机),则首先将develop分支mergefeature分支,然后再将feature分支mergedevelop分支,最后推送到远程主机,并删除feature分支。

  11. code review

    Code review是指同事之间互相阅读代码并给出自己的意见或想法的过程。通过pull request来进行code review,同时让打开pull request的人来将分支合并到远程主机中,在原作者为同意意见之后由写code review的人将代码合并到主分支。

    pull request会在其他人想要将自己的分支合并到远程主机的分支时提出,字面意思是提出者希望原作者能够将他们的分支pull到项目代码当中

  12. 在develop分支开发完成准备要发布的时候,首先需要将develop合并到master分支当中,注意此时合并的时候要带上--no-ff选项,从而避免合并出fast-forward类型。而新的提交可以打上X.X.X的tag,然后再将master合并到develop分支中,这样develop分支中也会有版本号的tag。

什么是fast-forward:

​ 如下图
在这里插入图片描述

​ 当在master分支进行bugfix之后,现在要合并回去,如果原master分支没有更新,那么直接merge的话会变成下面这样:

​ 如果加上--no-ff选项

在这里插入图片描述

​ 区别在于此时master分支直接移动到了bugfix这里,而没有形成一个新的提交,这里git是偷了一个懒。但是问题会出现在如果将这个bugfix分支删除掉的时候,如下两图所示:

这是直接merge

在这里插入图片描述

这是使用--no-ff选项。
在这里插入图片描述

可以看到,非fast-forward的情况下会生成多一个提交在主分支上,这里就可以写上bug-fix,也能看到整个bug-fix的过程的提交信息,而如果不加,在删除掉之后就只剩下最后一次的bug-fix的提交信息,不利于后续的回滚。

  1. bug-fix

    在发现bug需要修改的时候,要从master分支中分出分支然后修改,修改完成后合并回去(注意–no-ff),然后将master分支合并到develop分支。

  2. Commit message

    在写commit message的时候,使用一般现在时,不使用三单形式,第一行书写Summary,一般在50词以内,如果需要进一步说明则换行使用段落的格式进行,注意正确的标点和大小写,并且控制在72列以内就要换行。

  3. release

    release分支从develop分支中分出,首先生成一个新的分支release-X.X,(然后通过一个固定脚本将代码中的版本号统一进行修改),然后git commit -a即可,之后切换回master分支,在这里用–no-ff合并刚刚的release分支到master分支,然后通过git tag -a X.X打上tag,最后将release分支删除即可。

6 总结

Git的使用是团队开发过程中难以避免的一环,正确的使用Git可以为开发节省下大量的精力,但同时错误地使用也可能造成整个团队项目的崩溃,所以对Git的学习使用一定要规范正确,多加练习,才能在真正需要的时候发挥出它的强大威力。

7 参考资料

  1. Learn git
  2. 一小时学会Git
  3. A successful Git branching model