为什么我在其中一个分支进行修改后,git会改变每个分支?

3

我本地仓库有3-4个分支,当我在其中一个分支上工作时,我所做的更改会传播到其他分支。事实上,如果我git reset --hard origin 其中一个分支,它将重置每个本地分支。

我应该如何只保留更改在一个分支中?

请注意,我对git不熟悉。


1
你为什么认为它会影响到每个分支?更改只会保留在你正在工作的分支上。顺便说一下,我并不是在反驳...只是想让你更详细地阐述你的问题。 - eftshift0
@eftshift0 我的意思是每一个单独的变化。即使我在分支“a”上的文件中添加了一行空白,我也会在所有其他分支(比如“b”和“c”)中找到该文件中的新空白行。请注意,我没有提交和推送。我只是按Ctrl+S保存(我只是在做测试)。 如果我没记错的话,分支应该是用来本地保留更改的。只有当我合并时,它们才会传播到“主分支”上。 我错了吗?对之前过于简洁表示抱歉。 - eddie
4
这是关键:注意我没有提交 - 在这种情况下,您没有对任何分支进行任何更改。 - torek
1
如果你要切换分支,相应的更改也会随着你一起切换。 - eftshift0
感谢@torek的帮助。我刚刚尝试了在分支“a”上进行更改,然后使用'git add filename'添加,提交并'git checkout b'。现在它按预期工作,我看不到在分支“a”上所做的更改。 仍然不明白为什么之前会以“错误”的方式运行(您说如果我不提交,我就没有保存任何更改),但还是感谢:) - eddie
@eftshift0 哦,现在清楚了。谢谢 - eddie
1个回答

13
你真正需要的是一份好的教程。我不确定要推荐哪些——Git很难,有很多糟糕的教程,其中许多开始不错和/或有良好的意图,但最终遇到了困难部分。
然而,你现在需要知道的是,分支——或更准确地说,分支名称——并不真正意味着什么。Git实际上完全是关于提交的。在进行新的提交(或以某种方式操纵现有提交)之前,你还没有在Git本身中做任何事情。
不过,提交的一个问题是它们永久冻结。你真的不能在任何提交内部更改任何内容。每个提交都以特殊的只读Git专用冻结格式存储所有文件的完整快照。这意味着它们非常适合归档,但对于进行任何新的工作则完全无用。
因此,Git为你提供了一个可以进行工作的区域。这个区域称为(各种各样的)工作树,或者工作树,或者工作树(我自己喜欢连字符术语),或者任何数量的其他类似名称。在这里,文件只是普通的文件。这意味着你可以使用它们——因此称为工作树。当你使用它们时,Git大多不关心:这个区域是你的工作树。Git只在必要时从提交中填充它。

索引

在Git中进行新提交是有技巧的。其他版本控制系统要简单得多,因为在这些系统中,您的工作区也是您建议的下一个提交。但在Git中并非如此!Git添加了一件更重要的事情,即使您不想知道,也必须了解它。这个东西非常重要,尽管您看不到,Git称之为“索引”、“暂存区”,有时候-现在很少了-称之为“缓存”。所有三个名称都可以指同一件事情。它以不同的方式使用(“缓存”术语现在主要用于内部数据结构,这就是为什么现在它有点罕见),但对索引的相当不错的简短描述是,它保存您的“下一个建议提交”。您可以将其视为保存每个文件副本从当前提交中获取。1

当您更改工作区中的文件时,“索引”中的副本不会发生任何变化。它仍然与您选择的提交中的副本匹配。您必须运行“git add”将文件从工作区复制到索引中。现在,索引副本不再与提交副本匹配,因此您建议下一个提交与当前提交不同。

运行git commit会从索引中构建一个新的提交,该索引包含当前状态。所以在Git中,你可以在工作树中进行操作,然后使用git add将更新的文件复制回索引,然后使用git commit从索引创建新的提交。这有点麻烦,这就是为什么其他系统没有索引的原因:它们不需要让你更新所有文件的中间副本。但是Git有,最好熟悉并了解它。虽然有一些技巧可以尝试隐藏它,2但最终它们会失败:Git中的某些功能只能通过指向索引来解释。

