防范胜于救灾!版本管理是你的后悔药!

作者还在研究生阶段时,犯过一个刻骨铭心的错误:那是夜里两点,在 6 个小时之后,我们研发的系统就要在某个会议室里,交付给近 20 名使用者使用,而这些人将使用我们的软件,判定近 1 万青年人的前途。就在这个时候,我们发现之前通过了测试、已经打包好的版本,在清理磁盘空间时被删除了;但在那个版本之后,我们又改动了一些代码。这些改动包含了功能增强、小 bug 的修复,大约是一天多的工作量,未经测试,不能发布。

今天的程序员很难理解这为什么会是一个大问题。但在当时,由于缺乏版本管理工具(cvs 推出没有几年,还刚刚进入国内,在校园里的我们不了解也是情有可原),我们面临十分尴尬的局面:既没有现成可发布的软件,也没有一种办法能轻易地让代码回滚到通过测试的那个版本,从而构建出一模一样的版本。

那是一个痛苦的夜晚。一些人开始准备善后方案,我们则带着愧疚的心情,努力试图让代码尽可能地回滚到通过测试的那个版本的状态。

如果有 CI/CD,这种失误几乎不可能发生。如果有版本控制的话,即使发生了这种失误,纠错代价也会小很多。

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。在本书写作时,作者就使用了版本控制,包括正文内容和附带源码。

如果你是一名图形或网页设计师,可能会需要保存某一幅图片或页面布局文件的所有修订版本(这或许是你非常渴望拥有的功能),采用版本控制系统(VCS)是个明智的选择。有了它你就可以将选定的文件恢复到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时导致了某个功能缺陷等等。使用版本控制系统通常还意味着,就算你乱来一气把整个项目中的文件改的改删的删,你也照样可以轻松恢复到原先的样子,但额外增加的工作量却微乎其微。

回到我读研的那个年代,许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会在目录名上加上备份时间以示区别。这么做唯一的好处就是简单,但是特别容易犯错。有时候会混淆所在的工作目录,一不小心会写错文件或者覆盖意想外的文件。

为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。在这个时期最流行的版本控制系统可能是 RCS。其工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。

RCS 的缺点是它不支持分支操作(我们后面详细介绍什么是分支操作),加上它只能管理本地文件,无法支持多人协作。因此又发展 C/S 架构的集中化版本控制系统 (Centralized Version Control Systems,简称 CVCS),比如 CVS, subversion 以及 perforce。这些系统的基本原理是,有一个单独的服务器充当集中式的文件存储和版本库,所有的文件都必须通过这台服务器来访问,这样就可以有效地控制谁可以干什么事情。CVCS 的缺点是必须联网才能工作,如果在局域网环境下搭建服务器,还需要一台 24 小时开机的电脑作为服务器,而且所有的文件都必须通过这台服务器来访问,这样就会造成访问速度的瓶颈。

为了解决这些问题,人们又开发了分布式版本控制系统 (Distributed Version Control Systems,简称 DVCS)。比如 Git,Mercurial,Bazaar 等。DVCS 的基本原理是,每个开发者的电脑上都是一个完整的版本库。当开发者克隆了某个项目后,就拥有了这个项目的完整历史版本。如果在本地作了一些修改,想要与其他人分享,只需要把本地的版本库推送到服务器上即可。此后,其他人就可以从服务器上抓取最新的版本到本地,然后与自己的修改进行合并,再推送到服务器上。这就形成了一个分布式的协作开发模式。

在本书中,我们只介绍 Git 这一种版本控制系统。

2. 版本管理工具 Git

Git 诞生于 2005 年,它的开发者 Linus Torvalds 同时也是 Linux 操作系统的缔造者。它性能优越,适合管理大项目,有着令人难以置信的非线性分支管理系统,是目前世界上最流行的代码管理系统之一。

