当结账并使用回购的不同分支推送时,您计算机上的明显文件发生了怎样的变化



我将github repo的一个特定分支克隆到本地机器上,进行了一些更改,然后推送到github网站。现在我想把同样的改变推到另一个分支。但当我尝试克隆该分支时,它会说"致命的:目标路径‘REPO’已经存在,并且不是一个空目录。">

所以我的下一个想法是,我应该为repo的每个分支创建一个新的目录。但我想知道,Github可能为此做了一个系统。

所以我在网上看了看,似乎确实有这个。

用git编辑分支?

但我很难理解这一点。

首先,看起来我应该只克隆整个repo,而不是repo的特定分支。

接下来的步骤似乎是

git结账新分支

所以我想知道当我这样做的时候会发生什么。我认为发生两件事中的任何一件

A: 当我克隆repo时,它也将所有分支克隆到我的计算机上,它们都是隐藏的,但当我执行gitcheckout newbranch时,我计算机上的明显文件会更改为该分支。

B: 只有master在我的计算机上,当我执行git checkout newbranch时,它会用我指定的在线分支替换我计算机上的文件。

C: 还有别的。

我们稍后会得到您的答案。它涉及使用Git所称的索引工作树(或workingtree或类似)。但让我们从纠正一些错误印象开始。

首先,看起来我应该克隆整个repo,而不是repo的特定分支。

一般来说,是的,但它比这更多,或者实际上可能更少。这里真正的问题是人们对"分支"这个词的假设。Git根本不是关于分支,而是关于commit。Git中的基本存储单元是提交。

每个提交都保存一个快照:一组完整的文件。这是提交的数据部分。但每个提交还包含元数据,例如提交人的姓名和电子邮件地址,他们提交的日期和时间戳,以及他们提供的日志消息,通过该消息他们可以告诉您为什么提交。还有一件事对每次提交都至关重要,但在开始之前,我们应该谈谈如何命名提交。

任何提交的真实名称都是其哈希ID。每个提交都有自己唯一的哈希ID,在您(或任何人)创建提交时分配给它。这个散列ID是一个由字母和数字组成的大而丑陋的随机字符串,例如08da6496b61341ec45eac36afcc8f94242763468。虽然它看起来是随机的,但实际上它完全依赖于提交中的数据和元数据:它实际上是提交内容的加密校验和。

这意味着,一旦做出承诺,每一项承诺都将永远冻结。其名称是其内容的校验和。如果你试图更改提交的内容,即使只是其中某个字节中的一个比特,你也会得到一个新的、不同的提交,带有一个不同的哈希ID。你真正完成的只是添加一个新的提交。原始提交保留在存储库中。因此,存储库是一个庞大的提交集合,在大多数情况下,它只是随着时间的推移而不断变大1

这就引出了每个提交元数据中的关键项,即父散列ID的列表。通常这个列表只有一个条目——一个父散列——这是这个提交的唯一父散列。父级是在我们现在查看的提交之前的提交。有些提交——合并提交——有两个或多个父级,并且至少有一个提交没有父级:我们在一个新的、最初为空的存储库中进行的第一次提交是从无到有创建的。在此之前没有提交。

如果我们用单个大写字母替换提交哈希ID,并从A开始按顺序分配它们,那么一个只有三个提交的存储库可能会将它们排列如下:

A <-B <-C

也就是说,commitC——三者中的最后一个——将commitB的哈希ID作为其父项。我们说提交C指向提交B,因为我们可以从C读取B的哈希ID。我们可以使用它来查找提交B。同时提交B点来提交A,所以现在我们可以找到A。CommitA没有父级——Git将其称为根提交,并且我们可以在找到所有三个提交后停止回溯历史。


1有可能"忘记"旧的提交,但会有一些困难,如果再也找不到提交,可以将其删除。Git将自动垃圾收集这些提交和其他无用的Git对象。但对于像这样可收集的提交或其他对象,它必须是真正的垃圾。通常,您可以通过"放弃"一些提交,用新的和改进的提交替换它们来实现这一点。新的提交会得到新的、不同的哈希ID:同样,不可能更改任何现有的提交。然后,我们使用分支名称的特性来放弃原始提交,转而使用新的和改进的提交。我们不会在这里展示它是如何工作的,但通过一些逻辑推理,你可以自己弄清楚。


分支名称如何进入此图片

当我们看到这种图表时——显示提交,用字母或点替换它们的实际哈希ID(●)或者其他什么——我们可以在存储库中看到各种提交的图形表示:

● ←● ←● ←●

例如,显示了四个提交(没有名称)。这些序列可以形成分支状结构:

D--E
/
A--B--C

F--G

