【问题标题】:how to edit and update files for different git branches?如何编辑和更新不同 git 分支的文件?
【发布时间】:2021-02-12 13:02:40
【问题描述】:

我的 GitHub 中的存储库有两个分支:mastersolution。首先我git clone

git clone <master url>

然后我 cd 到那个文件夹并切换到 solution 分支

git checkout solution

我发现文件的内容仍然与master 中的相同,例如README.md。如何访问solution 文件?

然后我尝试git pull 更新solution 分支中的文件

git pull origin solution

它工作了,现在文件的内容是solution,但是当我想切换回master时,它失败了,说我需要合并,因为我认为有些文件在两者中有不同的内容分支机构。如何切换回来?

一般来说,如何编辑和更新不同分支的文件以及如何轻松地来回切换?

另一个例子:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2     
              \
               M--N
                   \
                    P

是否需要另一个工作树?

【问题讨论】:

  • 不确定是否可以。我通常使用 git stash。这是一个不同的解决方案,但它解决了相同的问题 - 在工作副本之间切换。这是一篇很棒的文章atlassian.com/git/tutorials/saving-changes/git-stash
  • 关于编辑:什么 name 找到哈希 ID 为 P 的提交?从提交 P 你可以回到提交 N 然后 M 等等,但是你将如何找到 P 本身?
  • 我可以从L 工作到P 吗?这里我也一头雾水,这种情况下我需要用git worktree add吗?

标签: git github git-merge git-pull git-checkout


【解决方案1】:

Git 新手通常认为 Git 将更改存储在分支中。这不是真的。不过,就您而言,我认为您遇到的事实是,当您在 Git 存储库中工作时,您是在 Git 所谓的 工作树 中进行的。您在此处所做的任何事情Git 中都没有(目前)。

您可能希望使用git worktree add 来处理您的特定情况。在介绍了 Git 如何处理所有这些之后,我们会谈到这一点,因为如果没有很多基础知识,它就没有任何意义。

我想解释一下,Git 根本不存储更改,也不真正关心分支。 Git 存储和关心的是 commits。这意味着您需要知道提交是什么以及为您做什么、如何找到提交、如何使用现有的提交以及如何进行一个新的提交。

什么是提交

当您使用 Git 工作时,您将使用的基本实体是 commit。关于提交,您需要了解三件事。你只需要记住这些,因为它们是任意的:没有什么特别的理由必须这样做,只是当 Linus Torvalds 编写 Git 时,这些是他做出的决定。

  1. 每个提交都有编号。

    然而,这些数字并不是简单的计数:我们没有提交 #1 后跟提交 2、3、4 等等。取而代之的是,每个提交都会获得一个唯一但又大又丑的数字,以十六进制表示,介于 1 和非常大的数字之间。1Everyevery 中提交存储库 获得一个唯一的、随机的数字。

    看起来是随机的,但不是。它实际上是内部对象内容的加密校验和。这种特殊的编号方案使两个 Git 能够通过相互传递这些大数字来交换内容。

    这样做的一个关键副作用是物理上不可能更改提交中的内容。 (Git 的所有内部对象都是如此。)原因是哈希 ID,这是 Git 找到对象的方式,内容的校验和。取出其中一个,对其内容进行更改,然后将其放回去,你得到的是一个新的提交(或新的其他内部对象),具有一个新的和不同的哈希 ID。现有的 ID 仍在其中,在现有 ID 下。这意味着即使是 Git 本身也无法更改已存储提交的内容。

  2. 每个提交都存储一个每个文件的完整快照

    更准确地说,每个提交都存储了 Git 在您或任何人进行提交时所知道的每个文件的完整副本。当我们了解如何进行 new 提交时,我们将稍后进入这个“知道”部分。

    这些副本是只读的、压缩的,并以只有 Git 本身可以读取的格式存储。它们也被去重复,不仅仅是在每个提交中,而是在每个提交中。也就是说,如果您的 Git 存储库有一些特定的 README 文件或其他文件的副本,存储在某个提交中,并且您曾经进行过 new 具有相同副本的提交 文件——即使是在其他一些名称下——Git 只会重新使用以前的副本。

  3. 而且,每次提交都会存储一些元数据

    提交的元数据包括提交人的姓名和电子邮件地址。 Git 从你的user.nameuser.email 设置中得到这个,并且简单地相信你就是你声称的那个人。它们包括您(或任何人)提交的日期和时间戳何时2元数据还包括为什么您(或谁)以提交消息的形式提交。 Git 对消息中的内容并不特别严格,但它们通常应该看起来很像电子邮件,有一个简短的单行主题,然后是一个消息正文。

    不过,此元数据的一部分仅适用于 Git 本身。每个提交在其元数据中存储上一个提交的提交编号。3这将提交形成简单的后向链:

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

    这里,每个大写字母都代表一些实际的提交哈希 ID。提交H,最近的一个,里面有之前提交G的实际哈希ID。当 Git 从 Git 保留所有提交的任何位置提取较早的提交 G 时,提交 G 在其中包含早于G 提交 F 的实际哈希 ID。

    我们说提交H指向提交G,它指向提交F。提交F 又指向一些更早的提交,它指向另一个更早的提交,依此类推。这会一直追溯到第一次提交,即第一次提交——不能向后指向,所以它不会。

