Git

本文主要讲解Git的使用,所有示例均基于Linux Ubuntu20.04操作系统,但Git目前支持多个操作系统,且不同平台Git使用方式并无大的差异。本文大部分内容均摘自廖老师的Git教程,笔者更改了廖老师原博客的一些知识点顺序,如将远程仓库放到了分支和标签后面说,另外在Git的分支管理策略中,笔者对git flow和Github flow做了补充说明,其间笔者也借鉴了网上一些其他文章和资料,相关文档均贴在了相应的地方。

1. 简介

Git为Linus为管理Linux代码而创建,Git与其他版本管理工具最大的不同是Git是分布式的,我们常见的如CSV或SVN是集中式的。

集中式比较好理解,即需要一个中央服务器,这个中央服务器来管理大家提交的各个版本的代码,大家如果需要做版本回退或查看版本记录,都需要和中央服务器通信,如果离开了中央服务器,那就无法得到其他版本的项目,也即无法回退版本。但Git不同,Git是分布式的,也即每个拥有项目的电脑都是一个中央服务器,每个人的电脑上都有着完整的版本库历史,当我们要回退版本时,直接本地就能完成,无需中央仓库。对于集中式版本管理,如果中央仓库没了,那就代表所有的版本都没了。换句话说集中式和分布式区别是自己本地是否有完整的版本历史

说到这里可能很多人会有疑问:我们实际在使用Git的时候明明也有个中央仓库啊,需要配远程仓库地址。Git远程仓库的目的不是为了版本管理(至少核心目的不是),而是为了多人协作。假设一个项目由三个人开发,大家都有自己实现的代码,如果没有远程仓库大家就得两两同步彼此的版本,这个成本太高了,因此我们一般会推出一个公用的电脑,大家将自己的版本推送到这个电脑上,用它来方便“交换”大家提交的代码。

另外多补充一句,什么叫做版本?每一次提交都是一个版本。也即大家执行git commit的时候就会生成一个新的版本,至于git push并不是生成新版本,而是将你本地的版本提交给远程服务器(或者说同步到远程服务器)。

下面对于Git的使用默认大家都已经安装了Git,且均已经配置了

git config --global user.name
git config --global user.email

2. Git基本使用

2.1 版本库的创建

如果你想让自己某个目录下的所有文件都能被Git管理,对于文件的修改,删除都能被Git追踪,以便可以随时追踪历史,甚至在某个时刻“还原”,就需要创建版本库,或者叫Git仓库。

仓库的创建非常简单,我们只需要在想要被管理的目录下执行git init指令即可。为方便演示,笔者在此先创建一个目录:

mkdir gitlearn
cd gitlearn

在当前目录下执行

git init

执行完后我们会在自己的目录下看到多了一个.git目录(Windows用户如果没看到请开启隐藏文件夹的显示)。

image-20220326191323505

这里多提一嘴,Git只能管理文本文件(所有版本控制都是这样),对于二进制文件如音频视频是没法追踪变化的。

既然创建了仓库,我们就来看看Git是如何帮我们管理文件的。我们现在在gitlean目录下编写一个文件:

vim readme.txt
Git is a version control system.
Git is free software.

我们只有将这个文件放进Git仓库才能被管理,将一个新的文件放进Git仓库只需要两步:

git add readme.txt
git commit -m "wrote a redme file"

image-20220326192533241

git commit -m中的-m xxx是用来对这一次提交的描述,我们之前说过每一次git commit都是一个新的版本,因此也可以说是对这个新版本的描述。一般第三方工具包括git都强制要求大家提交的时候加上-m,进行一个描述说明。

现在可能很多人有个新的问题,为什么一次文件的提交要用两条指令,git addgit commit对Git而言到底意味着什么?

2.2 工作区和暂存区

工作区(Working Directory)就是我们git init那个目录下非.git目录的其他内容。比如我们前面的例子,工作区就是gitlearn目录下非.git的部分。

至于.git它不属于工作区,它就是我们之前一直说的版本库或Git仓库。Git版本库包含很多东西,其中一个重要的部分就是暂存区(stage),还有Git在一开始会为我们创建的第一个分支master,以及一个指向master的指针HEAD

关于分支和HEAD我们后面讲,我们先讲清楚暂存区是什么:

之前我们在将一个文件加入Git版本库时,执行了两步:git addgit commit

  • 在执行git add的时候,我们其实是将工作区文件的修改加进暂存区
  • 在执行git commit的时候,我们是将暂存区中的所有信息提交到版本库

git-repo

大家可能还知道一些别的指令,如git diffgit reset

  • git diff是查看工作区和暂存区的差异(可以理解为当前有多少修改未add)
  • git diff --cached是查看暂存区和当前分支的差异(可以理解为看看当前有多少未commit)
  • git diff HEAD是查看工作区和版本库(当前分支)的差异
  • git restore是撤销工作区的修改,将暂存区的版本移到工作区(很多时候如果我们add了,又在工作区修改了文件,但现在想撤回修改,就是git restore的时候,将当前修改未add的丢弃,其与git add是反向命令)
  • git reset HEAD是将当前分支最新版移到暂存区(即取消add的但未commit的文件,其与git commit是反向命令)