但是,无论我们在存储库中有多少提交——通常非常多——它们的真实名称都是丑陋的随机散列ID。我们怎样才能区分一个和下一个?尤其是对于人类来说,08da6496b61341ec45eac36afcc8f9424276346808da96b61341ec45eac36afcc86f942427634684惊人地相似,尽管这些都是完全不同的哈希ID,但没有好的方法来处理原始哈希IDGit可以做到;我们不能。

Git需要一种快速的方法来找到分支中的最后一个提交,而我们(人类)需要一种谈论分支的方法。所以Git给我们提供了分支名称:

D--E   <-- master
/
A--B--C

F--G   <-- dev

名称masterdev只是保存分支中最后一次提交的实际哈希ID。最后一次提交指向前一步,即倒数第二次提交。这个提交指向另一个步骤,依此类推。在这种情况下,两个分支都快速指向提交C,它指向指向BA(然后我们停止,因为我们用完了父级)。

请注意,提交A-B-C在Git中的两个分支上。当我们添加和删除分支名称时,包含提交的分支集会动态更改。

要向分支添加新的提交,我们使用:

git checkout master   # select commit `E`

或:

git checkout dev      # select commit `G`

它从给定的提交中填充一个工作区域,还记得我们使用的提交和分支名称。我喜欢通过将名称HEAD添加到所选分支名称中来绘制此图:

D--E   <-- master (HEAD)
/
A--B--C

F--G   <-- dev

这告诉我们现在正在使用提交E和名称master

当你进行新的提交时,Git所做的是打包所有文件的完整快照,并写出一个新的提交:你的姓名、电子邮件地址、当前日期和时间以及日志消息都会进入这个新的提交。此新提交的父级是当前提交,在本例中为E。新的提交进入存储库:

H
/
D--E   <-- master (HEAD)
/
A--B--C

F--G   <-- dev

并且,作为最后一步,git commit将新提交H的实际哈希ID写入当前分支名称master:

D--E--H   <-- master (HEAD)
/
A--B--C

F--G   <-- dev

因此,现在名称master选择提交H。我们仍然分支master上,因为特殊名称HEAD仍然附加在名称master上。

这就是为什么Git是关于提交的,而使用分支名称

上面向我们展示了分支名称如何通过将哈希ID存储到分支名称中来找到提交。我们说分支名称指向分支中的最后一个提交。Git将其称为分支的提示提交。提示提交保存了所有文件的完整快照,还可以让Git找到一个更早的提交,因为提示指向其父文件。它的父级保存所有文件的快照,并指向历史上的又一步。

存储库中的历史记录只不过是所有提交,从所有分支名称开始,一步一个脚印地向后工作。委员会是历史;历史是(所有或任何选定的子集)提交。

提交快照已冻结

我们已经提到,每次提交中的所有内容都会一直冻结。这包括所有快照文件。因为被冻结,所以多次提交共享文件是可能的。如果您没有更改文件,或者事实上,如果您确实更改了文件,但随后将其更改回,则您现在进行的新提交可以以相同的方式共享任何以前提交的文件的副本。

事实上,Git正是这么做的。每个提交都与使用相同文件内容的其他提交共享其冻结格式的文件。因此,尽管Git存储库只是随着时间的推移而不断增长,大多数提交都非常小:它们大多只有一两个,甚至没有文件或新版本的文件。所有的旧东西都被重新利用了。

但出于同样的原因,提交的内容被冻结。它们也是一种特殊的、只读的、压缩的、只有Git才能使用的格式。这意味着您实际上根本无法使用提交来执行任何工作。Git必须将每个提交提取到一个工作区域中。

工作区域

每个存储库都有一个主要工作区域,2,Git称之为工作树的工作树或该名称的一些变体。自Git 2.5以来(但有些讨厌的错误直到Git 2.15才修复),Git支持使用git worktree add添加额外的工作树,但根据您的工作方式,您可能不需要它们。

工作树非常简单:Git将提交中的所有文件提取到工作树中,将它们转换回您可以使用的普通日常文件,以及您所有的计算机程序都可以使用的文件。当您git checkout某个特定的提交时——例如,作为签出不同分支的一部分——Git将删除由于先前签出而出现在工作树中的文件,并将其替换为来自其他提交的文件版本。

不过,除此之外,还有一些非常重要的事情需要知道,这就是Git在当前提交和工作树之间的


2此规则的例外是存储库,它只是一个没有工作树的存储库。大多数允许git push的服务器存储库都是空的,原因我们在此不介绍。


索引