Git 存储库中的这个向后看的提交链该存储库中的历史记录。历史就是提交;提交是历史;而Git向后工作。我们从最近的开始,并根据需要向后工作。


1对于 SHA-1,数字介于 1 和 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,975 之间。这是十六进制的ffffffffffffffffffffffffffffffffffffffff,或 2160-1。对于 SHA-256,它介于 1 和 2256-1 之间。 (使用任何无限精度计算器,例如 bcdc 来计算 2256。它非常大。在这两种情况下,零都保留为空哈希。)

2实际上,有两个用户电子邮件时间三元组,一个称为“作者”,一个称为“提交者”。作者是自己编写提交的人,而且——在 Git 被用于开发 Linux 的早期——提交者是通过电子邮件收到补丁并将其放入的人。这就是 为什么 提交消息的格式就像它们是电子邮件一样:通常,它们电子邮件。

3大多数提交都只有一个先前的提交。至少有一个提交——第一次提交——之前没有个提交; Git 将此称为 root 提交。一些提交指向 两个 较早的提交,而不仅仅是一个:Git 称它们为 merge commits。 (合并提交可以指向两个以上的早期提交:具有三个或更多父级的提交称为 octopus 合并。它们不会做任何你不能用多个普通合并做的事情,但是如果您将多个主题捆绑在一起,他们可以以一种简洁的方式做到这一点。)


分支名称是我们查找提交的方式

Git 总能通过其丑陋的大哈希 ID 找到任何提交。但是这些哈希 ID 又大又丑。你能记住你的一切吗? (我不记得我的了。)幸运的是,我们不需要记住所有。请注意,在上面,我们如何能够从 H 开始并从那里向后工作。

所以,如果提交是在向后指向的链中——它们确实是——并且我们需要从某个链中的 最新 提交开始,我们如何找到 的哈希 ID最后链中的提交?我们可以把它写下来:把它记在纸上,或者白板上,或者其他什么东西上。然后,每当我们进行 new 提交时,我们可以删除旧的(或将其划掉)并记下新的最新提交。但是我们为什么要为此烦恼呢?我们有一台计算机:为什么我们没有记住最近的提交?

这正是分支名称的含义和作用。它只是保存链中 last 提交的哈希 ID:

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

名称 master 保存最后一次提交 H 的实际哈希 ID。和以前一样,我们说名称master指向这个提交。

假设我们现在想要创建第二个分支。让我们起一个新名称,developfeaturetopic 或任何我们喜欢的名称,也指向提交 H

...--F--G--H   <-- master, solution

两个名称标识相同的“最后一次提交”,因此直到H 的所有提交现在都在两个分支上。

branch 名称的特殊之处在于,我们可以使用 git switch 或在 Git 2.23 之前的 Git 中使用 git checkout 切换到该分支。我们说git checkout master,我们得到提交H,并且“开启”master。我们说git switch solution,我们也得到了提交H,但这次我们“开启”了solution

为了告诉我们使用哪个名称来查找提交H,Git 将特殊名称HEAD 附加到一个(也是唯一一个)分支名称:

...--F--G--H   <-- master, solution (HEAD)

如果我们现在进行 new 提交——稍后我们将了解 如何——Git 通过使用 commit @ 写出新提交来创建新提交987654365@ 作为其父级,以便新提交指向H。我们将新提交称为I,尽管它的实际编号只是其他一些看起来很随机的大哈希 ID。我们无法预测哈希 ID,因为它取决于我们制作它的确切秒数(因为时间戳);我们只知道它将是独一无二的。4

