您有一篇git 原理,请注意查收

把人生一分为二,前半生不犹豫,后半生不后悔。------溥仪

大家好,我是柒八九

前言

作为一个新时代的开发者,想必大家在工作中,有一样东西是和大家形影不离 的。那就是git。(当然,这里也有个例,如果大家项目还停留在svn阶段,就算我刚才的话唐突了)。

无论大家平时是喜欢在命令行中手搓git命令,还是利用git可视化工具(SourceTree)进行代码管理。终究都逃不过,add/commit/merge/push等命令的支配。所以,今天我们来聊聊,在进行这些命令的时候,在最底层到底发生了啥。

还有一点,也算是一个认知提升吧。需要和大家唠叨一下,以后遇到比较棘手的问题,可以往这方面来靠拢

所有软件的底层实现都是操作和管理数据

无论是我们平时用到的桌面程序,亦或是在命令行中进行敲敲打打处理一些特定的操作,还有就是我们熟悉的编程开发中,无论是前端的开发过程中,使用原生也好,各种框架也罢,最后的根结都是数据的罗列和排布;还是后端就更明显了,有SQL的操作,那就更是再玩弄 数据。 之所以我们看到的现象有些不同,无非就是数据的表现形式和处理方式的不同。可以说,在编程界,--万物皆数据

这里简单举一个例子,日历大家都见过哇。

如果,给我们一个需求,要让我们实现一个飞书日历或者google 日历的开发任务,我们是不是一时感觉到无法下手。

那我们往万物皆数据 这个定论上靠,那是不是每一个日程(无论是简单日程还是重复日程),它们本质上就是在每个小格子上展示。无非就是有的日程在单个格子上,有的日程是跨格子。 而针对这种情况,是不是就是在当前视图中,我们需要维护一个数组,而这个数组中的项就是每个格子的示例。(针对月视图/周视图/日视图的数据,其实都是一套,只不过在框架内部为我们提供了各自的展示逻辑)

这是一个开源的日历库(FullCalendar)。

而下面的events就是我们在日历上显示的日程信息。

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 前置知识点
  2. git init
  3. 新增一个文件
  4. git commit
  5. 新增修改
  6. 创建分支
  7. 分支切换
  8. 分支合并
  9. 远程提交

1. 前置知识点

前置知识点 ,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履 。以下知识点,请酌情使用

什么是git

Git是一种用于源代码管理的工具。它是一个免费且开源的版本控制系统,用于高效地处理从小型到非常大型的项目。Git用于跟踪源代码的更改,使多个开发人员能够共同在非线性开发中合作。Git是由Linus Torvalds2005年为Linux内核的开发而创建的。

集中式管理

在使用Git之前在维护代码之前,团队合作的模式如下:

  • 开发人员过去会将他们的代码提交到中央服务器,而没有自己的副本。
  • 对源代码所做的任何更改对其他开发人员来说都是未知的
  • 没有任何开发人员之间的沟通。

它的典型代表为SVN

分布式管理

  • 每个开发人员都在其本地系统上拥有完整的代码副本
  • 对源代码所做的任何更改都可以被其他人跟踪
  • 开发人员之间有定期的沟通。

毋庸置疑,git是这方面的王者。

git基础概念

  • workspace:是本地项目的工作目录,属于本地代码发生更新但尚未执行 git add 命令时的状态working tree的状态也随之更新
  • index:是索引文件,它是连接working treecommit的桥梁,每当我们使用git add命令来登记后,index file的内容就会改变,此时index file就和working tree同步了。
    • 它还有一个家喻户晓的名字 -暂存区
  • local repository:是本地仓库 ,当我们使用git commit命令提交最新代码时,代码才真正进入git仓库。
    • git commit -m "xxx" 就是index 里的内容提交到本地仓库中
  • remote repository:是远程仓库 ,当我们使用git push命令时就会将本地仓库的代码上传至远程仓库,完成整个代码的上传工作

git init --bare VS git init

git init --baregit init 是两种不同的Git初始化命令,它们用于创建不同类型的Git仓库。

下面是它们之间的主要区别:

  1. 仓库类型:

    • git init: 这个命令用于创建一个标准的Git工作目录仓库 。它会在当前目录下创建一个.git子目录,其中包含Git的版本控制文件和历史记录(这是我们这篇文章的重点)。这种类型的仓库通常用于开发和维护代码。

    • git init --bare: 这个命令用于创建一个"裸"(bare)仓库,它不包含工作目录。这意味着它只包含Git版本控制的文件和历史记录,没有实际的项目文件 。"裸"仓库通常用作中央版本库,用于协作和共享代码。

  2. 默认分支:

    • git init 默认创建一个带有master分支的工作目录仓库。

    • git init --bare 默认不创建分支,因为裸仓库不包含工作目录。我们需要手动创建和设置分支。

