代码版本管理系统Git介绍与应用实例

Git是一个开源的分布式版本控制系统,它可以有效、高速地处理从小到大的任何项目。

本文将从Git的诞生、原理、使用方法、分支与合并功能以及与GitHub的关系等方面对其进行简要介绍。

零、背景

Git的诞生与Linux内核的开发有着密切的关系。最初,Linux的内核开发使用的是一个商业版的版本控制系统BitKeeper,但由于Linux创始人Linus Torvalds与BitKeeper开发公司的理念不合,双方合作终止。Linus Torvalds也是个狠人,既然商业公司的BitKeeper用不了,那就自己用C语言开发一个更好的版本控制系统吧。于是Linus 闭关一个月,写出了 Git,这就是Git的由来。

江湖传说,BitMover 公司 CEO Larry McVoy 与 Linus 曾是好友, Larry 说服 Linus 在内核开发中使用 BitKeeper。而 BitKeeper 在免费使用的许可证中加入很多限制条件,惹恼了内核开发者,最终促使 Linus 开发出了毁灭 BitKeeper 的 Git。

Git的设计目标是速度、简单、强大的分支管理和完整性。Git最初只是为了管理Linux内核代码,但后来逐渐成为最流行的分布式版本控制系统之一,被许多开源和商业项目所采用。

一、版本控制系统

参考: 《版本控制系统(CVS、SVN、BitKeeper、Git )概念、分类》

在我们的实际开发过程中,经常会有这种需求或问题:

  1. 实际项目开发中,总是需要将源码拷贝多份,以满足不同的需求。例如,每发布一个版本,就需要复制一份来存档当前版本的源码。
  2. 实际项目开发中,基本都是多个人合作完成,在多个人写代码时,就牵扯到代码合并成一份的问题。

这些就是版本控制系统需要解决的问题。

目前最常用的版本控制系统是Git,使用Git进行代码托管的网站包括Github码云Gitee等,此外乌克兰GitLabInc.公司的GitLab 可以用于在企业或校园的局域网中部署私有的代码仓库,用于代码托管。

二、Git的原理

Git与其他常见的版本控制系统(如CVS、SVN等)有很大的不同。Git不是以文件为中心,而是以数据为中心。Git的核心是一个简单的键值对数据库,它可以存储任何类型的内容,包括文件、目录、源代码、图片等。Git把每个文件(或目录)的内容作为一个对象(object)存储在数据库中,并用一个40位的SHA-1哈希值作为对象的唯一标识。Git还有另外一种对象,叫做提交(commit),它记录了一个或多个对象的快照,以及提交的作者、时间、信息和父提交等元数据。通过提交对象,Git可以构建出一个有向无环图(DAG),表示项目的历史版本。

Git的工作区域分为三个部分:工作目录(working directory)、暂存区(staging area)和本地仓库(local repository)。工作目录是用户编辑文件的地方,暂存区是用户暂存修改的地方,本地仓库是用户保存版本的地方。用户可以通过不同的命令在这三个部分之间移动文件和版本,实现版本控制的功能。

三、Git的使用方法和常用指令

Git的使用方法可以分为以下几个步骤:

  • 初始化一个本地仓库或克隆一个远程仓库
  • 在工作目录中修改或添加文件
  • 将修改或添加的文件添加到暂存区
  • 将暂存区的文件提交到本地仓库
  • 将本地仓库的提交推送到远程仓库或从远程仓库拉取更新

Git的常用指令如下:

  • git init:在当前目录下初始化一个空的本地仓库
  • git clone <url>:从指定的URL克隆一个远程仓库到本地
  • git status:查看当前工作目录和暂存区的状态,显示有哪些文件被修改或添加
  • git add <file>:将指定的文件添加到暂存区,如果文件名为.,则表示添加所有文件
  • git commit -m <message>:将暂存区的文件提交到本地仓库,并附上一条提交信息
  • git log:查看本地仓库的提交历史,显示每个提交的哈希值、作者、时间和信息
  • git branch:查看本地仓库的分支,显示有哪些分支,以及当前所在的分支
  • git branch <name>:创建一个名为的新分支
  • git checkout <name>:切换到名为的分支,如果是一个提交的哈希值,则表示切换到该提交的快照
  • git merge <name>:将名为的分支合并到当前分支,如果有冲突,则需要手动解决
  • git push <remote> <branch>:将本地仓库的指定分支推送到指定的远程仓库,如果远程仓库不存在该分支,则会自动创建
  • git pull <remote> <branch>:将指定的远程仓库的指定分支拉取到本地,并与当前分支合并,如果有冲突,则需要手动解决

image.png