从索引创建了一个新提交后,该新提交成为当前提交。现在当前提交和索引匹配。这也是在git checkout之后正常情况下:当前提交和索引通常匹配。请参见下文的例外情况。


1从技术上讲,索引保存了对内部Git blob对象的{{引用}}。然而,将文件的索引“复制”视为真正的独立副本,在大多数情况下都可以正常工作——只有当您开始涉及Git内部机制时,您才需要了解blob对象。

2例如,您可以使用git commit -a代替git commit。这只是为您运行git add -uadd -u 步骤告诉Git:对于所有已经在索引中的文件,请检查它们是否需要执行git add。如果是,则立即执行。然后提交使用已更新的索引。这里还有一些其他的复杂性,但仅当提交步骤本身失败时才会出现。但是,只有了解索引,才能在这些情况下正确地解释它们。


当你有未提交的更改时,检出另一个分支

当你git checkout某个特定的提交(通过某个特定的分支名称找到)时,Git将从该提交中填充你的索引和工作树。这可能会更新一些文件-无论是在索引还是工作树中-并且如果它们在旧提交和新提交中相同,则不会更改其他文件。

但是,如果您对索引和/或工作树进行了某些更改并且没有提交,Git将尝试在可能的情况下保留该修改。这就是您看到的情况。在这种情况下,您当前的提交和索引不匹配。(在某些情况下,工作树中发生的情况甚至更加复杂。有关此问题的过多信息,请参见Checkout another branch when there are uncommitted changes on the current branch。)

当您进行新提交时,分支名称以有趣的方式更改

在Git中,每个提交都具有唯一的哈希ID。这个哈希ID是由字母和数字组成的一串很长的丑陋字符串。从技术上讲,它是提交内容的SHA校验和的十六进制表示;但是它的主要特点是,每个 Git都将同意提交获取哈希ID,并且没有其他提交可以具有该哈希ID。每个其他提交都有一些其他哈希ID。

哈希ID看起来随机,对人类来说很难记住。计算机可以为我们记住它们。这就是分支名称的真正含义。
请记住,我们上面说过所有提交都被冻结了。但这对于分支名称来说并不正确;如果是这样,那么这些名称的用处将大大降低。
在Git中,分支名称仅包含一个提交的哈希ID。这个提交从定义上来说,是该分支上的最后一个提交。
每个提交都保留一些先前提交的哈希ID。大多数提交仅保留一个哈希ID。在此提交中,这一个哈希ID(以及所有文件的快照)是此提交的父提交。
每当一个Git项目(如分支名称或提交)保存了一个Git提交的哈希ID时,我们就说该项目指向该提交。因此,像“master”这样的分支名称指向一个提交。该提交指向其父提交。其父提交又指向另一个父提交,以此类推。
如果我们使用大写字母来代替这些丑陋的哈希ID,我们可以将所有内容绘制出来:
... <-F <-G <-H   <--master

{{名称}}为master,持有哈希ID为{{H}}的值。{{H}}是最后一次提交。提交{{H}}通过包含提交{{G}}的哈希ID指向其直接父级{{G}}。因此,提交{{G}}又指回其父级{{F}},再次指回去,依此类推。
这种反向指针一直延续下去,直到我们到达有史以来的第一个提交。它不会再进一步指回去,因为它不能。所以这就是行动最终停止的地方。因此,得出以下图示:
A--B--C--D--E--F--G--H   <-- master

代表一个具有八个提交的Git仓库,每个提交都有自己独特的哈希ID和一个分支名namemaster

我们可以像这样添加另一个分支名,也指向提交H

git branch develop
git checkout develop

现在我们需要以一种方式记录我们正在使用的{{分支名称}}。为此,让我们将特殊名称HEAD附加到两个分支名称中的一个:
...--F--G--H   <-- master, develop (HEAD)

