通过图像理解 Git
大家好,开发者社区!
什么是 Git?
分支
合并
变基
保持本地存储库为最新
实用函数
结尾
大家好,开发者社区!
我是个新手,在日本从事开发工作才几个月。我受到了Nico Riedmann 的《学习 Git 概念而非命令》一书的启发,并以自己的方式对 Git 进行了总结。当然,我也参考了官方文档
。 从系统结构入手理解 Git 会让 Git 变得更有趣。最近我对 Git 非常着迷,正在创建自己的 Git 系统。
最近,我写了一篇关于如何制作类似 git 的软件的文章!
制作原始的 git
什么是 Git?
管理版本和分发工作
Git 是一种称为分布式版本控制系统的源代码管理系统。Git
是一个通过记录和跟踪文件的变更日志(版本)、比较过去和当前文件以及明确变更来简化开发工作的工具。
该系统还允许多个开发人员同时编辑文件,从而实现工作分布式。
使用 Git 意味着
首先,在您的计算机上(以下称为“本地仓库”)一个可共享的存储位置(以下称为“远程仓库”)复制该文件或其他文件,然后添加或编辑新的代码或文件。之后
,通过将文件从本地仓库注册到远程仓库,即可更新这些文件。
通过图像理解
在使用 Git 时,重要的是遵循从“什么”到“什么”的“如何工作”的原则。
如果只是操作命令,可能会不理解正在发生的事情并使用错误的命令。
(信息)
当操作 Git 时,尝试想象操作前后发生的事情。
开始新的工作
存储库
Git 中的存储库是文件的存储,可以是远程的,也可以是本地的。
远程仓库是指将源代码放置在互联网服务器上,供所有人共享的仓库。
本地仓库是指将源代码放置在您的计算机上,只有您可以进行更改的仓库。
复制存储库并开始工作
首先,准备好你自己的开发环境。
你只需要决定在哪个目录中工作即可。
例如,你的主目录就可以,或者任何你常用的目录都可以。
接下来,从远程存储库复制并导入文件。
这称为clone
。
所调用的远程存储库project
仅包含,这是您远程存储库first.txt
时的图像。clone
(信息)
当然,您可以先创建本地仓库,然后镜像远程仓库。
这被称为“本地仓库initialize
”,允许您将正在处理的目录转换为仓库。
(补充)工作目录
工作目录并非任何特殊目录,而是您在计算机上始终工作的目录。
如果您将其视为一个目录,并且可以project
通过 Git 暂存区或本地存储库连接到 Git 管理的目标目录(在本例中为 ),则更容易理解。
更改和添加文件
源代码的修改是通过工作目录(暂存区)进行的。
实际上,我们在工作目录中工作。
让我们创建一个名为 的新文件second.txt
。
接下来,将修改后的文件移至暂存区。
这称为add
。
Git 的一个特性是,在更改反映到本地仓库之前会有一个缓冲。
稍后我会详细解释为什么存在这个缓冲。
然后,我们将暂存区的内容注册到本地仓库。
这称为commit
。
顺便说一句,我们可以在您 时发表评论commit
。
在本例中,我们添加了一个文件,因此请写入git commit -m 'add second.txt'
。
(信息)
提交操作会在仓库中创建一个提交对象
。 提交对象简单来说就是包含更新者信息和修改文件的数据。
(所有数据都会被保存,不仅仅是差异,还包括文件当时的完整状态(快照)。有关 Git 对象的更多信息,
请参阅Git对象。
适应远程存储库
那么,工作就完成了!
最后一步是将本地存储库中的更改反映到远程存储库。
这称为push
。
如果您将其视为对远程存储库的提交,可能会更容易理解。
查看差异
同一个文件之间的变化称为diff
。
我们可以在文件中看到变化点。
我不会详细介绍这些命令,但这里有三个我经常使用的命令。git diff --stage
查看之前原始工作目录的更改add
。git diff --stage
查看之后工作目录的更改add
。git diff <commit> <commit>
比较提交。
(旁白)有一个步骤叫做暂存区
随着开发工作的增长,我们经常在一个工作目录中进行大量更改。
如果将所有更改一次性放入本地仓库,会发生什么情况?
在这种情况下,在解析提交时,您可能不知道某个功能是在何处实现的。
在 Git 中,建议commit
每个功能创建一个。
因此,有一个暂存区,您可以在其中将commit
单元细分为更小的单元。
Git 的概念是仅暂存所需的内容,然后继续工作或commit
提前进行,以促进高效的开发,并可以追溯每个实现的历史记录。
概括
基本工作流程是一次,clone
然后、 和每次工作。add
commit
push
(信息)
clone
:从远程存储库复制到您的开发环境(本地存储库和工作目录)。add
:将文件从工作目录添加到暂存区并准备提交。commit
:将文件从暂存区注册到本地存储库。此时,将创建一个提交对象。push
:将本地存储库中的更改注册到远程存储库。
分支
我们创建一个分支,branch
用于在多个分支中更改和添加文件。
保存在分支中的文件main
正在被持续使用。
使用独立分支的原因是为了在不影响当前正在运行的源代码的情况下工作。
创建新分支
让我们创建一个名为 的分支develop
!我们可以使用或 来
创建分支。 前者直接创建一个分支,后者创建一个分支并将你移动到该分支。 (分支在代码库中维护。)git branch <new branch>
git checkout -b <new branch>
生成分支时的关键在于从哪个分支派生。
我们可以将源指定为git checkout -b <new branch> <from branch>
。
如果不指定,则当前正在处理的分支将成为<from branch>
。
(信息)
分支实际上是指向提交的指针(严格来说,是提交对象的哈希值)。
生成新的分支意味着新分支也向提交指明了 from 分支所指向的提交。
在分支机构工作
移动分支称为checking out
。
指向当前正在处理的分支的指针称为HEAD
。
因此,从main
分支移动到develop
分支意味着更改HEAD
。
现在两个分支都指向名为 的提交Atr3ul
。
您刚刚second.txt
通过在分支中提交添加了内容main
,因此您领先于提交f27baz
。
从这里开始,假设您在分支second.txt
中进行了更改develop
并进行了新的提交。
然后,如图所示,develop
分支创建了一个名为的提交m9sgle
并指向该提交。
当前的 HEAD 位置(工作分支位置)、文件已在哪个阶段进行处理、或者谁在处理该文件的状态称为status
。
(信息)
如果你熟悉面向对象,你可能会理解提交上箭头的含义。
它表示“父”提交和“子”提交之间的关系。
假设是parent←-child
,即从父提交(提交)衍生的子提交(提交)增长(改变了)了多少。
(题外话)Git-Flow 和 GitHub-Flow
不同开发团队的分支管理方式各不相同。
另一方面,就像编程命名约定一样,Git 中也有一个通用的分支增长模型。
以下是两个简单的模型。我认为了解这一点就足够了。
“Git Flow” 是一个相当复杂和精妙的结构。
我认为它是 Git 应该如何使用的一个模型。
各分支的定义。
master
:发布产品的分支。此分支上没有工作。
development
:用于开发产品的分支。准备发布时,请合并到release
。此分支上没有工作。
feature
:用于添加功能的分支,准备发布时合并到开发中。
hotfix
:对于紧急的发布后工作(关键错误修复等),从 master 分支出来,合并到 master,然后合并到 development。
release
:用于产品发布的准备。从develop
包含待发布功能和错误修复的分支开始。
准备发布后,合并到 master 分支,再合并到 development 分支。
“GitHub Flow”是 Git Flow 的一个稍微简化的模型。
如您所见,它仅由master
和组成feature
。
重要的区别在于 的缓冲pull requests
(在下面的拉动中解释),它允许分支之间的集成。
概括
基本上,由于主干(master)上没有工作,我们为想要做的每个工作单元创建一个分支并创建一个新的提交。
(信息)
branch
:指向提交的新指针checkout
:移动HEAD
以改变branch
要进行的工作。
合并
整合分支称为merge
。
基本上,我们会合并到main
或develop
分支。
请注意不要搞错哪个分支合并(吸收)了哪个分支。
我们始终会将 HEAD 移动到您要从中派生的分支,然后从您要从中派生的分支进行整合。
我目前正在feature
分支上工作并创建了以下内容third.txt
。
第三.txt
Hello, World! I'm noshishi, from Japan.
I like dancing on house music.
然后我们add
又完成了commit
。
快进
当feature
分支指向可以追溯到该分支的提交时develop
,该develop
分支处于某种fast-forward
状态。
develop
首先,使用移至checkout
。
在这种情况下,develop
分支根本没有进展,因此merge
分支feature
只会将提交向前移动。
在这种情况下,develop
和feature
分支共享相同的提交。
禁止快进
如果develop
分支通过提交或合并进展到新的提交会怎么样?
这被称为no fast-forward
情况。
在develop
分支中,您已对 进行了更改first.txt
并已完成commit
。
因此develop
分支与feature
分支已完全分裂。
如果你尝试从merge
一个分支切换到另一个分支,Git 会检查你的变更日志。 如果没有冲突的编辑,就会立即创建一个。 这被称为。feature
develop
merge commit
automatic merge
处理冲突
在no fast-forward
状态下,工作内容的差异被称为conflict
。
在这种情况下,我们必须手动修复conflict
内容 和commit
。
在develop
分支中,我们创建了以下third.txt
和committed
。
第三.txt
Hello, World! I'm nope, from USA.
I like dancing on house music.
在develop
分支中,I'm nope, from USA
。
在feature
分支中,I'm noshishi, from Japan
。
第一行的内容有冲突。
merge
如果此时执行 a ,conflict
则会发生 a 。Git会在解决 后
要求您执行。commit
conflict
(我们工作的分支是develop
分支)
如果你按照说明查看third.txt
,你会看到以下附加内容
third.txt(冲突后)
<<<<<<<< HEAD
Hello, World! I'm noshishi, from Japan.
=======
Hello, World! I'm nope, from USA.
>>>>>>>> feature
I like dancing on house music.
上方HEAD
以 分隔的=======
表示分支的内容develop
。
下方表示feature
分支。
feature
你首先考虑了要采用哪一个,并决定采用这次分支中的更改。之后唯一的操作就是手动
编辑(删除不需要的部分)。third.txt
third.txt(编辑后)
Hello, World! I'm noshishi, from Japan.
I like dancing on house music.
接下来你要做的就是add
和commit
。
被解决,并创建conflict
一个新的。merge commit
冲突是初学者所害怕的,但是一旦你学会了这一点,你就不再害怕了。
(信息)
如果你merge
解决了conflict
,为什么不merge
再次解决呢?
当你merge
解决了 之后,develop
分支会进入merge
状态,如果没有conflicts
,新文件会自动added
和commit
。
所以它并不是一个特殊的解决commit
之后。 这就是为什么它被称为。conflict
merge commit
删除不需要的分支
合并后的分支基本上没什么用,所以我们会将其删除。
如果我们保留某个分支,您可以从要删除的分支移动到另一个分支,然后git branch -d <branch>
。
您可能认为该分支上的提交已被删除。
实际上,这些提交会被转移到合并后的分支。
您可以使用git log
来查看您在该分支上所做的所有提交以及合并分支上的提交。
(旁白)什么是分支
我们说过,分支是指向提交的指针,但它还包含另一个重要数据:
该分支上的所有提交。
分支是提交的集合,它有一个指向该集合中最新提交的指针。(严格来说,提交可以追溯到之前的提交。)
下图说明了这一点。
所以我们可以像 Git Flow 那样,将分支想象成水平轴上的分支。
顺便说一下,如果你把上面的图以水平轴为分支来绘制,它看起来就像这样。
概括
fast-forward merge
no fast-forward merge
no fast-forward merge with conflict
(信息)
merge
:将工作分支(例如feature
)集成(吸收)到特定分支(例如main
或develop
)并创建新的提交。
变基
Rebase
是通过更改分支的提交来合并分支的过程。
它与 类似merge
,不同之处在于您正在处理的分支是目标分支。
develop
假设您正在和分支上工作feature
。
移动分支
您可能想将develop
分支上的当前提交反映到feature
分支中。
您需要将feature
分支从gp55sw
提交移动到3x7oit
提交。
feature
可以通过执行以下操作立即将其从分支中移出git rebase develop
。
这个过程更像是feature
从分支上的最新提交重新生成分支,develop
而不是执行merge
。
区别在于,你移动整个提交并进行新的提交。
这样做的一个原因是,它随时都fast-forward
易于操作。 另一个原因是,提交是对齐的,这样可以轻松追溯提交历史,并且文件更新的顺序保持一致。merge
处理 rebase 冲突
当然,在conflict
中也有一个rebase
。
您在分支fourth.txt
中添加了内容feature
,但没有在分支fourth.txt
中更改内容develop
。
有conflict
。
然而,如果以下变化被彼此覆盖,conflict
就会发生。
你可以像处理 一样处理它merge
。
但是,在检查了差异并完成文件编辑后,你应该使用 来完成你的工作git rebase --continue
。
你不必commit
,它会自动提交。
(信息)
rebase
:将派生分支的提交移动到新的提交。
保持本地存储库为最新
在完成一些本地工作后,您可能会遇到远程存储库已被其他开发人员更新的情况。
在这种情况下,您可以使用pull
将远程存储库中的信息重新安装到本地存储库。
分支和存储库
每个仓库中都存储着分支。
这是实际工作完成的分支。
另一方面,本地仓库拥有远程仓库的复制分支。
这被称为“远程跟踪分支”。
它是一个名称与远程分支绑定的分支remotes/<remote branch>
。
这只是监控远程存储库。
查看最新状态
假设您遇到这样一种情况:develop
远程存储库中的分支比远程跟踪分支领先一步。
将远程存储库中分支的最新状态反映到远程跟踪分支上称为fetch
。
更新至最新状态
如果您希望它反映在本地分支中,您可以执行pull
。
当您 时pull
,本地远程跟踪分支将首先更新。
然后更新merge
到本地分支。
这一次,有一个提交比分支领先一个分支develop
,因此您merge
在本地develop
分支中创建了一个新的提交。
处理拉取冲突
当远程仓库的提交与本地仓库的提交发生冲突时,您将面临conflict
远程跟踪分支和本地分支之间的冲突pull
。
在以下情况下,remotes/develop
和develop
分支发生冲突。
由于push
是fetch
且,您可以按照中merge
相同的方法进行求解。 这次,,因此工作分支是。 打开导致问题的文件夹,并在修复问题后进行操作。conflict
merge
develop
merges
remotes/develop
develop
commit
(题外话)拉取请求的身份
远程和本地的关系基本上是从远程存储库拉取到本地存储库,再从本地存储库推送到远程存储库。
然而,GitHub 和其他服务有一种机制,在从远程存储库的分支合并到主分支(例如主分支)之前会发送请求。
这是因为如果开发人员推送到主分支并更新远程存储库,则没有人可以检查它,并且可能会发生重大故障。Pull request
就是插入一个由更高级别的开发人员审查一次代码的流程。
(信息)
pull
: fetch
+ merge
.pull
是为了在本地存储库中反映远程存储库的状态。
实用函数
更正提交
更正commit
先前的提交称为revert
。
例如,假设您second.txt
使用 向本地存储库添加了m9sgLe
。
当您 时revert
,提交将被撤销并且second.txt
不再位于本地存储库中。
的优点revert
是它允许你离开commit
。
这与 的区别reset
,稍后会介绍。
删除提交
撤消当前最新提交并再次对其进行处理称为reset
。
该--soft
选项允许您立即返回到 之后的阶段add
。
该--mixed
选项允许您返回到工作目录中之前工作的阶段。
该--hard <commit>
选项将删除到您要返回的提交点之前的所有提交,并移动head
到指定的提交。
由于会reset
完全删除提交,因此建议您不要使用它,除非您有充分的理由,尤其是对于“--hard”选项。
如果您想恢复您的提交,您可以使用git reflog
查看已删除的提交。
撤离工作
由于如果存在更改文件,您就无法移动到其他分支,因此您必须选择是继续更改commit
还是放弃更改。
这时就stash
派上用场了。
您可以暂时撤离工作目录或暂存区中的文件。
当您想要移动到另一个分支时,stash
以及当您返回时,使用它stash pop
来检索撤离的文件并恢复工作。
带来提交
将任何提交带到当前分支以创建提交称为cherry-pick
。
这是一个非常好的功能。
例如,当您只想恢复分支中以前实现的功能并将其用于当前分支中的工作时,可以使用此功能feature
。develop
掌握 HEAD
我解释过,HEAD 是指向你当前正在处理的分支的指针。
我还解释过,分支是指向提交的指针。
参见下图。
HEAD 指向develop
分支 ,develop
分支 指向 提交eaPk76
。
所以,在这种情况下, HEAD 指的是 提交eaPk76
。
HEAD
你是否经常看到 Git 文档或文章在命令后使用?
例如git revert HEAD
。
这是一个可以实现的命令,因为你可以HEAD
用 commit 替换它。
结尾
无需 Git 的源代码管理
Mercurial 的历史与 Git 相同。Mercurial
的命令行界面 (CLI) 非常简单,牺牲了 Git 的灵活性。
最近,Meta 基于 Mercurial 开源了一个名为 Sapling 的新源代码管理系统。
我想再次尝试一下,并写下我的使用感受。
远程存储库在哪里
托管服务是一种为远程存储库租用服务器的服务。
典型的例子是 GitHub、Bitbucket 和 AWS Code Commit 等供私人使用的服务。Git
和 Git Hub 完全不同。
顺便说一下,如上所述,我们可以将自己的服务器用于远程存储库。
指针
如果你接触过直接处理内存的编程,比如 C 语言,你大概会知道“指针”是什么。
然而,对于一个编程新手来说,它似乎很模糊。
我说了提交对象是存储在仓库中的,
如果仓库中有很多提交对象,该如何选择自己想要的那个呢?
我们需要一个标签(地址)来定位特定的提交对象。
“指针”是指向标签的宝贵数据,以便我们不会忘记它。
顺便说一下,标签通过 转换为一个神秘的字符串hash function
。
如果您好奇,请参阅Git 如何计算文件哈希值?。
进一步了解 Git
这篇文章中有很多事情我没有提到。
- Git 的核心是一个简单的键值类型数据存储
- Git 对象的详细信息,即值
- 如何与每个对象关联。
我希望有一天能够彻底探索这一点。