Git 在接口上基本延续了大多数版本控制系统的概念和 API,但在底层设计上却风格迥异,从而成就了它强大的引擎。它的主要特点是:

  1. 直接记录快照,而非差异比较:其他版本控制系统将不同版本之间的差异提取为增量进行记录,当要提取最新文件时,需要从最初始的版本开始,把到最新的版本之间的所有差异一路合并起来。早期计算机存储资源比较宝贵,因此前几代的版本控制系统采用这种设计,可以减少对存储资源的占用。Git 则反其道而行之,它把文件当作是对特定文件某一时刻的快照。每次你提交更新,或者保存项目的当前状态,Git 都会对当时的全部文件制作一个快照并保存这个快照的索引。Git 的这种设计使得 Git 非常适合处理大型项目,而且速度非常快。

  2. 几乎所有的操作都是本地执行:Git 的设计目标之一就是保证速度。Git 的主要操作都只需要访问本地文件和资源,几乎所有的信息都可以在本地找到,所以 Git 非常快。Git 的另一个设计目标是能够可靠地处理各种非线性的开发(分支)历史。Git 的分支和合并操作非常高效。

  3. Git 的完整性保证: Git 中的所有数据在存储前都计算校验和,然后以校验和来引用。这意味着,Git 在存储和传输数据时,会自动发现数据的损坏。

  4. 追加式操作:你执行的 Git 操作,几乎只往 Git 数据库中添加数据。也就是说 Git 几乎不会执行任何可能导致文件不可恢复的操作。这使得我们使用 Git 成为一个安心愉悦的过程,因为我们深知可以尽情做各种尝试,而没有把事情弄糟的危险。

接下来,我们将主要按照 git 的使用场景顺序,由浅入深地介绍 git 命令与技巧。

2.1. 创建 git 仓库

Git 仓库(有人也称作存储库)是项目的虚拟存储。它允许您保存代码的版本,您可以在需要时访问这些版本。

通常,我们有两种方式来创建 git 仓库,取决于你的开发工作是如何开始的。不过,在正式介绍之前,我们先看一个最基础的设置命令。

当你在某台机器上(或者某个工程中)初始使用 git 时,我们需要做的第一件事就是设置你的用户名和邮箱地址。这些信息在每次提交时都会用到。

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

这里我们使用了–global 选项,如此一来,在同一台机器上仅需配置一次,所有的项目都将复用这个设置。但如果你同时为多个项目工作,并且希望在不同的项目中,使用不同的用户名和邮箱地址的话,就不要使用这一选项。其结果是,这些配置将会被写入到当前项目的.git/config 文件中,而不会影响到其它工程。当然,这种按项目的配置,就必须等到项目仓库设置完成之后才能进行。

现在,让我们创建一个本地仓库。

2.1.1. 创建新的本地仓库:git init

要创建新的存储库,您将使用 git init 命令。git init 是您在新存储库的初始设置期间使用的一次性命令。执行此命令时,git 将在您当前的工作目录中创建一个新的子目录。这也将创建一个新的主分支。

此示例假设您已经有了一个现存的项目文件夹,希望把它加入的 git 的版本控制。我们需要先进入该项目的根目录,再运行 git init 命令。

$ cd /path/to/your/project/root
$ git init

这将创建一个名为。git 的子目录。这个子目录含有 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。但是,在这个时候,我们仅仅是做了一个初始化的操作,你的项目里的文件变化还没有被跟踪(如果我们是通过从远程仓库克隆来创建的仓库,则所有的文件变化都已经被跟踪了)。

2.1.2. 克隆现有存储库:git clone

如果项目已经在中央存储库中建立起来,克隆命令是我们获取版本的最常用方式。

假设 Github 是我们的服务器,我们需要把本书在 Github 上的仓库克隆到本地:

$ git clone git@github.com:zillionare/best-practice-python.git

这将在当前目录下,创建一个名为 best-practice-python 的文件夹,所有的文件和 git 信息都将保存在这个文件夹下。

除了上述方式还,我们还可以使用 https 协议来克隆:

$ git clone https://github.com/zillionare/best-practice-python.git

或者使用 Github CLI 来克隆:

$ gh repo clone zillionare/best-practice-python

Github CLI 有着强大的功能,通过它可以实现许多自动化工作,我们将在稍后介绍。

2.2. 建立与远程仓库的关联:git remote

如果本地仓库是通过 git clone 建立起来的,那么它就已经和远程仓库建立了关联。如果是通过 git init 建立起来的,我们还需要通过 git remote 命令来建立这种关联,以便后续我们可以将本地的改动,推送到远程服务器上。

如果你是为该项目第一个创建仓库的人,那么很可能你还得登录到服务器上,创建一个空的中央仓库。如果你是一个团队的一员,那么你可能已经有了一个中央仓库,你需要知道它的 URL。

象 Github, gitlab,或者 bitbucket 这些代码托管平台,都提供了 web 界面,我们可以登录到 web 界面上进行操作。

如果我们使用的托管平台是 github,还可以使用它的命令行工具 Github CLI 来进行操作。以下命令将在 github 上创建一个公开的仓库,名字为 sample。在很多场合下,这个名字也被称为 project_slug。

$ gh repo create sample --public

显然,上述命令执行前是需要鉴权的。

现在,既然已经有了远程仓库,也得到了它的 URL,我们可以来建立本地与远程之间的关联了:

# 请替换下面语句中的{{GITHUB_USER_NAME}}为你的 GITHUB 用户名,{{PROJECT_SLUG}}为你的项目名
# 比如, GIT REMOTE ADD ORIGIN GIT@GITHUB.COM:ZILLIONARE/SAMPLE.GIT
$ git remote add origin git@github.com:{{github_user_name}}/{{project_slug}}.git

这个命令还为远程仓库定义了一个别名,即 origin。这个别名可以是任意的,比如,既然我们的服务器使用了 github,我们也可以将别名声称为 github。不过 origin 是 git 默认的别名,因此,多数人一般都使用这个别名。定义了别名之后,我们就可以在其它命令中使用别名来代替远程仓库的地址,从而可以使命令变简洁。

2.3. 保存更改:add, commit, stash 等

当我们修改了工作区的一些文件(包括新建文件、修改文件内容和删除文件)时,我们需要保存这种更改到版本控制系统,或者暂时贮藏起来。这就需要 add, commit 和 stash 等命令。

# 将根目录下的文件及文件夹递归地加入跟踪,暂存模式
$ git add .

# 进入提交状态
$ git commit -m 'initial project version'

此时用 git branch -v 命令看一下,我们会发现,我们已经处于 main 分支上,而且已经有了一个 commit。如果分支名是 master,建议运行以下命令,将其改为 main:

$ git branch -M main

在这里插入图片描述

您已经注意到,我们将变更保存到 git 系统中的步骤不止一步。实际上,git 中的很多操作都是多阶段的,常常会经历一个修改(modified)、暂存(stagged),提交(committed)和推送(push)的过程。要理解这几个阶段,我们还得先深入介绍一下相关的三个基本概念:HEAD, index 和 working tree。

头(HEAD)

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。同时也是下一次提交的父结点。在某些场景下,我们也可以把 HEAD 看作是分支的代名词。它是我们执行 git commit 命令后,变更要去往的地方。

索引(index)

当我们调用 git add 命令时,我们就会把变更记录到索引区。然后再从这里把变更提交到分支。更多时候,人们会用暂存区这个词,但 index 是 Git 中使用的规范术语。

工作目录(working tree)

最后,我们得有自己的工作目录(通常也叫工作区)。另外两棵树(即 HEAD 和 INDEX)会以一种高效但并不直观的方式,将它们的内容存储在.git 文件夹中。而工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。

下图则展示了这三个概念的关系:

我们结合命令和图示来解释变更的流动。

$ git status

On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")

status 命令显示文件处在工作区(working directory),还没进入到暂存区。