不懂这些指令也不要紧,我们后续会讲到的。

了解了工作区和暂存区,也就明白了为什么需要git addgit commit两次指令了。

2.3 版本回退

我们说了版本管理工具最强大的功能就是版本的管理,当我们做了修改并提交就是一次新的版本。这里我们先对之前的readme.txt文件进行修改:

Git is a distributed version control system.
Git is free software.

Git给我们提供了git status命令,用于查看当前仓库状态,我们运行git status

image-20220326200816455

可以看到Git会提醒我们刚才修改的readme.txt未add。

如果你改完这个readme.txt未add,然后国庆节放假了,假期过后你完全忘了自己到底做了哪些修改,你也不敢随意的提交代码,我们可以通过git diff来看看改动是什么

image-20220326201153059

上图中红色的以-开头的是Git仓库中该行的数据,而绿色的以+开头的是当前工作区该行的数据,一对比我们就能看到哪些行做了哪些修改。(刚才我们讲到git diff是比较工作区和暂存区的不同,但因为对于readme.txt我们是进行过git commit的,后续没有进行git add,也即此时暂存区应该是空的,在暂存区为空的时候,git diffgit diff HEAD相同,都是比较工作区和版本库的差异)

知道了改动是什么,我们就可以安心的提交代码

git add readme.txt

此时使用git status

image-20220326201858913

我们可以看到,Git会提醒我们有哪些更改未commit,此时再执行

git commit -m "add distributed"

再执行git status

image-20220326202125314

现在我们已经做了两次提交了。我们再来修改readme.txt文件

Git is a distributed version control system.
Git is free software distributed under the GPL.

然后执行:

git add readme.txt
git commit -m "append GPL"

这样对于版本的提交有些类似于我们玩游戏,每通过一个关卡就会保存进度,如果有时我们在进行某一关的时候被Boss打死了,那么我们往往会读档,从上一次保存的地方重新开始。Git也是如此,你的每一次提交都类似于一个存档,一旦你当前的项目有问题可以通过“读档”来恢复到上一次的提交版本,我们把这个叫做版本回退。

我们先回顾一下关于readme.txt目前已经有几个版本了:

版本1:wrote a readme file

Git is a version control system.
Git is free software.

版本2:add distributed

Git is a distributed version control system.
Git is free software.

版本3:append GPL

Git is a distributed version control system.
Git is free software distributed under the GPL.

如果想查看所有的历史版本,我们可以通过git log

image-20220326203309745

git log会按时间倒序展示每次的提交。git log --oneline可以输出一个更精简的版本:

image-20220326203526953

前面那串黄色的字符串是版本号(commit id),由于Git是分布式的,是由一个SHA1计算出来的一个非常大的数字,用十六进制表示。你的版本号会和我的不一样,没有关系,以你的为准。

可以看出Git将每一次提交的版本串成了一个时间线

大概类似于这种

image-20220326205515666

现在我们要做版本回退,将当前版本回退到上一个版本,即add distributed版本,通过git reset命令实现

git reset --hard HEAD^

image-20220326205828016

上述指令 git reset --hard HEAD^

其中git reset代表回退(或修改),HEAD我们之前说过其指向当前分支的最新版本(当前版本更贴切些),整个版本的管理可以看作是一个链表,每次有新的版本提交,这个版本就加入到链表的尾部(也可能是头部),HEAD指针就指向当前版本,其中HEAD^是前一个版本,也即当前节点的前一个节点,类似于HEAD->prev。而--hard是指移除当前暂存区所有的内容并强制删除工作区的所有修改。也即加了--hard后我们的工作区就与仓库中该版本的文件一致了,同时暂存区也被清空了。

可以看到现在,Git已经提示我们版本回退成功了,我们查看readme.txt

image-20220326205945309

可以看到也确实回退了。这就好比你游戏读档,直接从某个之前的档开始玩。

现在我们做了版本回退,如果还想切回到之前的appendGPL版本该怎么办,如果大家记得appendGPL的commit id那可以执行

git reset --hard 想要更改的commitid

image-20220326210853969

再查看readme.txt文件:

image-20220326210927624

发现也确实切换成功了。

如果大家不记得每个提交的commit id了怎么办?

Git提供了 git reflog

image-20220326211457914

可以看到Git依然按时间倒序的形式,将我们对版本的操作记录了下来。

Git对于版本的切换异常的快,其实在Git内部,版本的切换就是HEAD指针指向对象的切换(这种说法不准确,但大家可以那么理解)。

也即由

image-20220327095743191

改为

image-20220327095743191

2.4 修改和删除

我们现在再来修改readme.txt文件

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes.

将修改加入暂存区

git add readme.txt

image-20220327094657520

此时我们再修改readme.txt

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

然后直接执行提交

git commit -m "git tracks changes"

image-20220327095031366

可以看到Git会提醒我们有工作区的修改未进行add。

这也验证了我们之前说的,git commit只是会将暂存区的信息提到Git仓库。也即我们最后一次对readme.txt文件的修改并未执行git add,未提交到暂存区,因此git commit也未将我们的修改提到Git仓库。

通过执行git diff HEAD指令,我们可以看到当前工作区和版本库的区别:

image-20220327095444682

