Git学习——使用Git进行版本控制

概述

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/14/

什么是Git?

Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency. ——Git

The stupid content tracker. ——Linus Torvalds

Git是一个免费的开源分布式版本控制系统,可以高效快速地解决任何项目的版本管理问题。

Git是Linux内核的发明人——Linus Torvalds大佬为了帮助管理Linux内核开发而开发的一套系统。为了开发一个神器而开发的另一个神器。

当你需要和他人协作开发某个大工程时,当你需要清晰明确地管理自己乱七八糟的代码时,当你修改错了什么东西想要恢复时……你都会需要Git的。

总而言之,好处很多,学就完事了。

本篇教程主要来源于Learn Git Branching这个游戏,地址https://learngitbranching.js.org/ 。想图文并茂地学习可以去网站上玩游戏。本篇主要进行翻译记录(理直气壮)和盗图(小声)。

Git Commits——提交

Git仓库中的**提交(commit)**记录了你的仓库目录下所有文件的一个快照,它就像你的目录的一个完全复制,但是不仅仅只是复制。

Git希望保持提交尽可能小,所以每当你提交时,它不是盲目地复制整个目录。如果可能的话,Git会将一次提交压缩成从仓库的一个版本到下一个版本的一组修改,或者称为“Delta”。

Git同时记录着提交的时间,这就是为什么大多数提交都有祖先提交(ancestor commits),我们在图片中用肩头来表示这种关系。记录仓库的历史对于每个维护项目的人来说都有好处。

这只是一种简化的说法,需要理解的东西还有很多,但现在你可以仅仅将提交看作是项目的快照。提交是一个轻量级的操作,两个提交之间的切换很迅速。

让我们看看这在实际操作中是什么样的。下面是一个小型的Git仓库的可视化图像。现在有两个提交——初始化提交c0,以及之后的一个提交c1,c1可能在c0的基础上做了什么改动。

commit1

现在我们做一些修改后输入git commit,就可以将修改提交。我们刚刚做出的提交c2有一个父提交c1,父提交这个概念表明该次提交是在哪次提交的基础上做出的。

commit2

Git Branches——分支

Git中的**分支(branch)**同样轻量级,它仅仅是一个指向某次提交的指针——没了。这就是为什么Git有句名言:

branch early, and branch often.

因为创建很多分支没有多余的存储开销,所以逻辑上用分支划分工作比创建大型的分支要容易得多。

当我们开始混合提交与分支时,我们就能看出这两个特性是如何结合在一起的了。但是现在,仅仅记住,一个分支实际上是指“我想包含该次提交及其父提交的所有工作”。

同样还是这个仓库:

commit1

我们使用Git branch newImage创建一个叫newImage的分支:

branch1

就这样,我们创建了一个新的分支!现在newImage分支指向c1提交。然而当我们对分支做出一些修改,想要提交时,使用Git commit会发生下面的情况:

branch2

啊哦!master分支移动到了下一个提交但是newImage没有!因为我们没有“处于”这个分支上,这就是为什么master分支上有一个*号。

我们使用git checkout(检出)指令来切换分支,这会在我们做出一个新提交之前切换到新的分支上。我们使用git checkout newImage后再git commit:

branch3

搞定!我们的修改现在就被记录在新的分支上了。

Branches and Merging——分支和合并

我们现在已经知道如何进行提交和分支。现在我们需要知道某种方法来将两个不同分支的工作合并起来。这样的话,当我们在开发新特性的时候,就可以创建一个分支开发功能,开发完成后在合并回来。

我们要说的第一个合并工作的方法就是git merge。在Git中**合并(Merging)**会创建一个有两个父提交的特殊提交。一个有两个父提交的提交实际上是指“我想包含两个父提交及其祖先提交的所有工作”。

现在我们有两个分支,每个分支都有一个独立的提交。这就意味着没有一个分支包含了我们对仓库所做的所有工作。

merge1

现在我们用合并来解决这个问题。我们使用git merge bugFix

merge2

哇哦,看见了吗?首先,master现在指向了有两个父提交的提交。如果你从master开始沿着提交树(commit tree)的箭头向上走的话,就可以遇到所有的提交,直到提交树的根。这就意味着master包含着这个仓库的所有工作。