此时在 vscode 侧面板上,我们可以看到 README.md 文件出现在 changes 类别下(注意 vscode 使用的术语与 git 不同,这是可以理解的),

我们可以通过 git add 命令将 README.md 文件添加到暂存区,然后再次查看状态:

$ git add README.md
$ git status

On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   README.md

Status 命令提示 README.md 还没有提交(此时文件处在暂存区 staging area 中)。此时在 vscode 侧面板上,我们可以看到 README.md 文件出现在 Staged Changes 类别下,如下图所示:

接下来,我们提交到本地仓库,并再次查看当前状态:

$ git commit -m "update README.md"
$ git status

On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

现在,三个区域的状态完全一致(见上面输出中的最后一句:nothing to commit, working tree clean)。但还有一条提示:本地分支领先远程 main 分支一个提交(Your branch is ahead of ‘origin/main’ by 1 commit),即我们刚刚提交的变更还没同步到远程服务器。此时,我们刚刚执行的提交将可以在 COMMITS 类别下找到。

如下图所示,此时在 vscode 侧面板上,SOURCE CONTROL 类别下已清空,只出现了一个 sync changes 的按键,一旦我们点击此按钮,我们刚刚提交的变更就会发布到远程服务器上。

这种情况下,我们就需要用到 stash 命令。我们先介绍 git stash 命令行模式的一些例子,然后再介绍如何从图形界面来使用它:

# 贮藏当前未提交的变更
$ git stash
Saved working directory and index state \
  "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")

# 查看所有的贮藏
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log

# 应用最近的贮藏,但不删除它。
$ git stash apply
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   index.html
	modified:   lib/simplegit.rb

no changes added to commit (use "git add" and/or "git commit -a")

# 应用最近的贮藏,并删除它
git stash pop

# 直接删除最近的贮藏
git stash drop

当我们应用贮藏时,贮藏中的变更会被应用到当前的工作区,这也可能发生冲突,需要我们手动解决。在上述命令中,我们没有指定要操作的贮藏的名字,所以操作的目标都是最近的贮藏。但我们也可以在操作时,指定 stash 的名字。

在 vscode 的 git lens 扩展中,我们可以在"Changes"面板中,将当前的变更贮藏(如图 8- 5 所示):

上图中红色框内的按钮即是 stash all 按钮。我们点击它,就可以将当前的变更贮藏起来了。我们可以在"Stashes"面板中,查看所有的贮藏:

点击"Apply"按钮,在弹出来的对话框中,给我们两个提示,一个是只应用,不删除该贮藏;另一个是应用并删除该贮藏:

最后,我们介绍一下.gitignore 文件。有一些文件我们并不想被 git 跟踪,比如 IDE 产生的临时文件、日志文件,涉及到密码的某些文件(比如.env) 等等。这些文件我们可以在.gitignore 文件中进行配置,git 就会忽略这些文件。

.gitignore 文件的每一行都是一个符合 glob patternglob 的字符串匹配模式。被该模式匹配到的文件(或者文件夹),将不会被 git 跟踪。我们可以手动编辑这个文件,但一般地,我们应该使用相关的扩展来协助管理此文件。

2.4. 与他人同步变更:git push 和 git pull

前面的操作只是把变更记录在本地的 git 系统中。为了使得这些变更能为他人所见,我们得把这些变更推送到远程仓库中。

$ git push -u origin main

-u 参数是为了将本地的 main 分支与远程的 main 分支关联起来,在以后的推送或者拉取分支时,就可以简化命令:

# 当我们不指定分支名时,GIT 会使用当前分支
$ git push

# 从远程仓库拉取其它人所做的变更
$ git pull

注意,本地仓库与服务器关联的命令(即git remote add)只需要执行一次;而每次创建了新的分支后,都要在第一次往该分支推送变更时,执行git push -u ...这个命令,以便在推送的同时,也完成本地分支与远程分支的绑定。一旦绑定完成,在随后的推送(或者拉取)动作中,就可以省略-u 参数。