一般情况下,如果我们需要创建一个新的Git仓库用于开发和维护代码,我们应该使用 git init。如果我们需要创建一个中央版本库用于团队协作和共享代码,我们可以考虑使用 git init --bare


Hook

钩子(Hooks)是一种通用概念,通常用于在特定事件发生时触发自定义代码。虽然不是编程语言本身的一部分,但编程语言和开发工具通常提供一些机制来支持编写和使用钩子。

下面我们简单介绍几种大家比较常见的利用Hook概念的技术。

名称 描述 示例语法
Git Hooks Git 允许在代码仓库的特定事件上运行自定义脚本。事件包括提交、推送、合并等。 使用 Bash 脚本编写,如 pre-commitpost-commit 等。
JavaScript Hooks JavaScript 用于前端和后端开发,事件处理程序在特定事件发生时执行自定义 JavaScript 代码。 前端中,事件处理程序如事件监听器。后端中,使用 EventEmitter 模块。
React Lifecycle Hooks React 用于构建用户界面,包括生命周期方法,允许在组件的不同生命周期阶段运行自定义代码。 生命周期方法如 componentDidMountcomponentWillUnmount。 当然,还有甚嚣尘上的针对函数组件的React Hook
GitHub Webhooks GitHub 提供 Webhooks,是 HTTP 回调,用于在存储库的特定事件上触发自定义操作。 开发者编写 Webhook 处理程序响应事件,配置 Webhook URL。
Jenkins Pipeline Hooks Jenkins 是一个持续集成工具,允许创建自定义流水线脚本。使用钩子定义流水线的阶段和操作。 钩子嵌入到 Jenkinsfile 中以定义流水线。

Git Hook

Git Hook是一种非常强大的Git自定义脚本系统 ,它允许我们在Git版本控制过程的不同阶段执行自定义操作。这些操作可以是自动化测试、代码格式化、验证提交消息格式、预防性错误检查等等。Git hooks是一种强大的自定义工具,可以提高代码质量和协作效率。

  1. Git Hook的种类 : Git提供了多种不同类型的Hook,每种类型对应着Git操作的不同阶段。以下是一些常见的Git挂钩类型:

    • pre-commit :在执行实际提交之前运行,用于执行预提交检查
    • pre-push :在执行实际推送之前运行,用于验证推送到远程仓库的内容
    • pre-receive :在接收端执行,通常用于验证推送到远程仓库的提交
    • post-receive :在接收端执行,通常用于通知或自动化部署
  2. Hook的位置 : 每个Git存储库都有一个.git/hooks目录,其中包含用于存储各种Hook脚本的文件。当我们在存储库中运行git init时,Git会为我们创建示例Hook文件,我们可以根据需要编辑或替换它们。这些示例文件以.sample为扩展名。

  3. 编写Git Hook : 要编写Git Hook,我们只需创建一个可执行的脚本文件并将其放入.git/hooks目录中。脚本的名称必须与hook类型相匹配(例如,pre-commit)。在脚本中,我们可以执行任何自定义操作,例如检查代码、验证提交消息、运行测试等。


git diff

git diff命令后通常需要跟两个参数,参数1是要比较的旧代码,参数2是要比较的新代码。如果只写一个参数,表示默认跟 workspace 中的代码作比较。

git diff 显示的结果为第二个参数所指的代码在第一个参数所指代码基础上的修改

  • git diff:查看 workspaceindex 的差别
  • git diff --cached:查看 indexlocal repositorty 的差别
  • git diff HEAD:查看 workspacelocal repository 的差别

HEAD 指向的是 local repository 中的代码最新提交版本

  • git diff HEAD^ 是比较 workspace 与最新commit的前一次commit的差异,与git diff HEAD的是不同的
  • git diff HEAD~2 是比较 workspace 与上2次commit的差异,相当于 git diff HEAD~2 HEAD~0,注意两个HEAD的位置,diff显示的结果表示 参数2(HEAD0) 相对于参数1(HEAD2)的修改

git 别名

在Git中,别名(Git Aliases)是一种机制,允许我们为常用的Git命令或命令序列创建简短的自定义命令。别名使我们可以用更短、更易记的名称来执行常用的Git操作,提高工作效率。

1. 创建别名: 我们可以使用git config命令来创建Git别名。

bash 复制代码
git config --global alias.<alias-name> <git-command-or-sequence>
  • <alias-name>:自定义别名的名称,我们可以选择任何喜欢的名称。
  • <git-command-or-sequence>:要与别名关联的Git命令或命令序列。