可以看到不同的地方Git用颜色标注了出来,且以-+开头,我们最后一次修改后执行git commit确实未提交到版本库。

因此我们需要:

git add readme.txt
git commit -m "append of files"

image-20220327095743191

现在是凌晨两点,又困又饿的你在码代码的时候,将readme.txt文件多加了这么一行:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

你猛然间发现不小心将自己的真实想法写到文件里了,因此你肯定会立马想撤销这个更改,但该删除哪些更改呢?会不会删多了或者删错了把之前提交本来有用的内容也删了呢?能不能将文件回退到上一个add的版本,这样就可以保证文件肯定是能用的,且肯定是没有一些“过激言论”的。

如果大家使用git status会看到Git会提醒你如果丢弃工作区的修改:

image-20220327100546553

Git会提醒我们使用git restore来丢弃工作区的修改

git restore -- readme.txt

此时再执行git status

image-20220327101118738

再查看readme.txt

image-20220327101155248

我们之前工作区的修改确实已被丢弃,也即git restore命令是将暂存区的文件写入工作区。(注:由于我们之前每次add后都进行了commit,所以准确的说此时的暂存区是空的,在暂存区为空的情况下,git restore是将仓库的文件写回到工作区)

我们刚才说的自己写的这个文件并未add的情况下丢弃,那如果我们已经add了怎么办?我们重新修改readme.txt为:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

然后进行add

git add readme.txt

git add后我们才发现自己将“过激言论”也加入进去了,此时如果想撤销修改,Git给我们提供了 git restore --stagedgit restore --staged将版本库中的文件写入到暂存区

git restore --staged readme.txt

此时我们通过 git diff可以查看当前暂存区与工作区的区别

image-20220327102607762

可以看到之前的add已经被丢弃,也即git restore --staged命令生效了,此时我们再执行git restore将暂存区的内容写入到工作区:

 git restore -- readme.txt

git staus和查看readme.txt文件:

image-20220327102818407

因此对于已经add的文件,需要

git restore --staged <filename>
git restore -- <filename>

image-20220328191659826

git restore --staged其实与我们之前说的git reset相似,并且上述流程我们完全可以通过

git reset HEAD readme.txt

将版本区的文件写入到暂存区中。

另外,如果大家看廖老师博客或对Git有一定了解的话,可能会发现git restoregit checkout --指令相同,由于git checkoutgit checkout --极易混淆(注意这俩功能完全不同),因此Git官方已经不建议再使用git checkout,改用git restore

我们一直在说文件的修改和提交,一直没讲删除,其实对于Git而言,删除也是修改。我们先在gitlearn下创建一个test.txt文件,然后执行:

git add test.txt
git commit -m "add test.txt"

此时如果我们在本地删除了test.txt文件

image-20220327104049465

可以看到在执行git status时,Git会认为我们对test.txt文件的修改未进行add和commit。

对于这次删除往往有两种情况,一种是大家确实想删除这个文件,并且要提交这次删除结果,那么我们就需要:

git rm test.txt
git commit -m "remove test.txt"

image-20220327104451544

其中git rm filename的作用是将文件从工作区和暂存区中删除。git add是加入,而git rm是删除。

另一种情况就是我误删了,我将test.txt文件误删了,这时我们可以通过之前的git restore丢弃工作区的修改,将暂存区写回到工作区

image-20220327105146903

当然,如果大家不仅误删了test.txt还进行了add,那就需要 git restore --stagedgit restore两步了。

3. 分支

分支应该是Git又一十分强大的功能,虽然其他版本管理工具如SVN也支持分支,但其他版本管理工具的分支功能都十分难用,基本上都是又慢又反人类。但Git不同,Git的分支十分的快,并且Git官方是鼓励大家频繁的使用分支的。

本章我们先来了解下什么是分支以及如何才是好的分支使用方案,最后再聊聊多人开发里面的冲突问题。

3.1 分支的创建和合并

假设有这样一种场景,你们公司正在开发一个项目,你负责这个项目的登录模块。首先你肯定会先将现在的已有项目clone到本地,我们假设现在项目只有一个master分支,如果你直接在master分支上开发,那么比如这个登录模块需要一周才能开发完,第一天你写了20%的代码,一般来说为了保证可以被Git管理,你会不断的add然后再commit到master分支。但现在问题来了,你要不要push呢?由于你这个模块还没开发完,如果你push了,那可能会导致别人在同步代码的时候会项目运行不起来。而如果不push,整个功能模块开发完再push,那万一某天你的电脑死机了或者硬盘坏了,之前写的代码都没push,那一切就白干了。于是你可能会想,如果有一种功能,既能让我提交代码,又能不被别人看到我的提交,不同步我的代码就好了,等我整个项目都开发测试完了,再同步我的代码。其实Git提供了这个功能,就是分支。

我们之前只有master分支的时候说过Git会将每一次的提交串成一个时间线,类似于这样

image-20220327195020140

但如果有了新的分支,比如我需要开发一个登录功能,我就需要在当前分支(假设当前是master分支)上创建一个分支叫login,后续每次提交和版本管理都是在login上进行:

image-20220327195847018

