为什么github会在我推送到的每个新存储库中显示我以前的所有提交



我不是github专业人士,这就是我需要清晰的原因。每次我在github上创建一个新的repo并向其推送新的cnent时,我都会发现其他存储库上以前的所有提交也都添加到了我的新repo中。除了一两个看起来不太好的文件夹外,我所有的转发都很相似。当我在本地机器,也就是我的笔记本电脑上的repo文件夹中添加新文件夹时,我导航到该repo文件夹,然后导航到我添加的新文件夹,并运行git添加和运行git状态,我发现没有添加任何临时文件夹。然而,当我回到repo文件夹并运行git add时。它添加了我刚刚添加的新文件夹的所有内容,这听起来不错,但当我进行提交和推送时,它会将以前的所有提交添加到新的repo中。

这是一个Git问题,而不是GitHub问题,它源于最初没有正确教授如何使用Git。(除非你完全自学成才,否则这不是你的错,也不完全是你的错。如果你是自己学习的,请注意,很多Git教程都从不太好到糟糕。)

Git主要是关于提交。Git存储库首先是提交的集合。因此,你需要知道提交是什么,它对你有什么作用,你如何创建一个新的存储库,Git如何找到一个存储库,还有很多:Git可能会变得非常复杂。但让我们简单地开始:

  • Git存储库主要由一对数据库组成。一个数据库包含提交和其他支持对象,另一个数据库则包含name(如分支和标记名称)。我们稍后将了解第二个数据库的原因。

  • 一个有用的Git存储库——例如,你在笔记本电脑上使用的那种——还附带了一个工作树,我们也会使用它。(GitHub或Bitbucket等服务器上的Git存储库是一个"裸"存储库,它只是省略了工作树,这样就没有人可以在上面工作:这很有用,因为它允许git push发送到该存储库。但这不是你在笔记本电脑上想要的。你想要一个正常的、非裸的存储库。)

Git将这些数据库,加上Git需要操作的一堆辅助文件,放入一个隐藏的目录或文件夹中(这些术语现在可以互换,所以可以使用您喜欢的任何一个)。此隐藏文件夹名为.git,在大多数正常存储库中,此隐藏的.git文件夹位于工作树的顶层。事实上,Git就是这样计算你的工作树的

例如,假设您的主目录(文件夹)是/home/bob(Linux)或/Users/bob(macOS)或其他什么。在这个文件夹中,您可能有许多子文件夹。例如,我喜欢在src目录中组织我的源,而像Go这样的一些子系统也需要(或过去需要)特定的路径。所以你可以:

cd /home/bob/src/fun
mkdir experiment1
cd experiment1

创建一个新的有趣的实验源项目。在这个文件夹中,您可能会有frontend/backend/子文件夹,用于某种web服务器玩具,或者您计划尝试的任何东西。如果你想让一个Git存储库包含所有这些,你现在可以运行:

git init

创建一个新的、完全空的存储库(两个空数据库)。这将创建/home/bob/src/fun/experiment1/.git/并填充一堆文件来保存各种数据库和其他内容。

您现在可以自由创建子文件夹和cd。假设您在这里将py作为Python代码子文件夹,并将cd放入其中。当您运行Git命令时,他们会在这里查找隐藏的.git,而找不到它。所以他们会爬上一个路径元素,从/home/bob/src/fun/experiment1/py//home/bob/src/fun/experiment1/,并在这里寻找隐藏的.git这一次他们找到了,所以您的工作树植根于/home/bob/src/fun/experiment1/存储库本身位于/home/bob/src/fun/experiment1/.git中。

第一次提交很奇怪,有点特别

在这一点上,您仍然根本没有提交。没有提交的存储库不能有任何分支。幸运的是,Git不需要分支来操作,但你真的想有一个分支名称,所以Git无论如何都会设置一个分支,即使你没有。如果你在这个时候运行git status,Git会告诉你你是on branch masteron branch main或其他什么,即使这个分支不存在!

您可以更改不存在但正在使用的分支的名称,例如使用git init --initial-branch=main,但由于它实际上并不存在,因此它并不是很重要。尽管如此,您还是需要非常快地进行第一次提交,以便分支名称开始存在。我喜欢创建一个初始README文件或类似文件,并提交:

$ echo playing around with the foo language > README
$ git add README
$ git commit -m "initial commit"

(这里的初始提交消息是枯燥且模板化的,因为初始提交本身是枯燥且模版化的)。

