你学到了一些错误的东西。注意:我没有看过你链接的特定 youtube 教程,所以我不确定它是好、坏还是无关紧要。我将本段仅基于您在问题中所写的内容。
首先,让我们来看看 Git 存储库是什么,不是什么:
Git 存储库是提交的集合。这些提交可以使用分支名称、标签名称和其他名称找到,但它不是分支的集合。它是 commits的集合。一个松散的类比可能是昆虫的集合,您在其中标记昆虫:这使得集合包含标签,但它不是标签的集合。也就是说,标签不是收集的目的。
实际的存储库本身存储在.git
目录(或文件夹,如果您更喜欢该术语)中。这个文件夹的存在,以及 Git 需要的特定文件和子文件夹,告诉 Git 这是一个存储库。如果.git
目录本身不存在,或者缺少这些重要文件和/或子文件夹,Git 会说这不是 Git 存储库。
一个裸存储库以文件夹开始和结束.git
(然后通常命名repo.git
或类似名称,而不仅仅是.git
)。从技术上讲,一个裸存储库仍然有一个 index/staging-area,但这只是因为 Git 对 index/staging-area 的实现主要是目录中的一个文件.git
,名为index
. (此文件不必存在:Git 将在需要时创建它。)
非裸存储库,这是您通常使用的那种,也有一个工作树。工作树是您查看和处理文件的地方。这些文件是您计算机上的普通日常文件,以您的计算机喜欢存储文件的方式存储在文件夹中。重要的是要了解这些工作树文件不在 Git 存储库中。
存储库(.git
文件夹)存储提交和其他内部 Git 对象。它们采用对象数据库的形式,Git 在其中通过哈希 ID 查找对象:大的、随机的数字,以十六进制表示。所有这些对象实际上都是只读的:一旦存储在数据库中,就无法更改任何对象。这意味着不能更改任何提交或文件。
存储库还存储一个单独的名称数据库:分支名称、标签名称、远程跟踪名称和其他名称。这些存储名称中的每一个都只包含一个哈希 ID。对于分支名称,存储的哈希 ID 始终是提交的哈希 ID。(允许标签名称存储其他内部 Git 对象哈希 ID,通常不会直接与之交互。)这是查找特定提交的好(且快速)方法。
查找、提取、使用和创建新提交是我们对 Git 存储库所做的大部分工作。由于存储库是如此以提交为中心——而我们对存储库所做的事情绝对是以提交为中心——重要的是要知道提交是什么和做什么,以及Git 如何提取一个:
每次提交都会存储所有文件的完整快照。提交中的文件不是普通的计算机文件形式。1 因为它们被存储为 Git 对象,所以它们都是只读的。甚至 Git 也无法改变它们。而且,因为提交本身是一个 Git 对象,它有一个丑陋的哈希 ID,因此 Git 可以在对象数据库中查找它。
每个提交还存储一些元数据:关于提交本身的信息,例如提交者和提交时间。在此元数据中,Git 存储先前提交的哈希 ID。更准确地说,大多数提交都存储了前一次提交的哈希 ID ,但这条规则也有一些例外。
因为每个提交都是只读的,但您通常需要读取和写入文件,您将告诉 Git提取一些提交。当你这样做时,Git 会将文件从提交中复制出来。为此,Git 将从内部存储的文件(及其内部存储的名称)中读取内部只读数据,并将其转换为您的计算机使用的普通文件夹中的文件设置。这些文件将进入您的工作树。
换句话说,当您运行git checkout
or时git switch
,Git 会执行以下两步序列的优化(和安全检查)变体:
- 首先,Git从工作树中删除所有(跟踪的)文件。
- Then, Git replaces all the files in your working tree with those from the commit you just checked out.
This is why each commit stores every file: because switching from one commit to another will remove all the files from the commit you're leaving behind, and extract, instead, all the files from the commit you're moving to. The commits themselves are completely read-only, so no commit changes in this process, and no files are lost. Only your working tree is emptied and re-filled.
请注意,这是关于从一个提交切换到另一个提交。从一个分支切换到另一个分支不一定会切换提交。为了正确理解这一点,我们应该画出提交的图片。
1它们以一种特殊的、只读的、仅限 Git 的形式存储,在其中它们被压缩并且——对于 Git 的内部操作很重要——<em>去重。此重复数据删除步骤允许每个提交存储每个文件,而不会占用任何额外空间。事实上,许多不同的文件——以及其他内部 Git 对象——可能存储在单个计算机文件中(以一些大而丑陋的哈希 ID 命名,后跟.pack
),尽管有时文件内容存储为 Git 所谓的松散对象。但是,无论哪种方式,这些文件也没有普通的文件名。
在分支中绘制提交的图片
每个提交都有一些大的、丑陋的、看起来随机的哈希 ID。我们不使用这些,而是使用单个大写字母来代替散列 ID。而且,大多数提交都包含一些先前提交的哈希 ID。让我们画一个箭头,而不是使用它,从后面的提交指向前一个提交。我们将从一些哈希 ID 为的提交开始H
:
<-H
提交H
指向向后,我们将调用 commit G
:
<-G <-H
G
当然指向另一个更早的提交,所以我们必须继续:
... <-F <-G <-H
最终,我们将拥有一个提交链,这些提交一直指向第一次提交。让我们A
在这里调用它,并绘制所有的提交——但我现在会变得有点懒惰并使用线条来连接它们,即使箭头实际上只指向backs。Git 无法进入并调整较早的提交以指向前向,因为较早的提交一旦完成,就会一直被冻结。所以我们有:
A--B--C--D--E--F--G--H
在这里,commitH
是链中的最后一个提交。H
我们——或 Git——可以一直工作到链中的第一个提交, commit A
;在那里,一切都停止了,因为没有更早的提交。
H
为了快速找到提交,我们给它一个分支名称,比如main
or master
。2 为了表示这一点,让我们输入名称,其中有一个箭头指向 commit H
:
...--G--H <-- main
如果您现在创建一个新 名称,例如M01
,该名称默认选择相同的提交。我们现在有两个 commit 名称H
:
...--G--H <-- main, M01
你自己的 Git 软件,在你自己的存储库中工作——简称“你的 Git”——只能在这两个分支之一上。为了表示你在哪个分支上,让我们将特殊名称附加HEAD
到这两个分支名称之一:
...--G--H <-- main (HEAD), M01
如果我们现在运行:
git switch M01
Git 将移至HEAD
名称M01
:
...--G--H <-- main, M01 (HEAD)
我们仍在使用commit H
,因为两个名称都选择了相同的 commit。工作树中的文件集将保持完全相同,因为我们没有更改commits。
现在假设我们从工作树中删除一堆文件——可能是整个文件夹中的文件——并创建一些新文件,然后运行git add .
添加删除和创建,然后运行git commit
. 当我们进行这个新的提交时,Git 将保存当时在 Git暂存区中的所有文件。更新了暂存区以匹配我们的工作树。3 这将创建一个新的 commit,它将获得一些看起来随机的哈希 ID,但我们将称之为“commit ”。让我们把它画进去:git add .
I
...--G--H
\
I
CommitI
作为其父级,具有较早的 commit H
。那是因为我们使用commitH
来提交;我们曾经是,现在仍然是,就像这里所说的那样,就在刚才——在我们跑之前——这个名字指向了 commit 。但是现在提交存在,该命令将's hash ID 写入当前分支 name,所以我们拥有的是:I
on branch M01
git status
git commit
M01
H
I
git commit
I
...--G--H <-- main
\
I <-- M01 (HEAD)
提交I
包含我们告诉 Git 它应该有的文件;分支名称M01
选择提交I
;并HEAD
告诉我们我们在分支上M01
。如果我们现在运行:
git switch main
Git 将删除与 commit 一起使用的文件I
并提取与 commit一起使用的文件H
。我们的工作树现在将匹配提交H
,我们将拥有:
...--G--H <-- main (HEAD)
\
I <-- M01
作为我们的提交图片。
2您自己的 Git 可能默认为,master
而 GitHub 现在默认为main
,这会在以后产生一些问题。
3我在这里跳过了一些关于.gitignore
文件、跟踪文件、未跟踪文件以及索引如何工作的重要细节,以便专注于提交和分支。
你做错了什么
因此,如果我通过键入创建一个新的本地文件夹 M02 和一个分支git branch M02
,然后通过 git switch M02 切换到它,它会显示我之前添加到 M02 分支的 M01 分支中的所有内容......
新分支和旧分支当前共享相同的最终提交。
当您查看您的工作树时,您看到的只是您的工作树文件。它们与提交的文件不同,尽管如果git status
说一切都匹配,它们确实匹配提交的文件。提交的文件永远不会——<em>不能——改变。
请注意,Git 只存储文件。分支末尾的提交中的文件M01
可能具有类似M01/somefile
. 这是文件的实际名称:M01/somefile
. 它不是一个名为的文件夹M01
,其中包含一个名为. 你的工作树有这样的设置——里面有文件的文件夹——在你的工作树中,斜线甚至可能是另一种方式,. 但是,在commit中,它只是一个名为. 这一点通常并不重要,但这意味着 Git 根本无法存储空文件夹(因为空文件夹不包含文件,而 Git 只能存储文件)。4somefile
M01\somefile
M01/somefile
但是如果通过键入从 M02 中git rm . -r
删除文件(它会删除本地文件),它也会从 M01 分支和 M02 分支中删除文件。
该git rm -r .
操作清除了 Git 的索引/暂存区域和您的工作树。现有的提交不会——也不能——改变。您现在所做的未来提交将不包含任何文件(因为您删除了所有文件);当您放回一些文件git add
时,未来的提交将包含这些文件。
Git 的 index / staging-area 因此充当您提议的未来提交。您在工作树中创建一个文件并运行git add
以将其复制到 Git 的暂存区。你git checkout
或git switch
某个现有的提交告诉 Git:删除所有当前提交的文件并从另一个提交中交换文件。如果您已经提交了所有内容,这是完全安全的;如果你有未提交的工作是不安全的,或者通常git checkout
会git switch
检测到这些不安全的情况并避免破坏未提交的工作。5
4 Git子模块有一些技巧可以在这里使用,但我们不要深入探讨这些技巧。
5请注意,git checkout
具有“危险”操作模式会破坏未保存的工作。新git switch
命令实现了 的安全部分git checkout
,而新git restore
命令实现了 的不安全部分git checkout
。因此,学习新的git switch
and可能会更好git restore
,这样您就始终知道自己是否正在运行安全检查命令。
由于各种原因,这两个“安全”命令有时都会让您切换分支,即使未保存的工作也是如此。当他们让您切换,但您忘记先保存(添加和/或提交)时,您可以切换回来。当一个人进入完整的细节时,这变得相当复杂。如果您真的想知道,请参阅在当前分支上有未提交的更改时签出另一个分支。
一些进一步的细节
ElpieKay 的回答向您展示了如何创建多个根提交。根提交是与我们上面的提交类似的提交A
:新的空存储库中的第一个提交,它没有父提交。显然,第一次提交不能有先前的提交,所以 Git 必须能够创建根提交。使用git checkout --orphan
orgit switch --orphan
是另一种创建根提交的方式。
这里的 checkout 和 switch 之间有一个很小但很关键的区别:
git checkout --orphan newbranch
让 Git 的索引/暂存区域充满文件(并且根本不会触及您的工作树),但是:
git switch --orphan newbranch
清空 Git 的 index / staging-area(因此,从工作树中删除任何相应的跟踪文件)。如果您打算删除所有跟踪的文件,请使用git switch --orphan
此步骤。