让我们画出新的提交链,包括 Git 使用的诡计:

...--F--G--H   <-- master
            \
             I   <-- solution (HEAD)

在完成新提交 I 后,Git 将新提交的哈希 ID 写入 当前分支名称 solution。所以现在 name solution 标识了提交 I

如果我们切换回 name master,我们将看到提交中的所有文件 H,当我们再次切换回 solution 时,我们会看到'将看到提交I 中的文件。或者,也就是说,我们可能以这种方式看待它们。但我们可能不会!


4pigeonhole principle 告诉我们这最终会失败。大size 的散列ID 告诉我们失败的可能性很小,实际上,它永远不会发生。 birthday problem 要求散列非常大,deliberate attacks 已经从 SHA-1 的纯理论问题转变为至少在理论上可行的问题,这就是 Git 转向更大、更安全的散列的原因。


进行新的提交

现在是时候更仔细地了解我们实际上是如何进行上述I 的新提交了。请记住,我们提到提交中的数据——构成快照的文件——是完全只读的。提交以特殊的、压缩的、只读的、仅 Git 的格式存储文件,只有 Git 本身可以读取。这对于做任何实际的工作都是毫无用处的。

出于这个原因,Git 必须 提取从提交中的文件到某种工作区。 Git 将此工作区称为您的工作树工作树。这个概念非常简单明了。 Git 只是从提交中获取“冻干”文件,重新水化或重组它们,现在您有了可用的文件。这些可用的工作树文件副本当然是副本。你可以对他们做任何你想做的事情。这些都不会触及提交中的任何原件。

正如我在上面提到的,这些文件的工作树副本不在 Git 中。他们在你的工作区。它们是 您的 文件,而不是 Git 的。你可以做任何你想做的事或与他们一起做。当你告诉 Git 这样做时,Git 只是从一些现有的提交中填充。在那之后,它们都是你的。

但是,在某些时候,您可能希望 Git 进行 new 提交,并且当它这样做时,您希望它更新 its 文件你的文件。如果 Git 只是重新保存所有自己的文件不变,那将毫无用处。

在其他非 Git 版本控制系统中,这通常非常简单。您只需在 Mercurial 中运行,例如 hg commit,Mercurial 就会读回您的工作树文件,将它们压缩成自己的内部形式,5 并进行提交。这当然需要已知文件的列表(例如,hg add 更新列表)。但是 Git 没有这样做:这太容易了,而且/或者可能太慢了。

相反,Git 所做的是与工作树中的提交 分开保留每个文件自己的额外“副本”。该文件采用“冻干”(压缩和去重)格式,但实际上并不像提交中的那样冻结。实际上,每个文件的第三个“副本”位于 提交和您的工作树之间。6

每个文件的这个额外副本存在于 Git 所称的位置,不同的是,index,或 暂存区,或者——现在很少——缓存。这三个名称都描述了同一件事。 (主要实现为一个名为.git/index的文件,除了这个文件可以包含将Git重定向到其他文件的指令,并且可以让Git对其他索引文件进行操作。)

所以,当你切换到某个特定的提交时,Git 会做什么:

  • 从该提交中提取每个文件;
  • 将原始数据(和文件名)放入Git的索引中;和
  • 将 Git 格式(“冻干”)文件提取到您的工作树中,您可以在其中查看和处理它。

当你运行git commit 时,Git 所做的是:

  • 将索引的内容打包,作为保存的快照;
  • 组装和打包所有适当的元数据以创建提交对象 — 这包括使用当前提交的哈希 ID 作为新提交的父级,将新提交点返回到当前提交;
  • 将所有内容写成一个新的提交;和
  • 将新提交的哈希 ID 填充到当前的分支名称中。

因此,在您运行git commit 时,索引(即暂存区)中的任何内容都会被提交。这意味着,如果您在工作树中更改了 内容(无论是修改某个文件、添加新文件、完全删除文件还是其他任何内容),您都需要复制更新后的文件回到 Git 的索引(或者从 Git 的索引中完全删除文件,如果想法是删除文件)。通常,您用来执行此操作的命令是git add。此命令采用一些文件名并使用该文件或那些文件的工作树副本来替换该文件或那些文件的索引副本。如果文件从您的工作树中丢失(因为您删除了它),git add 也会通过从那里删除文件来更新 Git 的索引。