如上图,我们从master分支中劈出了一个login分支,对于使用者来说当前master分支和login分支类似于两个平行时空,彼此互不干扰。注:上图画的可能有些歧义,比如假设我们在login分支修改了之前append GPL版本提交的代码,其实并不会影响其他分支,每一个版本都是彼此独立的,和其他版本没有影响,一种对于分支更准确的表达应该是:

image-20220327200354096

image-20220327200742063

后续如果我们在master上提交新的版本:

image-20220327201056524

对于login也是如此:

image-20220327201242109

也即两个分支是完全平行,没有任何关系的,这样你在login分支上开发你的登录模块,其他同事在别的分支上开发别的模块,大家互不干扰。等到你开发完登录模块后再将自己的login分支合并到master分支中。

既然分支那么强大,我们就来学习分支的创建和合并吧。

为更贴近于Git的使用,我们就创建一个分支叫dev吧:

git switch -c dev

image-20220327202718883

其中 git switch -c <branch>是指创建并切换一个新的名为branch的分支,如果大家已经有了这个分支,如master,则直接 git switch <branch>即可:

image-20220327202925307

现在我们要思考的是,当创建或切换分支的时候,对于Git而言意味着什么?

之前我们说过HEADHEAD其实是一个指向当前分支指针的指针。是的,HEAD并不直接指向当前分支,每一个分支都有自己的指针,比如master分支就是master指针,master指针才指向master分支,同理dev也有个dev指针,当我们创建并切换到dev分支时,Git其实就是创建了一个dev指针,并将HEAD指向dev:

image-20220327203549748

当我们切换分支到master时:

image-20220327203754176

后续如果你在dev分支上开发,那么每提交一个版本,dev指针就会向前移动一个版本。

image-20220327204031922

后续如果在dev分支上开发完,将代码合并到master分支时,master指针就指向dev指针指向的位置即可:

image-20220327204316106

如果合并完成并想删除dev分支,则直接将dev指针删除即可:

image-20220327204338511

另外如果大家想看当前项目有多少分支,可以通过git branch

image-20220327204442478

可以看到Git还会贴心的告诉我们当前我们位于哪个分支。

我们切回到dev分支,并修改readme.txt文件:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick.

然后提交当前修改:

git add readme.txt
git commit -m "branch test"

这时我们切回到master分支,查看readme.txt文件:

image-20220327204945966

可以看到master分支的代码并没有被修改,现在我们想合并dev分支的版本到master:

git merge dev

其中git merge <branch>是指将指定分支合并到当前分支中,记住是合并到当前分支,大家在合并前一定要看清自己在哪个分支,可以使用git branch查看。

合并后我们再查看下readme.txt文件:

image-20220327205634774

如果大家在master分支执行git log可以看到当前master和dev的最新版本为branch test并且HEAD指针指向master和dev

image-20220327205823647

现在dev分支完成了它的使命,我们可以将dev分支删掉了:

git branch -d dev

这里有几点需要提醒,首选大家不能删除目前所在的分支,也即不能是当前在dev分支下删除dev,需要先切到master分支才能删除dev,另外如果大家dev分支有一些版本没有合并到master分支并且想丢弃这些版本,直接删除,则需要

git branch -D dev

也即-d就是--delet的缩写,而-D--delete --force,即不检查merge状态直接删除。

另外,很多同学可能知道Git还有一个指令是git checkout也是用来创建和切换分支的,我们之前说了git checkout和git checkout --很容易混淆,因此git checkout也被Git弃用,更推荐git switch

3.2 分支管理策略