2. 例子: 以下是一些Git别名的例子:

bash 复制代码
git config --global alias.co checkout        # 创建 'co' 别名来代替 'checkout'
git config --global alias.br branch          # 创建 'br' 别名来代替 'branch'
git config --global alias.ci commit          # 创建 'ci' 别名来代替 'commit'
git config --global alias.st status          # 创建 'st' 别名来代替 'status'
git config --global alias.unstage 'reset HEAD --'  # 创建 'unstage' 别名来取消暂存

3. 使用别名: 创建别名后,我们可以在命令行中使用它们。例如,使用上面的例子,我们可以这样执行命令:

bash 复制代码
git co my-branch     # 等同于 'git checkout my-branch'
git br -a            # 等同于 'git branch -a'
git ci -m "Message"  # 等同于 'git commit -m "Message"'
git st               # 等同于 'git status'
git unstage file.txt # 等同于 'git reset HEAD -- file.txt'

从基本层面上说,Git只是一堆通过文件名相互关联的文本文件
还有一点需要提前声明,如果大家也在自己的电脑中进行实验,下面文章中出现的各种hash值,都是和内容有关系。所以,大家要和自己的内容对号入座,不要和本文中的hash值比较。

2. git init

为了演示方便,我们在本地的合适的文件夹中新建了一个dot_git的项目。

bash 复制代码
mkdir dot_git

与此同时通过git init来初始化项目。

现在让我们来看看.git文件夹中有什么内容。

我们使用erd来查看文件结构。

bash 复制代码
erd -y inverted .git

文档结构如下

看起来它创建了一堆文件和文件夹。让我们挑几个重要的来解释一下:

  • hooks包含了在Git执行任何操作之前/之后可以运行的脚本。

  • HEAD 指向的是 local repository 中的代码最新提交版本

    • 根据我们设置的"默认"分支是什么(git config --global init.defaultBranch <分支名称>),它将是refs/heads/master(默认),refs/heads/main,或者我们设置的其他分支名称。
    • 它指向了refs/heads文件夹 ,并指向一个叫做master的文件,这个文件在我们进行第一次提交之前是不存在的。
    • 这个master文件只会在我们进行第一次提交后出现
  • config是一个文本文件 ,它包含了当前仓库的Git配置

    • 如果我们查看它,我们会看到一些关于我们的仓库的基本设置,比如是否bare、文件模式等。
  • objects包含了Git对象,也就是关于仓库中文件、提交等的数据。(这个狠最重要,狠重要)

  • refs,存储引用(指针)的地方。

    • refs/heads包含分支的指针
    • refs/tags包含标签的指针

3. 新增一个文件

现在,我们已经了解了.git目录中初始文件的情况,让我们执行第一个将内容添加到.git目录的操作。我们将创建一个文件并将其添加到暂存区(但还没有提交)。

bash 复制代码
echo 'hello,789' > file
git add file

我们继续使用erd -y inverted .git来查看文件变化。

git add file这会引起两个主要的更改。

  • 首先,它新增了索引文件(index)。Index用于存储有关当前暂存区 的信息,用于表示名为file的文件已被添加到暂存区。
  • 第二个更为重要的更改是添加 了一个新文件夹objects/c3,其中包含一个名为dc8e6cf3e1117a8d9731ddde9916da644296aa的文件。这是Git中存储对象的部分。

窥探objects中信息

我们使用file来查看一下内容是何方神圣。

bash 复制代码
file .git/objects/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa
.git/objects/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa: zlib compressed data

结果显示这是经过zlib压缩的数据。这就很让人抓马。你有张良计,我有过墙梯 ,我们可以使用zlib库对其解压处理。

bash 复制代码
zlib-flate -uncompress < .git/objects/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa
blob 10hello,789

结果显示它包含了文件名为file的文件的类型大小数据

也就是说,/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa表示它是一个大小为10的blob,内容是hello,789的数据。(只不过是被zlib处理了)

上面提到的/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa这是Git对象的一部分,用于存储文件内容。

注意,此时我们用到了zlib库,我们可以通过brew install zlib下载该库。(我是Mac环境,其他环境大家自行寻找解决方案)


文件名的由来

文件名来自内容的SHA-1 hash值。

如果我们将zlib压缩的数据通过sha1sum命令处理,我们会得到文件名。

bash 复制代码
$ zlib-flate -uncompress <.git/objects/c3/dc8e6cf3e1117a8d9731ddde9916da644296aa | sha1sum
c3dc8e6cf3e1117a8d9731ddde9916da644296aa 

