Git 基础知识,完整指南
如果您已经每天使用Git,但想要很好地理解 Git基础知识,那么这篇文章适合您。
在这里,您将有机会真正了解Git 架构以及添加、检出、重置、提交、合并、变基、挑选、拉取、推送和标记等命令的内部工作原理。
不要让 Git 控制你,而是学习 Git 基础知识并掌握 Git。
做好准备,关于 Git 的完整指南即将开始。
💡 首先要做的事情
您必须在阅读这篇文章的同时进行练习。
接下来,让我们首先创建一个名为的新项目git-101
,然后使用以下命令初始化一个 git 存储库git init
:
$ mkdir git-101
$ cd git-101
Git CLI 提供两种类型的命令:
-
plumbing ,由用户输入高级命令时 Git 在后台内部使用的低级命令组成
-
ceramic,这是Git 用户常用的高级命令
在本指南中,我们将了解管道命令与我们日常使用的瓷器命令之间的关系。
⚙️ Git 架构
在包含 Git 存储库的项目中,我们可以检查 Git 组件:
$ ls -F1 .git/
HEAD
config
description
hooks/
info/
objects/
refs/
我们将重点关注以下几个主要方面:
-
.git/对象/
-
.git/refs
-
头
让我们详细分析一下每个组件。
💾 对象数据库
使用 UNIX 工具find
我们可以看到文件夹的结构.git/objects
:
$ find .git/objects
.git/objects
.git/objects/pack
.git/objects/info
在 Git 中,所有内容都保存在.git/objects
结构中,即Git 对象数据库。
我们可以在 Git 中持久化哪些内容?所有内容。
🤔等待!
这怎么可能呢?
通过使用哈希函数。
🔵 哈希救援
哈希函数 将任意动态大小的数据映射到固定大小的值。通过这种方式,我们可以存储/持久化任何内容,因为最终值的大小始终相同。
哈希函数的错误实现很容易导致冲突,其中两个不同的动态大小数据可能映射到相同的固定大小的最终哈希。
SHA-1是哈希函数的一个众所周知的实现,它通常是安全的并且几乎不会发生冲突。
让我们以字符串的散列为例my precious
:
$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed
注意:如果您使用的是 Linux,则可以使用命令 sha1sum
代替OpenSSL
。
🔵 比较内容的差异
良好的散列是一种安全的做法,我们无法知道原始值,即进行逆向工程。
如果我们想知道值是否已更改,我们只需将值包装到哈希函数中,然后我们就可以比较差异:
$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed
$ echo -e "no longer my precious" | openssl sha1
2e71c9ae2ef57194955feeaa99f8543ea4cd9f9f
如果哈希值不同,那么我们可以假设值已经改变。
你能发现这里的机会吗?用SHA-1来存储数据,然后通过比较哈希值来追踪所有内容,怎么样?
这正是 Git 内部所做的🤯。
🔵 Git 和 SHA-1
Git 使用 SHA-1 生成所有内容的哈希值并将其存储在.git/objects
文件夹中。就这么简单!
管道命令完成hash-object
以下工作:
$ echo "my precious" | git hash-object --stdin
8b73d29acc6ae79354c2b87ab791aecccf51701f
我们来比较一下OpenSSL
版本:
$ echo -e "my precious" | openssl sha1
fa628c8eeaa9527cfb5ac39f43c3760fe4bf8bed
哎呀……完全不一样。这是因为 Git在内容大小和分隔符前面添加了一个特定的单词。Git 把这个单词称为对象类型。\0
是的,Git 对象有类型。我们首先要研究的是blob 对象。
🔵 blob 对象
例如,当我们向命令发送字符串“my precious”时hash-object
,Git 会将该模式添加{object_type} {content_size}\0
到 SHA-1 函数的前面,以便:
blob 12\0myprecious
然后:
$ echo -e "blob 12\0my precious" | openssl sha1
8b73d29acc6ae79354c2b87ab791aecccf51701f
$ echo "my precious" | git hash-object --stdin
8b73d29acc6ae79354c2b87ab791aecccf51701f
耶! 🎉
🔵 将 Blob 存储在数据库中
但是命令hash-object
本身不会持久化到.git/objects
文件夹中。我们应该添加选项-w
,这样对象就会被持久化:
$ echo "my precious" | git hash-object --stdin -w
8b73d29acc6ae79354c2b87ab791aecccf51701f
$ find .git/objects
...
.git/objects/8b
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f
### Or, simply
$ find .git/objects -type f
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f
🔵 读取 blob 的原始内容
我们已经知道,由于加密原因,无法从其散列版本中读取原始内容。
🤔 好的,但是请稍等。
Git 如何知道原始值?
它使用哈希作为指向值的键,该值是原始内容本身,使用名为Zlib的压缩算法,压缩内容并将其存储在对象数据库中,从而节省存储空间。
管道命令完成以下工作:给定一个键,膨胀压缩数据,从而获得原始内容:cat-file
$ git cat-file -p 8b73d29acc6ae79354c2b87ab791aecccf51701f
my precious
如果你猜对了,Git 是一个键值数据库!
🔵 推广 blob
当使用 Git 时,我们希望处理内容并与其他人共享。
通常,在处理完各种文件/blob 之后,我们准备共享它们并在最终作品上签名。
换句话说,我们需要对 Blob 进行分组、提升和添加元数据。这个过程如下:
-
将 Blob 添加到暂存区
-
将舞台区域中的所有 blob分组为树形结构
-
向树结构添加元数据(作者姓名、日期、语义信息)
让我们详细看看上述步骤。
🔵 阶段区域,索引
plumbing命令允许向舞台update-index
区域添加一个 blob 并为其命名:
$ git update-index \
--add \
--cacheinfo 100644 \
8b73d29acc6ae79354c2b87ab791aecccf51701f \
index.txt
-
--add
:将 blob 添加到舞台,也称为索引 -
--cacheinfo
:用于注册尚未在工作目录中的文件 -
散列
-
index.txt
:索引中 blob 的名称
Git 将索引存储在哪里?
$ cat .git/index
DIRCsҚjT¸zQp index.txtÆ
7CJVVÙ
但它不是人类可读的,它是使用 Zlib 压缩的。
我们可以根据需要向索引添加任意数量的 blob,例如:
$ git update-index {sha-1} f1.txt
$ git update-index {sha-1} f2.txt
将 blob 添加到索引后,我们可以将它们分组为准备提升的树结构。
🔵 树对象
当使用plumbing命令时write-tree
,Git 将对添加到索引的所有 blob 进行分组,并在文件夹中创建另一个对象.git/objects
:
$ git write-tree
3725c9e313e5ae764b2451a8f3b1415bf67cf471
检查.git/objects
文件夹,注意已创建了一个新对象:
$ find .git/objects
### The new object
.git/objects/37
.git/objects/37/25c9e313e5ae764b2451a8f3b1415bf67cf471
### The blob previously created
.git/objects/8b
.git/objects/8b/73d29acc6ae79354c2b87ab791aecccf51701f
让我们使用以下方法来检索原始值cat-file
以便更好地理解:
### Using the option -t, we get the object type
$ git cat-file -t 3725c9e313e5ae764b2451a8f3b1415bf67cf471
tree
$ git cat-file -p 3725c9e313e5ae764b2451a8f3b1415bf67cf471
100644 blob 8b73d29acc6ae79354c2b87ab791aecccf51701f index.txt
这是一个有趣的输出,它与返回原始内容的blob 有很大不同。
在树对象中,Git 返回所有添加到索引的对象。
100644 blob 8b73d29acc6ae79354c2b87ab791aecccf51701f index.txt
-
100644
:缓存信息 -
blob
:对象类型 -
散列
-
blob 名称
一旦推广完成,就需要向树中添加一些元数据,以便我们可以声明作者的姓名、日期等。
🔵 提交对象
管道命令接收一棵树、commit-tree
一条提交消息并在.git/objects
文件夹中创建另一个对象:
$ git commit-tree 3725c -m 'my precious commit'
505555f4f07d90ae14a0f2e67cba7f7b9af539ee
它是什么样的物体?
$ find .git/objects
...
.git/objects/50
.git/objects/50/5555f4f07d90ae14a0f2e67cba7f7b9af539ee
### cat-file
$ git cat-file -t 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
commit
它的价值又如何呢?
$ git cat-file -p 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
tree 3725c9e313e5ae764b2451a8f3b1415bf67cf471
author leandronsp <leandronsp@example.com> 1678768514 -0300
committer leandronsp <leandronsp@example.com> 1678768514 -0300
my precious commit
-
tree 3725c
:引用树对象 -
作者/提交者
-
提交消息我的珍贵提交
🤯 我的天哪!我是不是看出什么规律了?
此外,提交可以引用其他提交:
$ git commit-tree 3725c -p 50555 -m 'second commit'
5ea578a41333bae71527db537072534a199a0b67
该选项-p
允许引用父提交:
$ git cat-file -p 5ea578a41333bae71527db537072534a199a0b67
tree 3725c9e313e5ae764b2451a8f3b1415bf67cf471
parent 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
author leandronsp <leandronsp@gmail.com> 1678768968 -0300
committer leandronsp <leandronsp@gmail.com> 1678768968 -0300
second commit
我们可以看到,给定一个具有父级的提交,我们可以递归地遍历所有提交,遍历它们的所有树,直到到达最终的 blob。
一个潜在的解决方案:
$ git cat-file -p <first-commit-sha1>
$ git cat-file -p <first-commit-tree-sha1>
$ git cat-file -p <first-commit-parent-sha1>
$ git cat-file -p <parent-commit-sha1>
...
等等。好了,你说到了重点。
🔵 救援日志
瓷器 命令git log
解决了这个问题,通过遍历所有提交、它们的父母和树,让我们了解我们工作的时间表。
$ git log 5ea57
commit 5ea578a41333bae71527db537072534a199a0b67
Author: leandronsp <leandronsp@gmail.com>
Date: Mon Mar 13 22:42:48 2023 -0300
second commit
commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date: Mon Mar 13 22:35:14 2023 -0300
my precious commit
🤯 天啊!
Git 是一个巨大但轻量级的键值图数据库!
🔵 Git 图表
在 Git 中,我们可以操作图形中的指针等对象。
-
Blob是数据/文件快照
-
树是一串斑点或另一棵树
-
提交参考树和/或其他提交,添加元数据
这真是太棒了。但是在命令sha1
中使用git log
可能会很麻烦。
那么给哈希表命名怎么样?输入“引用”。
Git 参考
参考资料位于.git/refs
文件夹中:
$ find .git/refs
.git/refs/
.git/refs/heads
.git/refs/tags
🔵 为提交命名
我们可以将任何提交哈希与位于中的任意名称关联起来.git/refs/heads
,例如:
echo 5ea578a41333bae71527db537072534a199a0b67 > .git/refs/heads/test
现在,让我们git log
使用新的引用来发出问题:
$ git log test
commit 5ea578a41333bae71527db537072534a199a0b67
Author: leandronsp <leandronsp@gmail.com>
Date: Mon Mar 13 22:42:48 2023 -0300
second commit
commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date: Mon Mar 13 22:35:14 2023 -0300
my precious commit
更好的是,Git 提供了管道命令update-ref
,因此我们可以使用它来更新提交与引用的关联:
$ git update-ref refs/heads/test 5ea578a41333bae71527db537072534a199a0b67
听起来很熟悉,嗯?是的,我们正在谈论分支。
🔵 分支
分支是指向特定提交的引用。
由于分支代表update-ref
命令,因此提交哈希可以随时更改,即分支引用是可变的。
让我们暂时思考一下git log
不带参数的函数是如何工作的:
$ git log
fatal: your current branch 'main' does not have any commits yet
🤔 嗯……
Git 如何知道我当前的分支是“主”分支?
🔵 头部
HEAD 引用位于.git/HEAD
。它是一个指向头引用(分支)的单个文件:
$ cat .git/HEAD
ref: refs/heads/main
类似地,使用瓷器命令:
$ git branch
* main
使用管道命令symbolic-ref
,我们可以操纵HEAD 指向哪个分支:
$ git symbolic-ref HEAD refs/heads/test
### Check the current branch
$ git branch
* test
就像在分支上一样,我们可以随时update-ref
使用 HEAD 进行更新。symbolic-ref
在下图中,我们将 HEAD 从主分支更改为修复分支:
如果没有参数,该git log
命令将遍历当前分支(HEAD)引用的根提交:
$ git log
commit 5ea578a41333bae71527db537072534a199a0b67 (HEAD -> test)
Author: leandronsp <leandronsp@gmail.com>
Date: Tue Mar 14 01:42:48 2023 -0300
second commit
commit 505555f4f07d90ae14a0f2e67cba7f7b9af539ee
Author: leandronsp <leandronsp@gmail.com>
Date: Tue Mar 14 01:35:14 2023 -0300
my precious commit
到目前为止,我们学习了 Git 的架构和主要组件,以及管道命令,它们是更底层的命令。
是时候将所有这些知识与我们日常使用的瓷器命令联系起来了。
🍽️ 瓷器命令
Git 带来了更多高级命令,我们可以使用这些命令,而无需直接操作对象和引用。
这些命令被称为瓷器命令。
🔵 git 添加
该git add
命令将工作目录中的文件作为参数,将它们作为 blob 保存到数据库中并将它们添加到索引中。
简而言之,git add
:
-
hash-object
对每个文件参数运行 -
update-index
对每个文件参数运行
🔵 git 提交
git commit
以消息作为参数,将之前添加到索引的所有文件分组并创建一个提交对象。
首先,它运行write-tree
:
然后,它运行commit-tree
:
$ git commit -m 'another commit'
[test b77b454] another commit
1 file changed, 1 deletion(-)
delete mode 100644 index.txt
🕸️ 在 Git 中操作指针
以下瓷器命令被广泛使用,它们在后台操纵Git 引用。
假设我们刚刚克隆了一个项目,其中HEAD指向主分支,而主分支又指向提交C1:
我们如何从当前 HEAD创建另一个新分支并将HEAD 移动到这个新分支?
🔵 git checkout
通过使用git checkout
该-b
选项,Git 将从当前分支(HEAD)创建一个新分支,并将 HEAD 移动到这个新分支。
### HEAD
$ git branch
* main
### Creates a new branch "fix" using the same reference SHA-1
#### of the current HEAD
$ git checkout -b fix
Switched to a new branch 'fix'
### HEAD
$ git branch
* fix
main
哪个管道命令负责移动 HEAD?没错,就是symbolic-ref。
之后,我们在修复分支上做一些新的工作,然后执行git commit
,这将添加一个名为C3的新提交:
通过运行git checkout
,我们可以在不同的分支之间切换 HEAD:
有时,我们可能想要移动分支指向的提交。
我们已经知道管道命令update-ref
可以做到这一点:
$ git update-ref refs/heads/fix 356c2
用瓷器语言,让我向你介绍git reset。
🔵 git 重置
git reset
ceramic命令内部运行update-ref,因此我们只需执行:
$ git reset 356c2
但是 Git 如何知道要移动哪个分支呢?git reset 会移动 HEAD 指向的分支。
如果修订版本之间存在差异怎么办?通过使用reset
,Git 会移动指针,但会将所有差异保留在暂存区(索引)中。
$ git reset b77b
检查git status
:
$ git status
On branch fix
Untracked files:
(use "git add <file>..." to include in what will be committed)
another.html
bye.html
hello.html
nothing added to commit but untracked files present (use "git add" to track)
修订提交在 修复分支 中发生更改,所有差异都 移至索引。
那么,如果我们想重置并丢弃所有差异,该怎么办呢?只需传递选项--hard
:
通过使用git reset --hard
,修订之间的任何差异都将被丢弃,并且不会出现在索引中。
💡 关于移动分支的黄金建议
如果我们想在另一个分支上执行管道 update-ref
,则无需像git reset中那样检出分支。
我们可以进行瓷 git branch -f source target
改:
$ git branch -f main b77b
它在源分支中执行了操作。让我们检查一下主分支指向git reset --hard
哪个提交:
$ git log main --pretty=oneline -n1
b77b454a9a507f839880879a895ac4f241177a28 (main) another commit
另外,我们确认修复分支仍然指向369cd
提交:
$ git log fix --pretty=oneline -n1
369cd96b1f1ef6fa7de1ff2ed12e15be979dcffa (HEAD -> fix, test) add files
我们执行了“git reset”,但没有移动 HEAD!
并不罕见,我们不想移动分支指针,而是想将特定的提交应用到当前分支。
满足cherry-pick。
🔵 git cherry-pick
使用ceramic git cherry-pick
,我们可以对当前分支应用任意提交。
请考虑以下场景:
-
要点至 C3 - C2 - C1
-
固定点至 C5 - C4 - C2 - C1
-
HEAD指向修复
在修复分支中,我们缺少主分支所引用的C3 提交。
我们可以通过运行来应用它git cherry-pick C3
:
注意:
-
C3提交将被克隆到名为C3'的新提交中
-
这个新的提交将引用 C5 提交
-
修复将移动指针至 C3'
-
HEAD 一直指向修复
应用更改后,图表将显示如下:
不过,还有另一种移动分支指针的方法。它包括应用另一个分支的任意提交,并在需要时合并差异。
你没错,我们这里讨论的是git merge 。
🔵 git 合并
让我们描述以下场景:
-
要点至 C3 - C2 - C1
-
固定点至 C4 - C3 - C2 - C1
-
HEAD 指向主
我们希望将修复分支应用到当前(主)分支,也就是执行git merge fix。
请注意,修复分支包含属于主分支(C3 - C2 - C1)的所有提交,主分支之前只有一个提交(C4)。
在这种情况下,主分支将被“转发”,指向与修复分支相同的提交。
这种合并称为快进,如下图所示:
当无法快进时
有时,我们的树状结构 current 状态不允许快进。例如以下场景:
这就是当合并分支(上面例子中的修复分支)缺少来自当前分支(主分支)的一个或多个提交时的情况: C3 提交。
因此,快进是不可能的。
但是,为了使合并成功,Git 执行了一种称为Snapshotting的技术,该技术由以下步骤组成。
首先,Git 查找两个分支的下一个公共父级,在此示例中为C2提交。
其次,Git对目标C3 提交分支进行快照:
第三,Git对源C5 提交分支进行快照:
最后,Git 自动创建一个提交合并(C6),并将其分别指向两个父级:C3(目标)和 C5(源):
您是否想过为什么您的 Git 树会显示一些自动创建的提交?
毫无疑问,这个合并过程被称为三向合并!
接下来,让我们探索另一种无法快进的合并技术,但 Git 不会进行快照和自动提交合并,而是在源分支之上应用差异。
是的,这就是git rebase。
🔵 git rebase
请考虑以下图像:
-
要点至 C3 - C2 - C1
-
固定点至 C5 - C4 - C2 - C1
-
HEAD 指向修复
我们希望通过执行以下命令将主分支rebasegit rebase main
到 fix 分支。但是git rebase是如何工作的呢?
👉 git reset
首先,Git 执行git reset main,其中 fix 分支将指向相同的主分支指针:C3 - C2 - C1。
目前,C5 - C4 提交没有引用。
👉 git cherry-pick
其次,Git 执行git cherry-pick C5到当前分支:
请注意,在挑选过程中,挑选的提交会被克隆,因此最终的哈希值会发生变化:C5 - C4 变成 C5' - C4'。
经过 cherry-pick 之后,我们可能会出现以下情况:
👉再次重置 git
最后,Git 将执行git reset C5',因此修复分支指针将从C3 移动到 C5'。
重新定基过程已完成。
到目前为止,我们一直在使用本地分支,也就是我们自己的机器上。现在是时候学习如何使用远程分支了,这些分支与互联网上的远程仓库同步。
🌐 远程分支
要使用远程分支,我们必须使用ceramic命令将远程分支添加到我们的本地存储库。git remote
$ git remote add origin git@github.com/myaccount/myrepo.git
遥控器位于.git/refs/remotes
以下文件夹中:
$ find .git/refs
...
.git/refs/remotes/origin
.git/refs/remotes/origin/main
🔵 从远程下载
我们如何将远程分支与本地分支同步?
Git 提供了两个步骤:
👉 git fetch
通过使用瓷器,Git 将下载远程分支并将其与名为 origin/maingit fetch origin main
的新本地分支(也称为上游分支)同步。
👉 git 合并
在获取并同步上游分支后,我们可以执行,git merge origin/main
并且由于上游位于我们本地分支之前,Git 将安全地应用快进合并。
然而,fetch + merge 可能会重复,因为我们每天会多次同步本地/远程分支。
但今天是我们的幸运日,Git 提供了git pull瓷器命令,可以代表我们执行 fetch + merge。
👉 git pull
使用git pull
,Git 将执行 fetch(将远程与上游分支同步),然后将上游分支合并到本地分支。
好的,我们已经了解了如何从远程拉取/下载更改。那么,如何将本地更改发送到远程呢?
🔵 上传到远程
Git 提供了一个名为的瓷器命令git push
:
👉 git push
执行git push origin main
将首先将更改上传到远程:
然后,Git 会将上游origin/main
与本地main
分支合并:
在推送过程结束时,我们得到以下图像:
在哪里:
-
远程已更新(本地更改已推送至远程)
-
C4 要点
-
起点/要点至 C4
-
HEAD 指向主
🔵 为提交赋予不可变的名称
到目前为止,我们了解到分支只是对提交的可变引用,这就是为什么我们可以随时移动分支指针。
然而,Git 还提供了一种提供不可变引用的方法,这些引用的指针不能改变(除非删除它们并再次创建它们)。
例如,当我们想要标记/标记已准备好发布某些生产版本的提交时,不可变引用很有用。
是的,我们正在谈论标签。
👉 git 标签
使用 ceramicgit tag
命令,我们可以为提交命名,但不能执行重置或任何其他会改变指针的命令。
它对于发布版本控制非常有用。标签位于以下.git/refs/tags
文件夹中:
$ find .git/refs
...
.git/refs/tags
.git/refs/tags/v1.0
如果我们想改变标签指针,我们必须删除它并创建另一个同名的标签指针。
💡 Git 引用日志
最后但同样重要的是,有一个名为的命令git reflog
可以保存我们在本地存储库中所做的所有更改。
$ git reflog
369cd96 (HEAD -> fix, test) HEAD@{0}: reset: moving to main
b77b454 (main) HEAD@{1}: reset: moving to b77b
369cd96 (HEAD -> fix, test) HEAD@{2}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{3}: checkout: moving from fix to main
369cd96 (HEAD -> fix, test) HEAD@{4}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{5}: checkout: moving from fix to main
369cd96 (HEAD -> fix, test) HEAD@{6}: checkout: moving from main to fix
369cd96 (HEAD -> fix, test) HEAD@{7}: checkout: moving from test to main
369cd96 (HEAD -> fix, test) HEAD@{8}: checkout: moving from main to test
369cd96 (HEAD -> fix, test) HEAD@{9}: checkout: moving from test to main
369cd96 (HEAD -> fix, test) HEAD@{10}: commit: add files
b77b454 (main) HEAD@{11}: commit: another commit
5ea578a HEAD@{12}:
如果我们想在 Git 时间线上来回切换,它就非常有用。和reset、cherry-pick 等类似的工具一样,它也是掌握 Git 的强大工具。
总结
多么漫长的旅程啊!
这篇文章有点太长了,但我可以表达我认为理解 Git 的重要主题。
我希望您在阅读本文后能够更加自信地使用 Git,解决合并/变基过程中的日常冲突和痛苦情况。
在Twitter上关注我并查看我的网站博客leandronsp.com,我在那里也撰写了一些技术文章。
干杯!
文章来源:https://dev.to/leandronsp/git-fundamentals-a-complete-guide-do7