同时,注意到提交的颜色改变了吗?为了帮助学习,我(游戏作者)加入了一些颜色。每一个分支都有自己的颜色。每次提交都会改变颜色,它包含了该提交的所有分支的混合组合。

因此,我们看到master分支的颜色被混合到所有的分支中,但是bugFix分支的颜色没有,我们现在来解决这个问题……

我们现在将master分支合并到bugFix中,使用git checkout bugFix;git merge master

merge3

由于bugFix是master的祖先提交,git没有做任何事情,它仅仅是将bugFix指向master分支指向的同一个提交。

现在所有的提交都是相同的颜色了,这就意味着每个分支都包含了仓库的所有工作,耶!

Git Rebase——变基

第二种在两个分支间合并工作的方法是变基(rebase),变基实际上是接受一组提交,复制后再放在别的地方。

尽管这听起来让人困惑,但是变基的优点在于它可以创建一个漂亮的线性提交序列。如果一个项目只允许变基,那么仓库的提交日志/历史将更加清晰。

现在还是那两个分支。当前操作的是bugFix分支。

rebase1

我们希望将我们的工作从bugFix分支直接转移到master上,这样看起来就像是两个特性是顺序开发的,然而实际上它们是并行开发的。

使用git rebase master指令:

rebase2

太棒了!现在bugFix分支的工作就正好在master的上方,我们就得到了一个漂亮的线性提交序列。

请注意,c3提交仍然存在(在树中用浅色表示),c3‘是我们复制的、变基在master上的副本。

唯一的问题是,master分支还没有被更新,我们来解决这个问题。使用git checkout master命令切换到master分支:

rebase3

我们继续使用git rebase bugFix命令变基到bugFix分支:

rebase4

OK!由于master是bugFix的祖先提交,git仅仅是将master分支向前移动而已。

Moving around in Git——在提交树中移动

在我们学习Git的更多进阶特性之前,首先需要理解在你项目的提交树中移动的不同方法,这是很重要的。

当你习惯了移动切换提交,你使用其它Git命令的能力将大大增强!

首先我们要讨论的是头指针(HEAD),头指针是当前切换到的提交的符号话名称——它实际上就是你现在正在处理的提交。

头指针总是指向工作树中反映的最近的一次提交。大多数改变工作树的Git命令都是从改变头指针开始。

通常,头指针指向一个分支名称(如bugFix)。当你提交时,将更改bugFix的状态,该更改通过头指针可见。

现在我们会展示提交前后头指针的状态。

任旧是这个小仓库。

commit1