以上只是Git的一些基本指令,Git还有很多高级功能和选项,可以通过git help 查看具体的用法和说明。

3.1、Git的分支与合并

参考:

特别提一下Git的分支与合并功能。分支与合并是Git最强大的特点之一。分支可以让用户在不影响主线(master)的情况下,开发新的功能或修复bug。合并可以让用户将不同分支的修改整合到一起,形成一个统一的版本。

Git的分支实际上是一个指针,指向某个提交对象。创建分支的代价很低,因为只需要增加一个指针。切换分支的代价也很低,因为只需要改变HEAD的指向。合并分支的代价取决于分支之间的差异,如果差异较小,合并很快;如果差异较大,合并可能需要解决冲突。

Git 会有很多合并策略,其中常见的是 Fast-forward、Recursive 、Ours、Theirs、Octopus。默认 Git 会自动挑选合适的合并策略,如果用户需要强制指定,使用git merge -s <策略名字>

  • Fast-forward 是最简单的一种合并策略, 是 Git 在合并两个没有分叉的分支时的默认行为,Git 只需要将 master 分支的指向移动到最后一个 commit 节点上即可完成合并。
  • Recursive 是 Git 分支合并策略中 最重要也是最常用的策略 ,是 Git 在合并两个有分叉的分支时的默认行为。其算法可以简单描述为:递归寻找路径最短的唯一共同祖先节点,然后以其为 base 节点进行递归三向合并。
  • Ours 和 Theirs 这两种合并策略也是比较简单的,简单来说就是保留双方的历史记录,但完全忽略掉某一方的文件变更。具体来说,Ours 是只采用自己这一方的文件变更而忽略对方的变更,Theirs 是只采用对方的文件变更而忽略自己这一方的变更。
  • Octopus 合并策略用于多条分支(大于等于三条)的合并,一般用于测试环境或预发布环境将多个开发分支修改的内容合并在一起。

虽然Git的合并策略很多,并且部分合并策略的原理也很复杂,但一般我们在使用Git时不需要考虑这些问题, Git会帮助我们选择最适合的那个策略。

四、一个例子

为了更好地理解Git的工作流程,这里举一个小例子。假设我们正在开发一个名为HelloWorld的项目,这个项目使用Git管理代码。同时在Github上,我们以yourname的账号建立一个叫做”HelloWorld”的远端存储库。我们需要实现添加代码并提交到远端存储库,以及创建分支和合并分支的操作。下面是具体步骤:

首先,我们在工作目录下创建一个名为HelloWorld的文件夹,并在里面创建一个名为hello.py的文件。这个文件是项目的主要代码,内容是:

1
2
# hello.py
print("Hello, world!")

接下来,在我们的工作目录下执行git init命令,这样,Git就会在文件夹下创建一个名为.git的隐藏文件夹,这个文件夹是我们的本地仓库,它用来存储项目的所有版本信息。

1
2
3
4
5
6
/home/HelloWorld $ git init
git init
Initialized empty Git repository in /home/HelloWorld/.git/

/home/HelloWorld $ ls -a
. .. .git hello.py

然后,我们执行git add hello.py命令,这样,Git就会把hello.py文件添加到暂存区,暂存区是一个临时的区域,它用来存储我们准备提交的文件。

接下来,我们需要执行git commit -m "First commit"命令,这样,Git就会把我们的暂存区的文件提交到本地仓库,并附上一条信息,表示这是我们的第一次提交。这时,我们的本地仓库就有了一个提交对象,它记录了我们的hello.py文件的内容,以及我们的用户名、邮箱、时间和信息。

1
2
3
4
/home/HelloWorld $ git commit -m "First commit"
[main (root-commit) cbc7607] First commit
1 file changed, 1 insertion(+)
create mode 100644 hello.py

然后,我们需要在GitHub上创建一个名为HelloWorld的远程仓库,这是一个网上的空间,它用来存储和分享我们的项目。我们需要复制我们的远程仓库的URL,例如 https://github.com/yourname/HelloWorld.git

接下来,我们需要执行 git remote add origin https://github.com/yourname/HelloWorld.git 命令,这样,Git就会把我们的远程仓库的URL与一个 名为origin的别名 关联起来,这样,我们就可以用origin来代替我们的远程仓库的URL,方便我们的操作。

然后,我们需要执行git push origin main命令。这样,Git就会把我们的本地仓库的main分支推送到我们的远程仓库的main分支,这时,我们的远程仓库就有了和我们的本地仓库一样的内容,我们的项目就成功地上传到了网上。

注意,Github最开始的默认的分支名都为 master ,因此许多老教程里面会用master分支作为默认分支进行教学。2020年发生在美国的一系列社会冲突对开源社区也造成了一定的影响,从那时开始,微软公司使用main分支取代master分支作为默认分支。更多内容可以参考 《为什么Git分支开始从“master”变为“main”了? 》