2.5. git 标签

随着开发的进行,终有一天,我们会到达某个重要的节点,比如某个版本的完成。此时我们就会给仓库的状态打上标签,方便回溯。这就需要用到 git tag 的一系列命令。

# 列出所有的标签
$ git tag
v0.9
V1.0

# 有筛选地列出标签
git tag --list "v1.0.*"

v1.0

# 创建标签:使用-A 选项指定标签名,-M 指定标签的描述文字
$ git tag -a v1.4 -m "my version 1.4"
$ git tag
v0.9
v1.0
v1.4

$ git tag -a v1.5 -m "my version 1.5"
$ git tag
v0.9
v1.0
v1.4
v1.5

# 查看标签信息
$ git show v1.5

tag v1.5
Tagger: zillionare 
Date:   Mon Jan 23 15:10:55 2023 +0800

second

commit 57eb735f513c753e49b2fe3005ccfa9b3412762d (HEAD -> main, tag: v1.4, tag: v1.5, origin/main)
Author: zillionare 
Date:   Mon Jan 23 10:19:28 2023 +0800

这里我们稍作停顿,解释下标签究竟意味着什么。前面我们说过,创建标签是给仓库的某个状态打上标记的动作。在上面示例中的第 14 行和第 15 行,我们在没有任何新的提交的情况下,连续创建了两个不同的标签。考虑到我们的仓库是一个线性的提交历史,那么这两个标签究竟指向了什么呢?随后,第 23 行的命令输出显示了,这两个标签指向了同一个提交,即 57eb。这两个标签关联的都是 57eb 提交之后的仓库状态。这也说明,标签与提交本身不是一对一的映射,而是多对一的映射。

既然标签是指向提交的,您可能会想,我们是不是可以为过去的某个状态追加标签?您的猜测是正确的,我们确实可以这样做:

# 假设我们又向仓库做了若干提交之后,发现需要对'57EB'这个提交打上标签
git tag -a v1.6 57eb -m "my version 1.6"

在这里插入图片描述

我们前面讲过,几乎所有的 git 操作都是多阶段的,创建 tag 也是一样。当我们执行git tag -a命令时,我们只在本地仓库创建了这个标签,我们仍然需要把它同步到远程服务器上:

# 一次性地将所有标签推送到远程
$ git push origin --tags

# 仅推送特定标签
$ git push origin v1.6

也有可能我们需要删除标签,这也是一个两阶段的操作

# 从本地删除 V1.4 这个标签
$ git tag -d v1.4

# 从远程删除 V1.4 这个标签。这一步可以与上一步独立运行
$ git push origin --delete v1.4

标签创建以后,我们使用到该标签的常见操作是检出这个标签指向的文件版本,比如在 CI 服务器上检出这个版本制作构建(build)。此时我们可以使用 git checkout 命令:

git checkout v1.4

如果我们签出某个标签的目的是为了进行修改,注意这种情况下,我们应该为这个检出创建一个新的分支。然后我们就可以在这个工作区里进行修改,再推送到服务器上。如果我们不创建新的分支,而直接对签出到工作区的代码进行修改,会导致仓库处于“分离头指针 (detached HEAD)”的状态。这种状态下,我们可以进行修改,但无法提交,也就是说,这些修改最终会丢失。

至此,我们基本上接触到了 git 中全部基础的操作。我们学会了如何创建仓库,跟踪变更,提交变更,与远程服务器同步,并且创建标签以记录开发中的重要时刻。当然还有一些查错性质的操作我们没有介绍,比如查看日志 (git log),读者可以自行了解。另外,在掌握 git 的工作原理之后,我们更倾向于通过图形界面来管理 git 仓库,因此象查看变更历史这样的操作,也应该通过图形界面来查看。

在这里插入图片描述

然而,git 中真正高级的技巧都与分支相关,这也将是我们接下来要讲述的内容。

  • 19
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

量化风云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值