我们使用以下指令:git checkout C1; git checkout master; git commit; git checkout C2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxc4HR5B-1591086400742)(https://img.guoziyang.top/images/2020/05/28/head1.png)]

看见了吗?头指针始终指向master分支!

分离头指针指的是将头指针指向一次提交而不是一个分支。这时在操作之前的状态:HEAD -> master -> C1。

当我们使用git checkout C1指令时,就变成了这样:

head2

现在的状态就成了:HEAD -> C1。

Relative Refs——相对引用

在Git中通过指定的提交散列来移动可能会有些无聊。在现实中,你的终端旁不会有一棵漂亮的可视化提交树,所以你需要使用git log指令来查看散列。

除此以外,在实际的Git环境中,散列通常要长得多。例如,指向前一级提交的散列是fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌头别缠到一起啊……

好在,Git在处理散列方面很灵活。你只需要提供足够的散列字符直到Git可以识别到唯一的一个提交即可。所以,我可以用fed2来替代那一长串字符。

正如我所说,通过哈希来定位提交从来都不是最简便的方法,这就是为什么Git有相对引用来解决问题。

有了相对引用,你就可以从某个特定的地方(如bugFix分支或头指针)开始工作。

相对提交很强大,但是我们这里只介绍两个简单的:

  • 使用^来一次向上移动一个提交
  • 使用-<num>来向上移动多个提交

我们先看看补字符操作符^。当你将^添加到一个引用名后时,你就是在告诉Git查找特定提交到父提交。

所以说master^等效于“master分支到第一个父提交”。

master^^是master分支的祖父提交(第二代祖先)。

我们现在使用这个仓库:

commit2

我们输入git checkout master^指令:

relative1

搞定!比输入散列值简单多了!

我们也可以使用HEAD引用作为一个相对引用。我们多次使用就可以沿着提交树向上移动。

relative2

我们使用git checkout C3;git checkout HEAD^;git checkout HEAD^;git checkout HEAD^

relative3

轻松愉快!我们可以用HEAD^返回到过去的提交。

The “~” operator——“~”运算符

假设你需要在提交树中向上移动很多级。有可能你会用许多无聊的^。但是,Git还有波浪操作符(~)来解决这个问题。

波浪操作符(可选)接受一个参数,该参数指定了你希望向上移动的祖先节点的层数。

目前的仓库如下:

tilde1

让我们用~来指定移动的提交数,git checkout HEAD~4

tilde2

就这么简单!相对指针用起来太爽了!

现在你已经是相对引用的大师了。所以我们要用它们做一些实际的事情。

我使用相对引用最通常是移动分支。你可以使用-f选项直接将分支重新指向一次提交,例如git branch -f master HEAD~3将master分支指向了头指针的三级祖先提交(强制)。

我们看看这是怎么实现的,现在仓库是这样的:

tilde3

当我们使用git branch -f master HEAD~3命令时:

tilde4

OK!相对引用给我们提供了一个简洁的方法来将引用C1提交,分支强制(-f)给我们提供了一个方法来快速移动一个分支到那个位置。

Reversing Changes in Git——在Git中撤销更改

在Git中有很多方法来撤销更改。就像提交一样,在Git中撤销更改既有低级组件(给单个文件或区块分块)和高级组件(更改如何被实际上撤销)。我们的应用主要在于后者。

在Git中有两种基础的方式来撤销更改——git resetgit revert

git reset通过将分支引用到一个过去的提交上来达到撤销的目的(重置)。这样的话你可以将其看作“重写历史”。git reset会将分支向前移动,就好像最开始就没有提交一样。

现在有个仓库的提交树如下:

commit2

当我们使用git reset HEAD~1指令时:

reserve1

完美!Git将master分支引用移回到了C1。现在我们的本地仓库的状态处于C1,好像C2没有发生过一样。

尽管重置工作对于本地机器上的本地分支来说足够了,它“重写历史”的方法对于别人正在使用的远程分支来说是无效的。

为了在撤销更改同时,将撤销的更改分享给别人,我们需要git revert(反转)。依旧是上面那棵提交树:

commit2

我们使用git revert HEAD命令:

reverse2

很怪,一个新的提交出现在我们想撤销的提交下面。这是因为新的提交C2’引入的更改恰好与C2的更改完全相反。通过反转,我们就可以将更改分享给他人。

Moving Work Around——移动工作

至今为止我们已经了解了Git的基础——提交、分支以及在提交树中移动。仅仅是这些概念就足以利用Git仓库的90%的功能,满足开发者的主要需求。

然而,剩下的10%在复杂的工作(或者当你陷入困境时)非常有用。我们要介绍的下一个概念是“移动工作”——换句话说,这是一种让开发者以精确灵活地方式表达“我希望这个工作在这儿,那个工作在那儿”的方法。

这看起来内容挺多,但是它其实是个简单的概念。

本章节的第一个命令是git cherry-pick。它的形式如下:

git cherry-pick <Commit1> <Commit2> <...>

这是一种非常直接的方式,表示你希望在当前位置(HEAD)下复制一系列提交。我个人很喜欢cherry-pick,因为它不复杂,很容易理解。

我们看个例子。

movework1

这是一个仓库的提交树,我们想要将side分支的一些工作复制到master分支。这个操作可以通过变基(rebase)的方法解决(我们已经学习过了),但是我们来看看cherry-pick是如何解决的。

我们使用git cherry-pick C2 C4

movework2

就是这样!我们想要C2和C4,Git就将它们直接放在当前位置下方。就这么简单!

Git Interactive Rebase——Git的交互式变基

Git的cherry-pick命令在当您知道想要包含哪个提交(以及对应的散列)时非常有效——它提供了无与伦比的简易性。

但是,如果你不知道你想包含哪个提交时,该怎么办呢?好在,Git把这种情况也考虑到了!这种情况下,我们可以使用交互性变基(interactive rebase)——这是查看你需要变基的一系列提交的最佳方法。

我们慢慢来看。

所有的交互式变基都使用带有-i选项的rebase命令。

如果你包含了该选项,Git将打开一个窗口,用来显示将在变基的目标提交下将要复制哪些提交。它还会显示它们的提交散列和信息,这对于了解什么是什么很有用。

对于“现实中”的Git来说,窗口意味着在文本编辑器(如vim)中打开一个文件。

当交互式变基窗口打开时,你可以做以下三件事:

  • 你可以改变提交的顺序,只需要改变窗口中的顺序即可。

  • 你可以选择忽略某些提交,这是由pick指定的——将pick设为off意味着你想舍弃这个提交

  • 最后,你可以压缩提交,它允许你将几个提交组合在一起。

我们来看个例子,仓库提交树如下:

interactive1

当我们使用git rebase -i HEAD~4命令时,会打开一个交互式变基窗口。随便改变下顺序:

interactive2

我们将顺序打乱,并且将C5的pick设为off,我们confirm看看:

interactive3

搞定!Git按照你在窗口中指定的方式将提交完全复制下来了。

Locally stacked commits——本地栈式提交

以下是一个开发时经常遇到的状况:我试图追踪一个漏洞,但是它相当难以捉摸。为了帮助我的调试工作,我输入了一些调试命令和一些输出语句。

所有的调试/输出语句都在一个单独的提交中。最终,我发现了这个漏洞,修复了它,完美!

唯一的问题是,我现在需要将这个bugFix分支合并到主分支去。如果我只是随便地合并进去,那么主分支中就会出现我所有的调试语句,这显然是不行的。肯定还有另一种方法……

我们需要告诉Git只复制某一次提交。这就像之前学到的一样——我们可以使用完全相同的命令:git rebase -igit cherry-pick来完成这件事。

本节主要是关卡,请到https://learngitbranching.js.org/ 中实际操作。

Juggling Commits——提交!提交!提交!

以下是另一种常见的情况。你有两组相关的更改,分别在newImage分支和caption分支。因此它们在仓库中堆叠在一起(前后相连)。

棘手的是,有的时候你需要对早期的提交做一些小改动。例如,美工希望我们稍稍更改一下newImage的尺寸,但是那已经是很久以前的提交了!

我们会用以下的方式解决这个问题:

  • 我们会使用git rebase -i将提交重新排序,以保证我们需要修改的提交处于最顶层
  • 我们会使用commit —amend做出小改动
  • 接着我们又会使用一次git rebase -i将提交还原回原来的顺序
  • 最后,我们只需要把master分支指向到更新了的部分就搞定了

有很多方法可以实现以上的效果(我知道你在想cherry-pick),稍后我们会看看别的办法,但是现在我们先来关注一下这个。最后,注意一下这里的目标状态——由于我们移动了这些提交两次,它们都添加了两个撇号。多余的一个撇号是为了我们修改的提交添加的,它给出了提交树的最终形式。

这也就是说,我现在可以根据结构和相对撇号的差异来比较层次。只要你的提交树的master分支和我的有相同的结构和相对撇号,就可以给予完全的信任。

本节主要是关卡,请到https://learngitbranching.js.org/ 中实际操作。

Juggling Commits #2——提交!提交!提交! 2

就像你上一章看到的那样,我们使用rebase -i来对提交重新排序。只要我们想要修改的提交处于提交树的顶层,我们就可以很容易地使用--amend修改,再还原回原来的顺序。

这里唯一的问题就是,如果有好多重新排序在同时进行的话,可能会造成变基冲突。我们看看另一种方法,使用git cherry-pick

这里要记住,git cherry-pick将从树中的任何位置将提交放在头节点处(只要该提交不是头指针所指提交的祖先提交)。

我们来看一个小例子:

juggling1

我们使用git cherry-pick C2命令:

juggling2

Git Tags——Git标记

正如你在之前学到的,分支很容易移动,并且在完成它们的工作时经常指向不同的提交。分支很容易发生突变,通常是暂时的,而且一直在改变。

如果这样的话,你可能会想知道是否有办法来永久地标记项目历史中的重要时间点。对于像大版本发布和合并,有没有比分支更加持久的方式来标记这些提交呢?

当然有了!Git的**标记(tag)**恰好可以解决这个问题——它们(在某种程度上)将特定的提交标记成为“里程碑提交”,接着你就可以像引用分支一样引用它们了。

更重要的是,它们不会随着提交的创建而移动。你无法“切换(checkout)“到一个标记,接着在那个标记上完成工作——标记作为指定特定位置的锚点(anchor)而存在。

我们来看看例子:

commit2

我们试着为C1创建一个标记,因为C1是我们的原型一号版本。我们使用git tag v1 C1

tag1

OK!就这么简单。我们将标记命名为v1,接着将它指向了提交C1。如果你不指定提交的话,Git会使用头指针所指的提交。

Git Describe——当前位置描述

因为标记在代码库中充当着十分重要的“锚点”,Git有一个命令来描述你当前相对于最近锚点(标记)的位置。这个命令叫做git describe

git describe可以帮助你在多次向前或向后提交之后找到方向。这种情况可能发生在一次Git二分查找(一种调试搜索)之后,或者你坐在刚刚度假回来的同事的电脑前。

git describe命令的形式是git describe <ref>。<ref>是任何Git可以解析为特定提交的形式。如果你不指定引用,Git就会使用当前的位置(头指针)。

命令的输出看起来是这样的:<tag>_<numCommits>_g<hash>

tag就是距离最近的锚点标记,numCommits是当前位置距离那个标记有多少提交,hash是所描述的提交的散列。

我们来看一个简单的例子:

describe1

我们使用git tag v2 C3给C3加上一个标记:

describe2

命令git describe master就会输出v1_2_gC2git describe side就会输出v2_1_gC4

Rebasing Multiple Branches——对多个分支变基

嘿,我们现在有好多分支了!让我们把所有的工作都从分支变基到master分支吧。

不过,管理人员让这事变得有些棘手——他们希望提交都是按照顺序排列的。这就意味着在我们最终的提交树中,C7‘应当在底部,C6’在它上方,以此类推,都是按顺序的。

本节主要是关卡,请到https://learngitbranching.js.org/ 中实际操作。

Specifying Parents——指定父提交

就像~操作符一样,^操作符也可以接受一个可选的数字参数。

不像~操作符一样指定祖先提交的代数,^操作符的参数指定了从一个合并的提交中引用哪一个父提交。要记住,一个合并的提交有多个父提交,所以选择哪一个是比较模糊的。

常规来说,Git会选择一个合并的提交的“第一个”父提交,但是使用^操作符加上一个参数可以改变默认的行为。

说的差不多了,我们举个例子。

specifying1

这里我们有一个合并了的提交。如果我们切换到master^而不指定参数的话,我们就会切换到合并的提交的第一个父提交。我们使用git checkout master^

specifying2

简单!就像我们一直认为的那个样子。现在我们来指定第二个父提交,使用git checkout master^2

specifying3

看见了吗?我们沿着另一条路径向上走了。

^~操作符让在提交树中移动变得更加强大,对于如下的仓库:

specifying4

我们使用这一串命令,git checkout HEAD~; git checkout HEAD^2; git checkout HEAD~2

specifying5

看看,多快!

更强大的是,这些操作可以被连在一起,只使用一条指令,就像这样git checkout HEAD~^2~2,与上面的命令有同样的效果。

Branch Spaghetti——好多好多分支!

哇哦哦哦哦哦!我们终于达到了这个水平!

现在我们有一个master分支位于onetwothree分支之前。出于某种原因,我们需要使用master分支的最后几次提交的修改版本来更新其它三条分支。

分支one需要重新排序,并且删除C5提交。分支two只需要重新排序。分支three需要一个新的提交。

本节主要是关卡,请到https://learngitbranching.js.org/ 中实际操作。

Git Remotes——远程仓库

远程仓库其实没有你想象的那么复杂。在当今云计算当道的世界,我们很容易就误认为Git远程仓库很神奇,但是实际上它只是将你的仓库复制到另一台电脑上。你可以通过互联网与这台计算机通信,这样就允许你来回传输提交。

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106499156
今日推荐