换句话说,git add 表示制作此文件的索引副本/这些文件与工作树副本匹配。仅当文件是全新的(在您运行 git add 时不存在于索引中)时,该文件才真正添加到索引中。7 对于大多数文件,它实际上只是替换现有副本

文件的索引副本类似于 Git:它存储在所有内部对象的大数据库中。但是,如果文件的索引副本以前从未提交,则它处于不稳定状态。直到您运行 git commit,并且 Git 将索引中的所有内容打包并将其转换为新的提交,它才会安全地提交到 Git 并且不能被删除或销毁。8


5Mercurial 使用非常不同的存储方案,它经常存储差异,但偶尔存储快照。这几乎无关紧要,但 Git 提供并记录了可以直接进入其内部存储格式的工具,因此了解 Git 的内部存储格式有时很重要。

6因为它总是去重复,这个文件的“副本”最初不占用空间。更准确地说,它的内容不需要空间。它在 Git 的索引文件中占用了一些空间,但相对较小:每个文件通常只有几十或几百字节。该索引只包含文件名、一些模式和其他缓存信息,以及一个内部 Git 对象哈希 ID。实际的 content 存储在 Git 对象数据库中,作为内部 blob 对象,这就是 Git 进行重复数据删除的方式。

7也许git add 应该被称为git update-indexgit update-staging-area,但已经有一个git update-index。 update-index 命令需要了解 Git 如何将文件存储为内部 blob 对象:它对用户不是很友好,而且实际上并非旨在成为您自己会使用的东西。

8一个提交的文件在 Git 中作为一个永久且完全只读的实体存在——但它的 永久,这里的前缀大多是谓词关于提交的持久性。 可以完全放弃提交。如果您从未向任何其他 Git 发送过某个特定的提交,那么从您自己的 Git 存储库中删除该提交将使其真正消失(尽管不是马上)。完全放弃提交的一个大问题是,如果您已经将它发送到其他 Git,其他 Git 可能会在稍后再次将其返回给您:提交在某种程度上是病毒式的。当两个 Git 彼此发生 Git-sex 时,其中一个可能会捕获提交。


总结

所以,现在我们知道什么是提交:带有编号的对象,由两部分组成,数据(快照)和元数据(信息)通过它们的元数据向后串在一起。现在我们也知道了分支名称是什么:它们存储了提交的哈希 ID,我们应该在某个链中调用 last(即使它之后还有更多提交)。我们知道任何提交中的任何内容都无法更改,但我们始终可以添加 new 提交。要添加新的提交,我们:

  • 让 Git 提取现有提交,通常按分支名称;
  • 弄乱我们工作树中的文件;
  • 使用git add 更新我们想要更新的任何文件:这会将更新的内容从我们的工作树复制回Git 的索引;和
  • 使用git commit 进行新的提交,更新分支名称。

如果我们进行一些这样的提交:

...--G--H   <-- main, br1, br2

并将HEAD 附加到br1 并进行两个新的提交,我们将获得:

          I--J   <-- br1 (HEAD)
         /
...--G--H   <-- main, br2

如果我们现在将HEAD 附加到br2 并进行两次新的提交,我们将得到:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2 (HEAD)

请注意,在每个步骤中,我们只是向存储库中所有提交的集合添加了一个提交name br1 现在标识 its 链上的最后一次提交;名称br2 标识其链上的最后一次提交;并且名称 main 标识该链上的最后一次提交。提交H 及更早版本在所有三个分支上。9

始终只有一个当前提交。它由HEAD 标识:HEAD 附加到您的分支名称之一。当前提交的 文件 通过 Git 的索引被复制到您的工作树中,并且也只有一个工作树和一个索引。如果您想切换到其他分支名称,并且该其他分支名称反映了其他提交,您将不得不切换 Git 的索引和您的工作树。10


9其他版本控制系统占据其他位置。例如,在 Mercurial 中,提交只在 one 分支上。这需要不同的内部结构。

10这并不完全正确,但细节变得复杂了。见Checkout another branch when there are uncommitted changes on the current branch


git worktree add

现在我们知道如何使用我们的一个工作树、Git 的一个索引和一个单一的HEAD,我们可以看到从一个分支切换到另一个分支是多么痛苦:我们所有的工作树文件每次切换时都会更新(无论如何,脚注 10 中提到的复杂情况除外)。