1
2
3
4
5
6
7
8
9
10
/home/HelloWorld $ git push -u orgin main  # `-u` 参数代表"set-upstream" ,建议带上
Username for 'https://github.com': yourname
Password for 'https://yourname@github.com':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 229 bytes | 114.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/yourname/helloworld.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'orgin'.

接下来,我们想给项目添加一个新的功能,让它可以打印出我们的名字。我们不想直接修改我们的main分支,因为这样可能会影响我们的稳定版本,所以我们决定创建一个新的分支,叫做feature。我们需要执行git branch feature命令,这样,Git就会在我们的本地仓库中创建一个名为feature的分支,它指向我们的当前提交,也就是我们的第一次提交。

然后,我们需要执行git checkout feature命令,这样,Git就会切换到我们的feature分支,这时,我们的工作目录和暂存区的内容也会变成我们的feature分支的内容,也就是我们的第一次提交的内容。

1
2
3
/home/HelloWorld $ git checkout feature
M hello.py
Switched to branch 'feature'

我们可以在我们的工作目录下修改我们的hello.py文件,添加如下代码:

1
2
3
4
# hello.py
print("Hello, world!")
name = input("What is your name? ")
print("Hello, " + name + "!")

接下来,我们需要执行git add hello.py命令,把我们修改后的文件添加到暂存区,然后执行git commit -m "Add name feature"命令,把我们的暂存区的文件提交到本地仓库,并附上一条信息,表示这是我们添加的新功能。这时,我们的本地仓库就有了一个新的提交对象,它记录了我们的修改后的文件的内容。我们的feature分支也会指向这个新的提交,而main分支仍然指向我们的第一次提交。

1
2
3
/home/HelloWorld $  git commit -m "Add some feature"
[feature 54ab6eb] Add some feature
1 file changed, 3 insertions(+), 1 deletion(-)

然后,我们需要执行git push -u origin feature命令,把本地仓库的feature分支推送到我们的远程仓库的feature分支,这时,我们的远程仓库就有了一个新的分支,它和我们的本地仓库的feature分支一样,包含了我们的新功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
/home/HelloWorld $ git remote add helloworld https://github.com/yourname/helloworld.git  # 远程仓库还需要再添加一次                                                    
/home/HelloWorld $ git push -u origin feature # `-u` 参数代表"set-upstream" ,建议带上
Username for 'https://github.com': yourname
Password for 'https://yourname@github.com':
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 296 bytes | 148.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/yourname/helloworld.git
cbc7607..54ab6eb feature -> feature
Branch 'feature' set up to track remote branch 'feature' from 'helloworld'.

接下来,我们想把我们的新功能合并到我们的主线上,让我们的项目变得更完善。我们需要执行git checkout main命令,切换回我们的main分支,然后执行git merge feature命令,把我们的feature分支合并到我们的main分支。

1
2
3
4
5
6
7
8
/home/HelloWorld $ git checkout main
Switched to branch 'main'
Your branch is up to date with 'orgin/main'.
/home/HelloWorld $ git merge feature
Updating cbc7607..54ab6eb
Fast-forward
hello.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)

这时,Git会自动创建一个新的提交对象,它记录了我们的两个分支的合并结果,以及我们的用户名、邮箱、时间和信息。这个提交对象有两个父节点,分别是我们的main分支和feature分支的最新提交。我们的main分支也会指向这个新的提交,而我们的feature分支仍然指向我们的第二次提交。

然后,我们需要执行git push origin main命令,把我们的本地仓库的main分支推送到我们的远程仓库的main分支:

1
2
3
4
5
6
7
/home/HelloWorld $ git push -u origin main
Username for 'https://github.com': yourname
Password for 'https://yourname@github.com':
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/yourname/helloworld.git
cbc7607..54ab6eb main -> main
Branch 'main' set up to track remote branch 'main' from 'helloworld'.

这时,我们的远程仓库的main分支也会更新为我们的合并后的版本,我们的项目就完成了一个完整的开发和发布流程。

五、尾声

Git是一个开源的分布式版本控制系统,它可以有效、高速地处理从小到大的任何项目。Git的诞生与Linux内核的开发有关,Git的原理是基于一个简单的键值对数据库,Git的使用方法是通过一系列的指令在工作目录、暂存区和本地仓库之间移动文件和版本,Git的分支与合并功能是Git最强大的特点之一,Git与GitHub的关系是工具与平台的关系。Git与GitHub的结合,使得开源项目的开发和贡献变得更加便捷和高效。