有了这个初始提交,这样你就有了一个分支,你现在可以更改,这很好。但是,如果你打算使用GitHub或其他网站,你可以选择跳过所有这些工作,方法是使用他们的网站中创建一个带有一个初始提交的存储库,使用点击式web按钮或其他方式。如果你这样做,那么你就可以将最初的单个提交存储库git clone转移到你的笔记本电脑上,跳过整个git init,并在你身边创建一个提交步骤。git clone操作将为您设置名为origin远程,并复制一个初始提交,将所有这些放入一个新存储库中,该存储库是通过创建一个空目录(与上面的mkdir experiment1相同)创建的,然后在其中为您运行git init

这是你的选择,是自己进行初始提交,还是让GitHub或其他什么为你做一个,但要选择其中一个:不要两者都做如果同时执行这两个操作,您将获得两个初始提交,稍微有些奇怪和特殊,这会在以后造成麻烦。(这不是无法克服的,但如果你只选择一个,你可以跳过它,这是一个烦恼。)

如果您自己运行git init并进行自己的初始提交,请记住创建您的GitHub或任何具有初始提交的存储库,然后自己执行git remote add origin步骤。

Commits

以下是您需要了解的关于提交的信息:

  • 每个提交都有编号。这些数字是巨大的,用十六进制表示时看起来很奇怪,很随机,而且它们是unique的。Git自己生成它们(使用加密校验和技术和大量可能的数字来保证这种唯一性;除非数字唯一的,否则Git无法工作)。

  • 每个提交存储两件事:每个文件的完整快照和一些元数据

  • 所有这些东西都是只读的。(这是使编号方案发挥作用所必需的。)

还有更多的东西要学,但我们现在就到此为止。

完整快照通过其他非提交对象(间接)存储在大对象数据库中,同时进行提交。此完整快照中的文件名为py/main.py,并带有嵌入式(正向)斜杠(即使在Windows上也是如此):没有文件夹,只有这些文件名。Git知道文件夹,并会根据需要在您的计算机上制作它们,以便以后能够提取提交的文件,但Git不存储文件夹(这就是为什么您不能存储空目录)。文件以一种特殊的压缩格式保存,并且(重要的是)消除重复,这样即使每次提交都存储每个文件,Git存储库也不会变得非常胖。

提交中的元数据包含有关提交本身的信息。例如,这包括您的姓名和电子邮件地址,以及一些日期和时间戳等。对于Git自己的操作至关重要的是,每个提交的元数据都包含一些早期提交的原始哈希ID。大多数提交都只持有一个这样的散列ID:这些是普通提交。一些提交是合并提交,并持有两个(或者,从技术上讲,两个或更多)以前的提交哈希ID。

任何非空存储库中至少有一个提交是特殊的:其先前提交哈希ID列表为。这就是我们做的一个奇怪的、有点特别的模板初始提交,只是为了让它摆脱困境。正如我们将看到的那样,该提交最终将出现在每个分支上。Git将此提交称为(或)提交。

Git将所有这些东西——提交对象及其所有支持对象——存储在几乎所有存储库的大型全对象数据库中1这个所有对象数据库是一个简单的键值存储,以哈希ID为键,因此Git需要哈希ID才能快速找到提交。因此,我们将不得不记住一些哈希ID,尽管很快,我们将看到如何避免这种情况。

因为每个提交(除了根提交)都至少保存一个以前的提交哈希ID,而大多数都只保存一个,所以我们只需要记住最新的提交哈希ID。假设我们有一个大约有八个提交的存储库。我们将分配大写字母来代替哈希ID,例如H代表哈希,并绘制一个这样的提交:

<-H

在这张图中,从提交中伸出的那个小箭头表示提交H的元数据中存储的哈希ID。也就是说,给定H的哈希ID,Git可以(快速)从对象数据库中获取提交H,并在其中找到前一次提交的哈希ID。让我们称之为提交G,并将其绘制为:

<-G <-H

提交GH一样,是一个普通的提交,因此它存储了更早提交的哈希ID。Git可以找出这个哈希ID,并使用它来查找更早的提交:

... <-F <-G <-H

这种情况会一直持续下去,或者更确切地说,直到Git达到有史以来的第一个提交:大概是提交A。这是我们的根提交,它允许Git停止向后工作。

请注意,Git是向后工作的,从最后一次提交开始我们需要以某种方式记住最新提交的哈希ID。哈希ID很难看,人类不可能记住,那么我们在这里能做什么呢?


1如果您创建一个存储库,并在其中只放入几个微小的提交,然后创建数百万个分支和标记名称,那么存储库的大部分由名称组成,而不是提交。这一点都不正常,但这就是为什么要把这句话作为脚注。


分支名称为我们记忆哈希ID