请注意,所有八个提交都在{{两个分支}}上。 (这很不寻常:大多数版本控制系统不是这样工作的。)
现在让我们按照通常的方式进行新提交:更改工作树中的一些文件,使用git add将它们复制到索引中,然后运行git commit。
现在Git要做的是将处于索引中的文件打包成一个新提交——它们已经处于冻结格式中,准备好被提交了——并将我们的姓名、电子邮件地址等信息放入新提交中,并计算出此新提交的新的、唯一的、跨所有Git的通用哈希ID。我们是唯一拥有此提交的Git,但我们的哈希ID现在意味着这个提交,且仅此提交,永远不会有其他提交。3让我们简称这个提交为I。Git以提交H为父提交,写出提交I。
...--F--G--H   <-- master, develop (HEAD)
            \
             I
< p > git commit 的最后一步是棘手的部分:Git 现在将 I 的哈希 ID 写入与 HEAD 相附的名称中。 在这种情况下,那就是 develop

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

现在,develop 指向提交 I。之前在 develop 上的提交直到 H 仍然存在于 develop 上。不过,名称 develop 特别选择了提交 I。Git 现在可以从 I 开始向后工作,找到 H,然后是 G,再然后是 F,以此类推;或者可以从 master 开始查找 H,然后向后查找找到 G,再然后是 F,以此类推。
这就是提交在分支上的含义。分支名称标识最后一个提交。Git 然后使用从一个提交指向其父提交的内部、向后连接的箭头来查找前面的提交,并一直这样做,直到到达不再返回任何更早的提交为止。
每个提交存储一个快照——在谁制作提交时,在索引中的所有文件的完整副本——加上这些元数据:谁制作了它以及何时制作的;父哈希 ID(对于合并提交,有两个或多个);以及日志消息,在其中制作提交的人应该告诉您他们为什么制作了该提交。
由于每个提交都有一个唯一的哈希 ID,并且所有宇宙中的 Git 都同意该哈希 ID 表示该提交,因此您可以将两个 Git 连接起来,它们只需检查彼此的哈希 ID 就可以看到谁具有哪些提交。然后,一个 Git 可以将另一个 Git 拥有而另一个需要但没有的任何提交提供给它。这使用了许多计算机科学图论和其他技巧(例如delta 编码),以使发送 Git 发送最少量的实际数据到接收 Git,因此即使每个提交都具有所有文件的完整快照,发送方也只向接收方发送更改。
正如你所想象的那样,这使得哈希ID计算成为Git中真正的魔法源泉。这有点棘手,但实际上它确实能够正常工作。存在哈希ID冲突的可能性,但迄今为止这从未成为真正的问题。另请参见新发现的SHA-1冲突如何影响Git?

摘要:

  • 一个仓库是提交的集合,以及一些名称的集合。
  • 提交由哈希ID标识。每个提交都包含文件的快照和元数据。
  • 每个分支名称或其他名称都保存了一个提交的哈希ID。这是链中的最后一个提交。
  • 每个提交在其元数据中保存有一些先前提交的哈希ID。至少有一个提交没有先前的提交,因为它是第一个提交。大多数其他提交只有一个:它们的一个先前提交。合并提交具有两个或更多先前提交。
  • 提交被永久冻结,但是选择一个最后一个提交的分支名称会随着时间而移动。要添加新提交,您(或Git)需要将其指向先前的提交,然后移动一些分支名称。
  • 传输(git fetchgit push)涉及连接两个Git,并让它们找出它们共享哪些提交,以及发送者将要发送哪些提交。接收方最终必须在某处保存最后一个哈希ID,以便稍后可以再次找到这些提交,但我们还没有介绍这是如何工作的。
  • 同时,索引暂存区是您构建新提交的地方。您无法直接轻松地查看其中的内容,但是我们还没有介绍的git status可以比较其中的内容,并告诉您这些信息。您的工作树是您可以查看和处理文件的地方。您必须将它们复制回索引/暂存区,以便创建保存更新后文件快照的新提交。在执行此操作之前,您所做的一切都只是更改文件的工作树副本。

1
非常感谢您的回答。现在一切都清楚了。非常感谢@torek。 - eddie
非常有用且清晰易懂。谢谢! - Andre W.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接