如果您需要在两个不同的分支中工作,有一个简单的解决方案:制作两个单独的克隆。每个克隆都有自己的分支、自己的索引和自己的工作树。但这有一个很大的缺点:这意味着您有两个完整的存储库。它们可能会占用大量额外空间。11 而且,您可能不喜欢处理多个克隆和涉及的额外分支名称。相反,如果您可以共享底层克隆,但拥有另一个工作树,该怎么办?

为了使第二个工作树有用,这个新工作树必须有自己的索引自己的HEAD。这就是git worktree add 所做的:它在当前工作树之外的某处创建一个新工作树,12 并为该新工作树提供自己的索引和HEAD。添加的工作树必须位于某个未在主工作树中检出的分支上,并且未在任何其他添加的工作树中检出。

因为添加的工作树有自己独立的东西,所以您可以在其中进行工作而不会干扰您在主工作树中所做的工作。因为两个工作树共享一个单一的底层repository,所以无论何时你在一个工作树中进行新的提交,它都会立即在另一个工作树中可见。因为提交更改存储在分支名称中的哈希 ID,添加的工作树不得使用与任何其他工作树相同的分支名称(否则链接分支名称、当前提交哈希 ID、工作树内容和索引内容之间的混乱)——但是添加的工作树总是可以使用 分离的 HEAD 模式(我们在这里没有描述) .

总体而言,git worktree add 是一种很好的方式来处理您的情况。如果您要为此做大量工作,请确保您的 Git 版本至少为 2.15。 git worktree 命令是 Git 2.5 版中的新命令,但如果您有一个分离的 HEAD 或在其中工作的速度很慢,并且您还在主工作树中执行任何工作,它可能会咬您一口;直到 Git 2.15 版才修复此错误。


11如果您使用路径名进行本地克隆,Git 将尝试硬链接内部文件以节省大量空间。这大部分解决了这个问题,但是有些人仍然不喜欢拥有两个独立的存储库,并且随着时间的推移空间使用量也会增加。使用 Git 的 alternates 机制也有一些技巧可以解决这个问题。例如,我相信 GitHub 使用它来让分叉更好地为他们工作。但总的来说,git worktree 填补了一个感知空白。也许你会喜欢它。

12从技术上讲,添加的工作树不必位于主工作树之外。但把它放在里面是个坏主意:它只会让人感到困惑。把它放在别的地方。通常,“就在隔壁”是一个不错的计划:如果您的主要工作树位于 $HOME/projects/proj123/ 中,您可以使用 $HOME/projects/proj123-alt$HOME/projects/proj123-branchX 或其他。

【讨论】:

  • thx,我试过git switch,它可以工作,并且不同的分支单独工作,就像您在摘要中绘制的数字一样。我还需要使用git worktree add吗?
  • 如果您对git switch / git checkout 和(单个)工作树中文件的改组感到满意,则无需添加另一个工作树。如果您对在唯一的工作树中混洗文件感到满意,并且您的 Git 至少为 2.5(最好至少为 2.15),请添加更多工作树以避免改组文件效果。
  • 我发现如果两个分支有不同的文件和文件名,当我git switch时,文件一直显示在不同的分支中。如何处理?
  • 听起来在这种情况下,你根本就没有告诉 Git 这个文件的存在。在这种情况下,它仍然是一个未跟踪的文件。它不在任何一个提交中,因此 Git 不必删除并替换它。它只是您留在工作树中的一个文件。 Git 不会管它的。
  • 我创建了一个文件和git addgit commit,然后我git rm 删除文件,然后我git push,它给出了错误。为什么会失败?如何解决?
【解决方案2】:

如果您想在分支之间切换(此处为 Master 和 Solution),您可以通过两种方式进行。例如,如果您在“解决方案”分支中进行了更改,并且想要切换到“主”分支。

  1. 如果您对“解决方案”分支中的更改感到满意,您可以在切换到“主”分支之前提交更改。

  2. 如果您不想提交更改,您可以存储更改。这将允许您将所做的所有更改存储在一个文件中,并将您的分支(“解决方案”)返回到您进行这些更改之前的状态。

我发现在分支上工作的最佳工具是SourceTree

【讨论】:

  • 我试图切换,但它说我需要合并,失败了。
  • 在切换之前,您是否在当前分支中提交了更改?
  • 是的,它有一个警告,我需要合并。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-09
  • 2011-12-29
  • 2018-06-07
相关资源
最近更新 更多