与其us记忆最新的提交哈希ID,我们为什么不将其存储在文件中,甚至可能存储在一个简单的键值数据库中呢?这是一个好主意:让我们有一个名称的数据库,比如分支和标记名称,并让每个存储一个哈希ID。

如果我们分支名称定义为";这个东西保存最近提交的散列ID";,把它画进去,我们得到:

...--F--G--H   <-- main

(假设我们使用的是分支名称main)。名称main包含H的哈希ID,因此H指向HH指向GG指向F等等。它们存储在提交(元数据中)中,并且提交是完全只读的。因此,如果H指向G,这意味着H永远指向G。箭头没有指向前方,因为当我们制作G时,我们不知道H的哈希ID是什么。(哈希ID取决于所有东西,包括那些精确的时间戳,我们不知道,当我们制作G时,我们将来制作H时间。我们也不知道G的ID是什么,H的哈希包括在G的哈希ID进入H后对其进行哈希!)

如果我们有多个分支名称,每个这样的名称都指向一些提交。因此,如果我们现在想创建一个新名称,例如developfeature或其他什么,我们选择一些现有的提交——可能是H,因为它是最新的——并将名称指向那里:

...--G--H   <-- develop, main

现在我们需要一种方法来记住我们使用的名称。我们将让Git将特殊名称HEAD附加到一个分支名称上,如下所示:

...--G--H   <-- develop, main (HEAD)

这意味着我们正在使用commitH,并且我们正在通过名称main执行

如果我们现在运行git switch developgit checkout develop,我们得到的是:

...--G--H   <-- develop (HEAD), main

我们仍然使用commitH,但现在我们通过名称develop这样做

为什么这很重要?答案与我们进行提交时会发生什么有关。现在,请注意每个提交都在两个分支上。一旦我们创建了新的分支名称,commitH就在两个分支上。以前,提交H一个分支上提交H时没有任何更改(因为没有任何更改),但包含它的分支集无论如何都发生了更改!

这是一条线索:分支名称实际上并不是很重要——除了一个方面:它们让我们和Git找到一些提交。Git需要散列ID,而分支名称包含散列ID。这就差不多了,还解释了下一点。

进行新的提交会更新当前分支名称

假设我们现在有了这个:

...--G--H   <-- develop (HEAD), main

并且我们以通常的方式进行新提交(不用担心"方式"是什么)。新的提交将是一个普通的提交,因此它将有一个父级。当进行新提交时,新提交的父级将是我们使用的任何提交。我们使用的是H,所以新提交的父级将是H:我们的新提交将向后指向H

新的提交将获得一个新的、不可预测的(看起来随机,但实际上不是随机的)哈希ID,它是唯一的——在我们获得该ID之前,世界上任何地方的Git存储库都不能使用该ID,在我们获得它之后也没有人可以使用它2——但我们只将其称为I,并将其绘制为:

....--G--H   <-- main

I   <-- develop (HEAD)

在获得了I的哈希ID(通过将提交I写入数据库)后,Git将新的哈希ID填充到当前分支名称中,因此现在HEAD所附的develop指向I

提交H保留在developmain上,但新的提交I当前仅在分支develop上。只有名称develop才能找到I。有两种方法可以找到H:使用main,或者使用develop,然后向后跳一跳。