Git的索引对于实际使用Git至关重要,但很难直接看到3名称index是一种通用的名称,不是很有用或有意义,所以很多Git使用短语stagingarea而不是单词index。这两个意思是一样的。Git的一些(大部分是旧的)部分也使用了单词cache,这也意味着同样的东西——索引。所以这个东西有三个单词或短语。但是到底是什么

这个问题的答案有点复杂,但如果我们忽略复杂性——主要是与处理冲突的合并操作有关——Git的索引真的很简单。它可以用一个短语来描述为,在这里构建下一个提交

当您git checkout某个特定的提交时,在Git更新工作树之前,Git必须更新其索引。索引中包含当前提交。实际上,索引保存当前提交的每个文件的副本4这就是Git如何知道Git将哪些文件放入工作树的方法。

因此,当你git checkout一些其他提交时,Git会读取索引,会从你的工作树中删除Git放在那里的文件。它读取您切换到的提交,并将文件的这些版本复制到索引中,并将那些文件(解压缩后可用)放入工作树中。

这意味着,除了一些我们不会在这里讨论的例外,就在git checkout之后,索引中的所有文件都与您当前的提交相同——例如,您使用git checkout mastergit checkout dev选择的提交。同时,你的工作树也有这些文件。因此,每个文件都有三个副本。关键区别在于:

HEAD        index      work-tree
---------   ---------  ---------
README.md   README.md  README.md
^           ^          ^
frozen      invisible  plain text;
in commit   but over-  yours to
writable   work with

文件的索引副本是git commit在进行提交时将使用的副本。因此,如果您确实对工作树副本进行了更改,则需要将这些更改复制到索引副本中。您可以使用git add:执行此操作

# edit README.md
git add README.md

git add步骤读取工作树版本,将其缩小为冻结和压缩的格式,并用新的格式覆盖索引副本。现在,新的README.md已准备好进行下一次提交。

请注意,当前提交后的其他所有文件仍在索引中,仍为冻结格式,并且仍准备就绪。运行git commit将使用该冻结文件生成新的快照。


3要直接查看,请尝试git ls-files --stage。请注意,这会为索引中的每个文件打印出一行,通常是很多行!

4事实上,索引持有对当前提交中冻结格式副本的引用。但是,索引允许您覆盖此内容,而无需更改冻结的格式副本。效果是,就好像索引保存了冻结格式文件的完整副本,您可以在不影响提交本身的情况下覆盖索引的副本。除非您开始了解--stage显示的内容,否则您可以认为索引具有每个文件的冻结格式副本。


未跟踪的文件

因为你的工作树是你的,Git允许你把不在索引中的文件放进去。Git调用这些文件未跟踪。根据定义,未跟踪文件是指工作树中但不在索引中的任何文件。因为它不在索引中,所以您进行的提交将没有该文件的副本。

如果您希望在工作树中创建的全新文件在下一个提交中,则必须git add该文件。这将它的一个副本(冻结格式)放入索引中,准备提交。下一个git commit将拥有该文件。同时,它现在在索引中,因此现在被跟踪

如果您有一个被跟踪的现有文件(在索引中,可能是因为它是从提交中复制出来的),并且您希望下一次提交而不是拥有该文件,则可以在该文件上使用git rm。这将从索引和工作树中删除文件。现在,该文件根本不存在,也不会出现在下一次提交中。对于下一次提交来说,重要的是从索引中删除了该文件。例如,您可以将文件保留在工作树中。但请记住,无论您在这里做什么,文件仍在现有的提交中。它不会出现在您做出的下一个(新)承诺中。

摘要

当您使用git checkout从一个分支名称切换到另一个分支时,Git:

  • 弄清楚你指的是哪个提交
  • 根据需要取消填充索引和工作树:必须删除当前提交中的文件,但不在您要切换到的文件中
  • 根据需要重新填充索引和工作树:必须创建要切换到的提交中但不在索引和工作树中的文件
  • 根据需要更新索引和工作树:两个提交中的文件,但在旧的和新的提交中不同,必须交换掉。错误的旧的必须换成正确的新的

为了速度和其他各种有用的效果,Git会检查哪些文件在新旧提交中是相同的,并且在切换提交时,不会在索引和工作树中触摸它们。这使得在许多情况下从一个分支切换到另一个分支的速度非常快(每当两个不同提交中的大多数文件都相同时)。

您不必(再次)克隆存储库。

您可以使用git worktree命令,正如我在"使用Git的多个工作目录?"中提到的那样

这样,您就可以在一个单独的文件夹中签出(实际上切换)另一个分支,而无需再次克隆存储库。

示例:

cd /path/to/my/cloned/repo
git worktree add -b emergency-fix ../temp master
pushd ../temp
# ... hack hack hack ...
git commit -a -m 'emergency fix for boss'
popd
git worktree remove ../temp

最新更新