分支管理策略也即如何合理的使用Git的分支,这里有一个很好的文章A successful Git branching model » nvie.com,这篇文章写于2010年,那时候Git刚出现不久,不过这篇文章对于Git分支的管理直接影响了后续十几年大家对Git的使用方式。(如果英语不好的同学,这里也有一版国人写的,大概内容相同 实际项目中如何使用Git做分支管理_ShuSheng007的博客-CSDN博客_git项目分支管理,这里还有一篇是阮一峰写的,内容也是基本相同Git分支管理策略 - 阮一峰的网络日志 (ruanyifeng.com)

我们可以说一下对于Git分支的使用方案:简单来说就是Git鼓励大家频繁的建分支,但是每个分支完成了其使命后也记得要删除分支。

首先在初始化的时候,Git就会为我们创建一个master分支,master是一个十分重要的分支,这个分支会随着项目的整个生命周期而存在,一般我们认为master分支是一个最为稳定的分支,这个分支上的代码代表着当前可直接发布的状态。

另外一个为大家熟悉的就是develop分支,develop作为一个开发分支,平行于master分支,基本上也会和整个项目的生命周期相同。

除了这些以外还有一个十分重要的分支叫Feature分支,我们可以称为功能分支,Feature是一系列具体分支的统称。一般我们在开发一个功能的时候,会从develop分支上创建一个自己的分支,在自己创建的分支上来开发,等到开发完成后再合并到develop分支,而这些为完成某个功能而创立的分支就是Feature分支。

比如王五和李四同时开发一个项目,王五开发注册模块,李四开发登录模块,王五会从develop分支下创建一个register分支,而李四同样会从develop分支下创建一个login分支,这些分支都叫Feature分支,他们在自己的Feature分支上平行开发,开发完后将自己的分支合并到develop分支,然后再删除各自的Feature分支。master,develop和Feature它们之间的关系如同:

develop从master中劈出来,并在某些成熟版本时合并到master分支。

70

Feature分支从develop中劈出来,每完成一个功能就可以合并到develop中。

70-16505113920912

因此正常的分支使用流程应该是:

项目会有master和develop两个分支,大家每次写自己模块的代码时,都需要先从develop分支下创建一个自己的Feature分支,自己在自己的Feature分支下commit和push,在Feature分支开发完成后,就merge到develop分支,然后删除自己的Feature分支。这也就是我们刚才说的,Git鼓励大家频繁的创建分支并在分支完成使命后删除分支。

除了这些分支外还有hotfix分支和release分支,这里不做赘述,感兴趣的同学可以看上面两篇文章,里面讲的很详细,这里贴一个他们的关系图:

70-16505113943854

另外正如A successful Git branching model » nvie.com作者在十年后的2020年写到

在这10年间,Git的使用席卷全球,而目前应用Git最多的软件开发是Web开发。作者认为Web软件往往是持续发布,版本迭代很快,而不会回滚(roll back)的,很多时候我们完全是不需要多版本的,也即发布一个版本后往往是不需要上一版本的内容的,或者说是不会多版本并行存在的。虽然作者提出的git flow这一使用策略被大多数软件公司视为标准,甚至奉为圭臬。但作者在写这篇文章的10年前,当时主流的软件类型还不是Web这种持续发布的项目,因此作者更建议对于Web开发不一定非要使用git flow,可以选择更简单的Github flow

git flow就是我们上面讲的流程,其核心是有develop和master两个分支,master的更新会比较慢,因此也十分稳定,大家有新功能的时候,会从develop上构建一个自己的Feature分支,当开发完成后,会提交到develop分支,在提交的时候往往会有人审核代码,审核代码通过就会将你的代码合并到develop分支。注意我们这里说的是远程的develop分支,也即每个功能的合并都是提交到远程仓库的develop,这样如果我们需要将develop合并到master的时候,还需要先更新本地的develop仓库然后再合并。

Github flow更为简单,其核心是为了处理像Web开发,快速迭代和快速部署的情况,Github flow有两个很重要的核心是重视测试和部署自动化。首先Github flow长期分支只有一个master,这个master也是需要实时是可部署状态,但这个master的更新会很快,每当有新功能开发时,我们直接从master上创建新的分支并在远程Github仓库创建同名分支定期push,然后当功能开发完需要merge到master的时候会发起一次Pull Request请求,让审核人员审核你的代码,审核通过后就可以合并到master分支,master分支一旦被合并就立即部署一个新的版本。很多时候大家同时开发不同的功能,一天可能会有很多次的提交和部署,因此master的更迭是十分快的。在这种情况下,充分测试和自动部署就十分重要。首先由于没有develop分支,且master更迭的很快,每次大家merge的代码都会直接到master且直接部署,因此在merge前充分测试很重要。其次,由于master的更迭很快,可能一天好几十版本,因此每次master更新都需要自动部署。

GitHub Flow & Git Flow 基于Git 的两种协作开发模式 - sloong - 博客园 (cnblogs.com)这篇文章的作者很好的讲解了git flow与Github flow的区别,也给出了一些测试和自动部署的方案。

3.3 解决冲突

很多人不愿用或者害怕用新的分支往往是因为担心分支合并的时候会有冲突,我们现在模拟一个分支合并冲突的场景,看看如何解决。

首先我们从master分支上新建一个feature1分支:

git switch -c feature1

然后在feature1分支修改readme.txt文件最后一行

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick and simple.

然后在feature1上提交修改:

git add readme.txt
git commit -m "append and simple"

此时我们再切回到master分支:

git switch master

并在master分支修改readme.txt文件:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick & simple.

然后在master分支提交修改:

git add readme.txt
git commit -m "append & simple"

现在master分支和feature1分支有了各自不同的修改和提交,就变为了:

image-20220328162452236

这种情况下如果我们将feature1分支合并到master分支就会有冲突,我们可以看下:

git merge feature1

image-20220328162646889

可以看到Git会提醒我们合并出现了冲突,需要修改冲突,现在我们查看readme.txt文件:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
creating a new branch is quick & simple.
=======
creating a new branch is quick and simple.
>>>>>>> feature1

很多同学看到这些奇怪的字符可能会下意识的发怵,觉得文件被冲突破坏了,其实不然,如果大家仔细看的话会发现不仅文件没被破坏,Git还将当前文件不同分支冲突内容标注了出来:

<<<<<<< HEAD
creating a new branch is quick & simple.

这段为当前分支,即master分支的内容

creating a new branch is quick and simple.
>>>>>>> feature1

这段代表feature1分支的内容,因此我们只需要衡量各个分支冲突的内容然后修改后再提交:

比如修改后readme.txt如下:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick and simple.

此时就可以再次保存提交:

git add readme.txt
git commit -m "conflict fixed"

此时master和feature分支就变为了:

image-20220328163754602

通过git log我们也可以很清楚的看到这一现象:

image-20220328163858529

现在feature1分支已经被合并过来了,我们就可以删除feature1分支了:

git branch -d feature1

3.4 保存现场

上一节解决冲突的时候,我们构造了一个feature1分支,我们在这个分支修改了readme.txt文件,然后进行了add和commit后才回到master进行merge。但大家有没有想过,如果我在feature1修改完readme.txt后不add或commit,然后此时切到master分支会怎么样?

现实开发中我们往往是有这个需求的:假设你正在进行一个feature1功能的开发,此时老板立马跑过来跟你说,master提交的版本有个功能有bug,很着急,赶紧改一下吧。此时你会怎么办?会直接切到master分支然后创建一个修改bug的分支来修改bug吗?如果是这样,那会不会有问题呢?我们现在就来测一下:

首先我们创建feature1分支:

git switch -c feature1

然后在上面修改readme.txt文件:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick and simple.
this is feature1 writed.

此时不进行add和commit,切换到master分支:

git switch master

先别急着创建bug修改分支,我们先看看此时的master分支的readme.txt是什么样的:

image-20220328165633090

是不是很奇怪,最后一行this is feature1 writed明明是在feature1分支写的,我明明没有merge到master分支,为什么会在master分支看到?

别急,我们现在切回到feature1分支,对readme.txt进行add但不commit

git switch feature1
git add readme.txt

此时再切到master分支看下readme.txt文件:

image-20220328165936896

可以看到在master分支依然可以看到feature1分支修改的这句话(大家不用关心截图里的分支领先4个提交之类的话,那是因为笔者在这时候将仓库和远程仓库进行了绑定,跟本示例要讲的东西无关)。

如果我再切到feature1然后commit readme.txt呢?

git switch feature1
git commit -m "feature1 commit"

此时再切到master然后查看readme.txt

image-20220328170320096

这一下就看不到feature1的提交了。为什么?

因为对于任何分支而言,工作区和暂存区都是公共的。

由于没有进行commit,我们的修改是保存在工作区或暂存区的,因此切到任意分支,工作区和暂存区都是同一个,都能看到我们的修改。换句话说,大家如果要切换分支,一定到保证当前分支是提交过的。

但回到我们刚才那个应用场景,你正在开发feature1的功能,老板让你立马热修一个bug,如果先将feature1保存提交,可能会很麻烦,包括很多文件可能根本没写完还有语法错误。那有没有一个功能,让我能暂时保存我当前feature1的现场,切到别的分支开发完后再切回到feature1分支然后恢复现场继续开发呢?

有的,这就是git stash

为了故事的顺利进行,我们假设你现在开发的是用户登录功能,因此你在login分支开发,我们先创建login分支:

git switch -c login

然后在login分支修改readme.txt文件:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick and simple.
this is a user login.

现在老板过来了,告诉你需要热修一个bug,只给你10分钟的时间,赶紧改完测完然后部署,因此你需要保存login分支现场:

git stash

此时切到master分支,查看当前master下的readme.txt文件:

image-20220328171620459

可以看到login分支修改的内容在master下已经不可见了,然后我们创建一个新的分支叫bugfix:

git switch -c bugfix

在bugfix下修改readme.txt文件,将其中一行bugGit is free software distributed under the GPL.,改为Git is a free software distributed under the GPL.

Git is a distributed version control system.
Git is a free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
creating a new branch is quick and simple.

进行add和commit

git add readme.txt
git commit -m "bug fixed"

然后切回到master分支进行merge:

git switch master
git merge bugfix

现在修改完了bug,我们就可以切到login分支继续开发我们的代码了

git switch login

之前我们保存了login分支的现场,现在我们需要恢复现场

恢复现场的方式有两种:git stash applygit stash pop

git stash apply可以帮我们恢复现场,但恢复后,stash保存的内容并没有删除,我们可以通过git stash drop删除stash保存的内容。

git stash apply
git stash drop

还可以通过git stash pop恢复现场的同时删除stash保存的内容

git stash pop

此时我们再查看readme.txt文件:

image-20220328175027222

可以看到现场已经被修复。

另外在热修bug的时候,我们修改完后就merge到了master分支,一般来说master分支有bug,那么dev或自己的开发分支也会有这个bug,也即我们刚才修复的bug在其他分支上也需要修复,git cherry-pick指令就是专门做这个用的

我们首先需要这次bug修复提交的commit id,我们切到master分支,通过git log可以看到bug fixed的提交记录

image-20220328175845417

然后可以切回到login分支,更新这次bug的提交:

git switch login
git cherry-pick 16bb37

此时查看readme.txt文件:

image-20220328180128979

发现bug确实也跟着被修改了。

4. 标签

标签(tag)是Git提供的,可以对一个版本(即一次commit)打上标签。标签的作用只有标记,即对某一个的版本做一个说明。

比如我们切换到master分支,然后对当前分支的当前版本打一个标签:

git tag version1.0

这样我们就对当前版本多了一个标签描述,当然我们还可以对任意版本加标签,但需要这个版本的commit id,如:

git tag version0.9 b66af6f 

很多同学可能会有疑问,我们在git commit的时候已经通过 git commit -m xxx加了对本次提交的描述了,为什么还需要标签,首先我们规定每次commit都需要加描述信息,但一次commit并不代表一个版本,可能多次commit才会对应一个版本,此时tag就可以在最终那次提交上打上标签。

我们可以通过git taggit show <tagname>查看当前打过的标签:

image-20220328185114133

image-20220328185145678

对于想删除的标签可以通过 git tag -d <tagname>命令,如:

git tag -d version0.9

image-20220328185335976

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。

如果要推送某个标签到远程,使用命令git push origin <tagname>

git push origin v1.0

或者,一次性推送全部尚未推送到远程的本地标签:

push origin --tags

如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:

git tag -d v0.9

然后,从远程删除。删除命令也是push,但是格式如下:

git push origin :refs/tags/v0.9

关于远程仓库可以看第5章。

5. 远程仓库

之前的学习相信大家对Git的使用已经很熟练了,实际开发中我们往往是使用一个远程仓库的,将自己本地提交的代码push到远程仓库中。这里需要大家清楚的是,Git对于版本的管理其实我们都已经说完了,换句话说push命令与版本管理无关。在你进行commit的时候,其实就是版本的管理,至于push只是将自己的版本推到了远程仓库,方便别人同步你的代码。因此在日常开发中,对于一个功能,我们往往需要写多个文件,每写完一个文件可以进行add,这样让文件可以被git管理,保证随时可以回退,多次add后,一个功能可能就写完了,这时再进行commit,统一将暂存区的文件提交到仓库中,这个仓库还只是自己本地的仓库,如果想让自己写的代码被别人同比,此时就需要将自己的仓库push到远程仓库。

对于远程仓库很多公司一般会选择自己搭建一个Git服务器,不过一般我们也知道自己写的代码可以被Github托管,这是因为当你注册一个Github账户的时候,其实就获得了一个免费的Git远程仓库,因此对于远程仓库的讲解,笔者均以Github作为远程操作,由于国内国情,有时Github可能需要科学上网才能连接,大家也可以使用Gitee,本质上是一样的。

对于Github或Gitee如何与本地的Git账户绑定笔者这里不赘述,大家搜相关资料如SSH Key等关键字。笔者默认大家的Git都是关联到远程仓库,可以下载和推送的。

5.1 创建远程仓库

对于远程仓库的使用,无非是自己创建一个仓库,然后将自己的项目提交或自己克隆一个别人的远程仓库下的项目到本地。本节我们先开看看如果创建一个远程仓库,并将自己的本地仓库推送到远程仓库。

首先我们要在Github下创建一个仓库:

首先登录github账户,在当前页面下点击New,其意为新建一个仓库

image-20220327112012037

点击New后,跳转的页面如下,我们首先需要输入仓库名,输入后点击Create repository即可创建一个远程仓库。笔者的仓库名叫gitlearn。

image-20220327112033514

创建完成后,回到当前页面在左侧仓库中就可以看到我们刚刚创建好的仓库,点击进去

image-20220327112417475

我们就可以看到当前仓库的一个远程HTTPS或SSH链接(建议选用SSH,HTTPS在科学上网被代理的时候可能会有些问题)。记住这个链接。

image-20220327112529839

回到我们的本地仓库,在gitlearn目录下,执行git remote add指令

git remote add origin git@github.com:coderZoe/gitlearn.git

需要说明的是,大家的链接肯定跟我的不一样,上述指令git@github.com:coderZoe/gitlearn.git需要以自己的远程仓库链接为准。

此时我们就将本地仓库和远程仓库关联上了,其中orgin就是远程库的名字,这是Git默认的叫法,下面我们就可以将自己本地的仓库推送到远程仓库

git push -u origin master

image-20220327114848213

其中git push指令为git push <remote> <branch>

加了-u是因为对当前分支master是第一次推送,-u参数可以将本地的master分支与远程的master分支关联起来,并且设置origin为我们的默认远程主机,这样后续就不需要-u了。对于分支不了解的可以不用急,我们下面会讲。另外如果是第一次本机和Github链接,会有一个Are you sure you want to continue connecting (yes/no/[fingerprint])?的提醒,直接输入yes即可。

现在让我们来看看自己的远程仓库

image-20220327115346432

可以看到我们确实将本地仓库推送到了远程仓库上且在master分支。

后续如果我们再在本地更改了项目并提交后,都可以通过git push <remote> <branch>来提交。

如果大家在进行git remote add的时候关联的远程仓库地址错了,可以通过git remote rm <name>来解绑当前本地库和远程库,如:

git remote rm origin

5.2 克隆远程仓库

我们用的更多的可能是克隆某个远程仓库,也即将远程仓库的代码下载到本地。这里我们先在远程仓库创建一个仓库,仓库名叫gitclonelearn,并创建一个README.md文件。

image-20220327131155905

创建和项目如下:

image-20220327131318437

此时如果我们想将这个远程仓库克隆到本地,需要使用git clone指令

git clone git@github.com:coderZoe/gitclonelearn.git

image-20220327131538367

进入到gitclonelearn目录下,可以看到我们已经将远程库下载到了本地

image-20220327131631474

5.3 推送和抓取

多人协作的时候,大家基本上都需要频繁的推送自己的代码和同步别人提交的代码。

不知道大家还记不记得我们在将本地仓库与远程仓库管理的时候执行了git remote add origin命令,而且在将自己的本地仓库推到远程仓库的时候执行了git push -u origin,我们之前说过,orgin是远程仓库的名字,我们一般默认叫orgin,那为什么推送的时候还需要带上远程仓库名?

原因很简单,对于一个本地仓库,我们其实是可以关联不止一个远程仓库的,也即大家可以 git remote add origin,还可以git remote add origin2git remote add remoterepository,这只是你对这个远程仓库起的一个名字,Git要求当前仓库关联的远程仓库名都是唯一的,我们可以通过git remote查看当前本地仓库关联的远程仓库都有哪些

image-20220328200639817

还可以通过git remote -v查看不仅仓库名还有仓库的路径:

image-20220328200738646

既然本地仓库可以配多个远程库,那自然push的时候得指定push到哪个远程库了。

git push指令为: git push <远程主机名> <本地分支名>:<远程分支名>

一般我们可能写为:

git push origin master

这个命令表示将当前本地的master分支推送到叫origin的远程库的master分支上,如果origin的master分支不存在就会被新建。

如果是

git push origin :master

则完全不同,刚才我们省略的是远程分支名,现在省略的是本地分支名,这表示要删除origin的master分支,也即我们将一个空的分支(因为没填本地分支)推到了origin的master分支。

我们也可以

git push origin

省略本地和远程分支,这代表将本地当前分支推送到远程对应分支。注意是当前分支,并不是所有分支,另外这种推送需要本地分支和远程分支建立追踪关系,一般我们clone下来的项目都有追踪关系,没有的可以通过

git branch --set-upstream master origin/master

建立追踪分支。上述命令代表将本地master分支与远程origin的master分支建立追踪关系。

如果我们之前git push -u origin配置过默认远程仓库或只有一个远程仓库,那直接可以

git push

这代表将当前分支推送到默认远程仓库对应的分支上。

git push --all origin

这代表将所有分支推到origin远程库上

git push --force origin 

很多时候我们在push的时候,自己本地版本可能和远程主机版本不同,这样Git一般会提醒我们先更新再push,如果大家执意push,就需要加--force这代表覆盖远程仓库的版本,以你的版本为主。大家尽量甚至说杜绝那么做,这是个很危险的指令。

另外多人协作的时候,大家会频繁的推送和拉取,我们以一个场景来模拟下这一情况:

假设你要开发一个登录的功能,你从master上创建一个新的分支叫login,你的同事王五需要开发一个注册的功能,他也从master上创建了一个分支叫register,当前项目的分支就如:

image-20220328203251410

然后你们各开发各自的,对于login和register分支就是:

image-20220328203546611

然后王五那小子开发的比较快,先你一步开发完并将代码合并到了master分支并推送到了远端,现在项目就变成了:

image-20220328203758043

没多久你也开发完了自己的功能,你将自己的代码也合并到了master上,但是是本地的master,这时你从本地master向远端推送的时候:

image-20220328211826938

会看到github会提醒你推送失败了,并告诉你fetch first。

原因很简单,你本地的master已经与现在远程仓库的master不一样了,由于王五的推送,远程的master已经有些新的版本了,因此你需要先拉取回来

git fetch <远程仓库>

将远程仓库的所有更新都取回来,而

git fetch <远程仓库> <分支名>

是将对应分支的更新取回来。

另外我们知道 git branch可以看到当前项目有哪些分支,通过 git branch -r可以看到远程仓库有哪些分支

image-20220328212402751

还有有一点大家需要注意的是,git fetch只是取回远程仓库的更新,它并不做合并,也即你拿到了远程仓库master上的更新,但并没将远程更新的master内容和本地的master进行合并,因此我们需要手动合并

git merge origin/master

此时如果有冲突就需要解决冲突,冲突解决方法和我们之前分支里讲的一样,完成后,我们再进行

git add
git commit
git push

如果没有冲突不需要再add和commit。成功后现在本地仓库和远程仓库就变成了:

image-20220328213839810

其中上图的功能1和功能2是login的功能1和功能2

看到这的时候可能大家可能想问,每次都需要fetch和merge也太麻烦了,有没有两步合一步的指令,自然是有的,它就是git pull

git pull的使用方法与git push很像

 git pull <远程主机名> <远程分支名>:<本地分支名>

比如我想从origin上拉取下来master分支的更新,再将它与本地的master合并

git pull origin master:master

如果分支名相同,可以省略后面的本地分支

git pull origin master

它就相当于

git fetch origin master
git merge origin/master

另外如果当前分支与远程分支存在追踪关系,我们还可以直接简写为:

git pull origin

如果大家只有一个远程仓库或配置过默认远程仓库,则还可以写为:

git pull

看,是不是很简单,和git push很像,一个是提交,一个是拉取合并。

6. Git指令合集

最后给大家上一个Git的指令集合,这个集合其实并不全,很多指令其实后面是可以跟很多参数的,我们本篇讲的Git也只是一些日常使用,更详尽的一些大家可以看Git官网或去看阮一峰老师的博客:分类:开发者手册 - 阮一峰的网络日志 (ruanyifeng.com)

image-20220328174514600

最后修改:2022 年 05 月 11 日
如果觉得我的文章对你有用,请随意赞赏