2从数学上讲,这是不可能永远保持下去的(参见鸽子洞原理,Git注定有一天会失败。哈希空间的大小将这一天带到了未来,我们可以希望我们都死了,也许整个宇宙在发生熵热死亡之前就已经结束了。


您的工作树和Git的索引/暂存区

我只想稍微谈谈这一点,以保持简短(好吧,无论如何都是简短的er),但是:

  • 使用git switchgit checkout签出提交,告诉Git从我的工作树中删除我使用的上一个提交中的所有文件,并将我切换到的提交中的文件放在适当的位置。

    我们需要这样做,因为每次提交中存储的文件都会一直冻结,作为一种永久存档。它们的格式只有Git可以读取,而且任何东西——甚至Git本身——都不能覆盖。为了完成任何实际的工作,我们需要每个程序都可以读写的格式的文件。这些";工作";文件进入";"工作区域";,Git称之为工作树

  • 奇怪的是——或者至少,如果你习惯了其他早期版本的控制系统,这会很奇怪——此时Git会将每个文件的额外副本填充到Git称之为索引暂存区的区域中。这个东西是如此重要,和/或最初命名得如此糟糕,以至于它实际上有三个名称:第三个是缓存,但现在这个名称大多出现在git rm --cached等标志中。

    这个额外的中间区域中的文件位于冻结的当前提交和工作树之间,是冻结的格式,预消除重复。但与提交中的文件不同,它们可以被大规模替换。这就是git add的作用。

在工作树文件上运行git add时,Git读取工作树文件,将其压缩为内部格式,并检查重复内容。如果内容已经存储在存储库中任何提交的任何位置,Git此时会重新使用该文件。如果没有,Git现在有一个准备好的副本,可以进行新的提交。

无论哪种方式,运行git add之前,索引都会保存一份预消除重复的文件副本(因此只是对提交中的同一文件的引用),准备提交;运行git add之后,索引会保存一个预消除重复并准备提交的文件副本。

换句话说,在任何时候,索引保存所有文件,准备提交。这是您提出的下一个快照。它从您签出的提交开始匹配这个快照。运行git add时,会更新建议的下一个快照

如果添加了一个全新的文件,Git会像往常一样压缩和检查重复的内容,然后在索引槽中将一个新名称(正斜杠名称的来源)写入索引,而不是引导出准备好的旧文件。所以你可以添加全新的名字。您还可以使用git rm从Git的索引中删除名称。事实上,由于git add的意思是使索引副本与工作树副本匹配,因此可以使用git add删除索引副本3

最终,您运行git commit,此时Git会打包索引中的任何内容,这就是新提交的快照。除非调用git addgit rm或类似的Git命令,使Git更新其索引,否则您对工作树所做的一切都与无关。当执行git commit时,Git会拍摄暂存文件的快照,并在其索引/暂存区域中仔细排列,以制作出您能构建的最漂亮的图片。这就是为什么你可以称之为集结区


3也就是说,假设您签出了一个提交,并且它有一个名为path/to/file的文件。您可以从工作树中删除该文件,但不能从Git的索引中删除。然后运行git add path/to/file。Git看到它的索引中有一个副本,但工作树副本已经不见了。git add命令表示删除索引副本以匹配。我自己觉得这很奇怪,甚至很诡异,从来没有故意使用过;我更喜欢运行git rm


git status简介:经常使用

git status命令:

  • 打印出当前分支的名称:on branch main或其他名称
  • 检查当前分支是否有上游,如果是,将告诉您领先和/或落后(我们在这里根本不涉及这一点);然后
  • 运行两个git diff操作

git diff操作比较两个快照。您可以在任何两次提交中使用它,它会告诉您这两次提交的不同之处。但您也可以在Git的索引/阶段区域和工作树上使用它,这就是git status如何告诉您为提交而暂存的更改和为提交而未暂存的更改。

当您比较两个实际的、现有的提交(如上图中的GH)时,Git会提取这两个快照,并查看哪些文件完全相同,从而消除了重复,哪些文件不同。完全相同的、消除重复的文件很无聊,它根本没有提到这些!它只提到不匹配的文件。

当使用完整的git diff时,Git会为每对不退出的文件玩一个"发现差异"游戏,并打印出将文件的旧版本或左手边版本更改为新版本或右手边版本的配方。但你可以要求它只说哪些文件,以及它们是被修改的,还是新添加的或删除的。

CCD_ 122命令使用这种更快的模式;只需告诉我哪些文件已更改";mode—比较当前提交建议的新快照。对于已更改(或新文件或已删除文件)的每个文件,git status告诉您该文件的名称,并表示该文件已被暂存以供提交。对于匹配的文件,它什么也没说:那些太无聊了!

然后,打印出该列表后,git status执行第二个快速模式diff,将每个索引文件与每个working tree的文件进行比较。在这里,对于不同的每个文件,git status会再次列出它们,这一次将它们放在非提交部分。

注意;阶段a改变";然后再次更改相同的文件:

$ git switch main
$ echo more data >> existing-file
$ git add existing-file
$ echo another change >> existing-file

现在所有三个副本都不同,因此git status将把existing-file列为为提交暂存和未为提交暂存。这不是大多数人在大多数时候做大部分工作的方式,但如果你使用高级Git,你可以使用git add -p来故意设置这种东西。

这里还有最后一件事,那就是未跟踪的文件。再次,我将省略所有重要的细节,但这里有一个简单的";未跟踪文件":未跟踪文件是指当前存在于工作树中,但当前不存在于Git索引的文件仅此而已。因为你可以更改Git索引中的内容,所以你可以更改某个文件是被跟踪还是未被跟踪——但请记住,从一个提交切换到另一个提交会通过从你切换到的提交填充Git索引来更改Git的索引。

这省略了所有类型的特殊情况(例如,请参阅在当前分支上有未提交的更改时签出另一个分支),但一开始就有很多内容。

最新更新