Git使用内容的SHA-1散列值,取前两个字符 (在这种情况下是c3),创建一个文件夹,然后将剩余部分用作文件名Git从前两个字符创建文件夹,以确保我们不会在单个objects文件夹下有太多文件。

Mac环境下,我们需要通过brew install md5sha1sum


git cat-file

由于objects的内容在Git中比较重要,Git还特意提供了一个名为git cat-file的命令,用于查看git对象的内容。 使用git cat-file命令

  • 带有-t选项查看类型(type)
  • 带有-s选项查看大小(size)
  • 带有-p选项查看内容(pretty-print)
    • 这个选项用于显示 Git 对象的内容,以更易读的方式呈现,通常用于查看提交、树或标签对象的内容
shell 复制代码
git cat-file -t c3dc8e6cf3e1117a8d9731ddde9916da644296aa
blob

git cat-file -s c3dc8e6cf3e1117a8d9731ddde9916da644296aa
10

git cat-file -p c3dc8e6cf3e1117a8d9731ddde9916da644296aa
hello,789

4. git commit

既然,已经将内容通过git add 添加到Index暂存区),接下来我们就需要将内容commitlocal repository:(本地仓库)

前面讲过,下面的ci等同于commit

shell 复制代码
git ci -m '首次提交'

继续使用erd -y inverted .git 来查看目录结构

嚯,一下多了很多文件。让我们来解读一下。

首先是一个新文件COMMIT_EDITMSG,它包含了(最新的)提交消息。

如果我们运行git ci命令而没有使用-m标志,那么Git获取提交消息的方式是打开一个文本编辑器,使用COMMIT_EDITMSG文件来让用户编辑提交消息。一旦用户更新了消息并退出编辑器,Git就会使用该文件的内容作为提交消息。

它还添加了一个全新的logs文件夹。这是Git用来记录仓库中所有提交更改的一种方式 。我们将能够在这里看到所有refsHEAD的提交更改

refs/heads目录,其中新增了一个名为master的文件。这是对主分支(master)的引用。

使用cat查看对于的内容信息。

shell 复制代码
cat .git/refs/heads/master
fe010b33df5078cdbd96f2397aad60ec5f42a967

看起来它指向了其中一个新对象。我们用内置命令cat-file查看内容。

ruby 复制代码
$ git cat-file -t fe010b33df5078cdbd96f2397aad60ec5f42a967
commit

$ git cat-file -p fe010b33df5078cdbd96f2397aad60ec5f42a967
tree 658524b859ae78d902597253a3b68b4da3b70466
author xxx <xxx@simple> 1697178492 +0800
committer xxx <xxx@xxx> 1697178492 +0800

首次提交

我们也可以使用以下命令查看该引用的类型:git cat-file -t refs/heads/master

看起来这是一种新的对象类型,似乎是一个提交对象commit object)。提交对象的内容告诉我们,它包含一个哈希为658524b859ae78d902597253a3b68b4da3b70466树对象tree object),这看起来就像我们在提交时添加的另一个对象。提交对象还包含了作者提交者的信息。最后,它还显示了这个提交的提交消息是什么。

我们继续来看看树对象包含了什么内容。

bash 复制代码
git cat-file -t 658524b859ae78d902597253a3b68b4da3b70466
tree

git cat-file -p 658524b859ae78d902597253a3b68b4da3b70466
100644 blob c3dc8e6cf3e1117a8d9731ddde9916da644296aa    file

我们会发现该文件指向了在我们执行git add file时添加的原始对象(c3dc8e6cf3e1117a8d9731ddde9916da644296aa)。

树对象内部使用更多的树对象来表示文件夹,这些树对象提交对象相连,用于表示目录结构。


5. 新增修改

让我们对文件进行更改并查看它是如何工作的。

bash 复制代码
echo 'git,hello,789' > file

git ci -am "修改文件内容"

还是利用erd查看文档目录

  • 创建了3个新的对象。
    • 一个包含文件新内容的blob对象
    • 一个是一个树对象
    • 最后一个是一个提交对象

让我们再次从HEADrefs/heads/master开始跟踪它们。

sql 复制代码
git cat-file -p refs/heads/master
tree 02185c57f2040abcaa0c67dfd7026464d916da2b
parent fe010b33df5078cdbd96f2397aad60ec5f42a967
author 789 <789@xx.net> 1697179597 +0800
committer 789 <789@xxx.net> 1697179597 +0800

修改文件内容

git cat-file -p 02185c57f2040abcaa0c67dfd7026464d916da2b
100644 blob 1f9224976e282aa9e32398a5bca0cec08041f1dc    file

git cat-file -p 1f9224976e282aa9e32398a5bca0cec08041f1dc
git,hello,789

提交对象现在有一个额外的属性,名为parent,它链接到前一个提交,因为这个提交是建立在前一个提交之上的。

这是Git中的提交历史的关键概念,

每个提交都有一个或多个父提交,形成一个提交链。


6. 创建分支

是时候创建一个分支了。让我们使用git br fix-text命令创建一个名为fix-text的分支。

前面讲过,下面的br等同于branch

这将在refs/heads文件夹下创建一个新文件,文件名为分支名称,文件内容为最新提交的ID。

我们首先用git log查看提交记录

发现最新的提交记录efa223e697c6452a393963887f9926ea0662c923

shell 复制代码
cat .git/refs/heads/fix-text
efa223e697c6452a393963887f9926ea0662c923

在Git中,分支是非常轻量级的。标签(Tags)的行为也类似,只不过它们是创建在refs/tags下的。

还会在logs目录下添加一个文件,用于存储与主分支类似的提交历史数据。这有助于跟踪各个分支的提交历史。Git的分支和标签是非常有用的版本控制工具,可以帮助我们管理项目的不同状态和版本。


7. 分支切换

Git中,检出(checkout)操作是获取提交树对象,并将working tree中的文件更新为与树对象记录的状态相匹配。

在这种情况下,因为我们从master切换到fix-text,而这两个分支都指向相同的提交和底层树对象 ,Git在working tree中没有任何事情要处理。

前面讲过,下面的co等同于checkout

bash 复制代码
git co fix-text 

.git目录内执行co操作时,唯一的变化是.git/HEAD文件现在将指向fix-text

bash 复制代码
cat .git/HEAD
ref: refs/heads/fix-text

另外,让我进行一次提交。

bash 复制代码
echo 'hello,git' > file

git ci -am "更换文本内容"

这将在fix-text分支上创建一个新的commit,将文件file中的内容更改为hello,git


8. 分支合并

合并(merging)有主要三种方式。

  1. 最简单和最容易的方式是快进合并fast forward merge

    • 在这种情况下,我们只需将一个分支指向另一个分支指向的commit object
    • 这实际上涉及将refs/heads/fix-text中的hash复制到refs/heads/master
  2. 第二种方式是变基合并rebase merge

    • 在这种情况下,我们首先逐个将我们的更改应用到主分支(main或master)当前指向的每个提交,然后执行类似于快进合并的操作。
  3. 最后一种方式是通过创建一个独立的合并提交来合并两个分支。

    • 这在于它将在其提交对象中有两个父节点(parent entries)。

首先,让我们看看在合并之前图形是什么样子。(先将分支切换回master(git co master))

scss 复制代码
git log --graph --oneline --all

* 4359ab4 (fix-text) 更换文本内容
* efa223e (HEAD -> master) 修改文件内容
* fe010b3 首次提交

执行合并操作

scss 复制代码
git merge fix-text
更新 efa223e..4359ab4
Fast-forward
 file | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

上面的操作,将一个master分支的引用(指向的哈希值)更新为fix-text分支的引用指向的哈希值。

scss 复制代码
git log --graph --oneline --all

* 4359ab4 (HEAD -> master, fix-text) 更换文本内容
* efa223e 修改文件内容
* fe010b3 首次提交

9. 远程提交

为了演示这一点,首先让我创建另一个Git仓库,它可以作为这个仓库的远程仓库

新建一个仓库

shell 复制代码
$ mkdir fake_git_remote
$ cd fake_git_remote && git init --bare

切换到我们dot_git项目中,为仓库设置remote

bash 复制代码
git remote add origin ../fake_git_remote

顺便说一下,添加一个新的远程仓库是一项配置更改,我们可以在.git/config文件中查看这个更改。我会让我们自己去查看这个更改是什么。

现在让我们进行推送操作。

bash 复制代码
git push origin master

让我们看看我们的本地仓库中发生了什么变化。

它添加了一个新的refs/remotes,用于存储有关不同远程仓库中的所有可用内容的信息。

但是发送到另一个Git仓库的是什么呢?实际上,

发送的内容就是.git/objects目录中的所有对象,以及我们显式推送的refs下的所有分支和标签。

这就是另一个Git仓库需要获取我们的完整Git历史记录所需的一切内容。


后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和"在看"吧。

相关推荐
余生H15 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍17 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai22 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默34 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_8572979144 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_1 小时前
说说你对es6中promise的理解?
前端·ecmascript·es6
Promise5201 小时前
总结汇总小工具
前端·javascript