为什么如果我在我的一个分支中进行更改,git 会更改每个分支?
Why if I make changes in one of my branches, git changes every branch?
在我的本地存储库中,我有 3-4 个分支,当我处理其中一个分支时,我在其中所做的更改会传播到我的所有其他分支。事实上,如果我 git reset --hard origin
其中之一,它会重置每个本地分支。
我应该如何将我的更改限制在一个分支中?
请注意,我是 git 的菜鸟。
您真正需要的是一个好的教程。我不确定推荐哪一个——Git 是 hard,并且有很多 bad 教程,其中许多开始是好的 and/or 有良好的意图,但最终 运行 进入困难的部分。 :-)
不过,此时您需要知道的是,分支——或者更准确地说,分支 names——并没有多大意义。 Git 实际上就是 提交 。在您进行新提交(或以某种方式操纵现有提交)之前,您还没有在 Git 本身中做任何事情。
不过,关于提交的一件事是它们会一直被冻结。您实际上不能 更改 任何提交中的任何内容。每次提交都以特殊的 read-only、Git-only 冻结格式存储所有文件的完整快照。这意味着它们非常适合存档,但对于执行任何 new 工作完全没用。
因此,Git 为您提供了一个 可以 工作的区域。这个区域被称为(不同的)工作树,或工作树,或work-tree(我自己喜欢带连字符的术语)或任何数量的其他类似名称。在这里,文件只是普通文件。这意味着您可以使用它们——因此术语 工作树 。当你和他们一起工作时,Git 基本上不在乎:这个区域是给 你 的,它是 你的 work-tree. Git 只需从提交 if/when 中填写即可。
指数
在 Git 中进行 new 提交很棘手。其他版本控制系统要简单得多,因为在这些其他系统中,你的工作区也是你提议的next提交。 Git不是这样的! Git 增加了一件你必须知道的事情,即使你真的不想知道。这东西是super-important,暴露给你,即使你看不到它。
Git 称这个东西为 index,或 staging area,或者有时——现在很少——缓存。所有三个名称都可以指代同一事物。它以不同的方式使用("cache" 术语现在主要用于内部数据结构,这就是为什么它现在很少见),但是对索引的一个相当不错的简短描述是它拥有您提议的下一次提交。您可以将其视为来自 当前 提交的每个文件的副本。1
当您更改 work-tree 中的文件时,索引 中的副本没有任何变化。它仍然与您选择的提交中的副本匹配。您必须运行git add
将文件从work-tree、复制到索引。现在索引副本不再与提交的副本匹配,因此您建议 下一个 提交与当前提交不同。
运行 git commit
从索引 现在 中的任何内容构建 new 提交。所以在 Git 中,你在 work-tree 中工作,然后使用 git add
将更新的文件复制回索引,然后使用 git commit
从索引中进行新的提交.这是一种痛苦,也是为什么其他系统没有 索引的原因:它们不会让您更新所有文件的 in-between 副本。但是 Git 有,而且最好习惯它,熟悉它。有一些技巧试图隐藏它,2 但它们最终失败了: Git 中的某些东西只能通过指向索引来解释。
从 索引中创建新提交,新提交成为当前提交。现在当前提交和索引匹配。这也是 git checkout
之后的正常情况:当前提交和索引通常匹配。请参阅下面的例外情况。
1从技术上讲,索引持有一个 reference 到内部 Git blob 对象。但是,将文件的索引 "copy" 视为真正的独立副本对于大多数用途来说都很好——只有当您开始进入 Git 内部时,您才需要了解 blob 对象。
2例如,您可以使用 git commit -a
而不是 git commit
。这只是 运行s git add -u
给你的。 add -u
步骤告诉 Git: 对于已经在索引中的所有文件,检查它们是否可以对它们进行 git add
处理。如果是这样,现在就做。 然后提交使用更新的索引。 此处 也有一些额外的并发症,但只有在提交步骤本身失败时它们才会出现。尽管如此,只有当它们出现时,才能通过了解索引来正确解释它们。
在有未提交的更改时签出另一个分支
当你 git checkout
一些特定的提交时——由某个特定的分支名称找到—Git 将 填写 您的索引和来自该提交的 work-tree。这可能会更新一些文件——在索引和 work-tree 中——并保留其他文件,如果它们在旧提交和新提交中都相同。
如果您对索引进行了一些更改 and/or work-tree 并且 没有 提交,但是 Git 会尝试,如果可能,保留该修改。这就是你所看到的。在这种情况下,您当前的提交和索引 不 匹配。 (在 work-tree 中发生的事情在某些情况下甚至更复杂。有关这方面的更多信息,请参阅 Checkout another branch when there are uncommitted changes on the current branch。)
当您进行新提交时,分支名称 会以一种有趣的方式发生变化
Git 中的每个提交都有一个唯一的哈希 ID。这个哈希 ID 是一大串丑陋的字母和数字。从技术上讲,它是提交内容的 SHA 校验和的十六进制表示;但最主要的是 every Git 到处都会同意 this commit gets this 哈希 ID,其他任何提交都不能拥有该哈希 ID。每个其他提交都有一些 other 哈希 ID。
哈希 ID 看起来是随机的,人类不可能记住。 计算机可以帮我们记住它们。这就是分支名称的真正含义。
记住我们在上面说过所有的提交都被永久冻结了。但是 分支名称 并非如此;如果是的话,名字就没那么有用了。
一个分支名称,在Git中,只包含一个提交的哈希ID。根据定义,该提交是分支上的 last 提交。
每个提交都包含一些previous 提交散列 ID。大多数提交只持有一个哈希 ID。这个哈希 ID,在这个提交中(连同所有文件的快照),是这个提交的 parent 提交。
只要一个 Git 项——分支名称或提交——持有 Git 提交的哈希 ID,我们就说该项 指向 提交。所以像 master
这样的分支名称指向一个提交。该提交指向其父级。它的父节点指向另一个父节点,依此类推。
如果我们用大写字母来代表丑陋的大哈希ID,我们可以把这一切都画出来:
... <-F <-G <-H <--master
namemaster
持有哈希 ID H
。 H
是 最后一次 提交。提交 H
通过包含提交 G
的哈希 ID 指向其直接父级 G
。提交 G
因此指向 its 父 F
,后者再次指向,依此类推。
这一切都在继续,这些 backwards-pointing 箭头,直到我们到达有史以来的第一个提交。它没有指向更远的地方,因为它不能。这就是行动最终停止的地方。因此这张图:
A--B--C--D--E--F--G--H <-- master
代表一个 Git 存储库,有八个提交,每个都有自己唯一的哈希 ID,和一个分支 name, master
.
我们可以添加另一个分支名称,同样指向提交H
,像这样:
git branch develop
git checkout develop
现在我们需要以一种方式进行绘制以记住我们正在使用的分支名称。为此,让我们将特殊名称 HEAD
附加到两个分支名称之一:
...--F--G--H <-- master, develop (HEAD)
请注意,所有八个提交都在两个分支上。 (这很不寻常:大多数版本控制系统都不是这样工作的。)
现在让我们以通常的方式进行新的提交:更改 work-tree 中的一些文件,使用 git add
将它们复制到索引中,然后 运行 git commit
.
Git 现在要做的是打包索引中的文件——它们已经是冻结格式,准备提交——到一个新的提交中,把我们的名字和电子邮件地址依此类推到新的提交中,并为这个新的提交计算新的、唯一的、universal-across-all-Gits-everywhere 哈希 ID。我们是唯一 Git with commit,但我们的 hash ID 现在表示 this commit,而 none other , ever.3 让我们简称为这个提交 I
。 Git 以提交 H
作为其父项写出提交 I
:
...--F--G--H <-- master, develop (HEAD)
\
I
git commit
的最后一步是棘手的部分:Git 现在将 I
的哈希 ID 写入 名称,HEAD
附上。在这种情况下是 develop
:
...--F--G--H <-- master
\
I <-- develop (HEAD)
现在 develop
指向提交 I
。 H
之前的 是 在 develop
之前的提交,在 develop
仍然存在。不过,名称 develop
专门选择提交 I
。 Git 现在可以从 I
开始,然后倒退到 H
,然后是 G
,然后是 F
,依此类推——或者它可以从 master
找到 H
,然后反向找到 G
,然后找到 F
,依此类推。
这就是提交在 b 上的意思anch. 分支名称 标识last 提交。 Git 然后使用内部的 backwards-pointing,连接从一个提交到它的父级的箭头来找到 previous 提交,然后继续做直到它到达一个不会再返回的提交。
每次提交都会存储一个 快照——提交时索引中所有文件的完整副本,加上这个元数据: 制作人和制作时间;父哈希 ID(合并提交有两个或多个);和一条日志消息,其中提交的人应该告诉你为什么他们做了那个提交。
因为每个commit都有一个唯一的hash ID,全宇宙所有Git都同意that hash ID表示that 提交,您可以将两个 Git 连接在一起,它们可以检查彼此的哈希 ID 以查看谁有哪些提交。然后,一个 Git 可以给另一个 Git 一个已经拥有的、另一个想要的和没有的任何提交。这使用了大量 CS 图论和其他技巧——例如 delta encoding——使发送方 Git 能够向接收方 Git 发送最少量的实际数据,因此即使每次提交都有所有文件的完整快照,发送方只向接收方发送更改。
3如您所想,这使得哈希 ID 计算成为 Git 中真正的魔法来源。这有点棘手,但它确实在实践中起作用。存在哈希 ID 冲突的可能性,但它从来都不是真正的问题。另见 How does the newly found SHA-1 collision affect Git?
总结:
- A 存储库 是提交和一些名称集的集合。
- 提交 由哈希 ID 标识。每个都包含文件快照以及元数据。
- 每个分支名称或其他名称都包含一次提交的哈希ID。这是链中的 last 提交。
- 每个提交都在其元数据中包含一些先前提交的哈希 ID。至少有一个提交没有以前的提交,因为它是第一次提交。其余大多数人都有一个:他们之前的一次提交。合并提交有两个或更多以前的提交。
- 提交被永久冻结,但是分支名称——挑选出 last 提交——随着时间的推移而移动。要添加一个新的提交,你——或者 Git——让它指向以前的提交,然后移动一些分支名称。
- 传输(
git fetch
和 git push
)涉及连接两个 Git 并让它们弄清楚它们共享哪些提交,以及发送者将要发送哪些提交。接收方最终必须将 last 哈希 ID 保存在某处,以便接收方稍后可以再次找到这些提交,但我们尚未介绍其工作原理。
- 同时,index 或 暂存区 是您构建新提交的地方。你看不到里面有什么——无论如何都不能直接和容易地看到——但是
git status
,我们这里没有介绍,会比较里面有什么,并且可以告诉你关于这些事情。您可以在 work-tree 或 工作树 中查看和处理文件。您必须将它们复制回 index/staging-area 以便进行新提交以保存更新文件的新快照。在您这样做之前,您所做的只是更改文件的工作树副本。
在我的本地存储库中,我有 3-4 个分支,当我处理其中一个分支时,我在其中所做的更改会传播到我的所有其他分支。事实上,如果我 git reset --hard origin
其中之一,它会重置每个本地分支。
我应该如何将我的更改限制在一个分支中?
请注意,我是 git 的菜鸟。
您真正需要的是一个好的教程。我不确定推荐哪一个——Git 是 hard,并且有很多 bad 教程,其中许多开始是好的 and/or 有良好的意图,但最终 运行 进入困难的部分。 :-)
不过,此时您需要知道的是,分支——或者更准确地说,分支 names——并没有多大意义。 Git 实际上就是 提交 。在您进行新提交(或以某种方式操纵现有提交)之前,您还没有在 Git 本身中做任何事情。
不过,关于提交的一件事是它们会一直被冻结。您实际上不能 更改 任何提交中的任何内容。每次提交都以特殊的 read-only、Git-only 冻结格式存储所有文件的完整快照。这意味着它们非常适合存档,但对于执行任何 new 工作完全没用。
因此,Git 为您提供了一个 可以 工作的区域。这个区域被称为(不同的)工作树,或工作树,或work-tree(我自己喜欢带连字符的术语)或任何数量的其他类似名称。在这里,文件只是普通文件。这意味着您可以使用它们——因此术语 工作树 。当你和他们一起工作时,Git 基本上不在乎:这个区域是给 你 的,它是 你的 work-tree. Git 只需从提交 if/when 中填写即可。
指数
在 Git 中进行 new 提交很棘手。其他版本控制系统要简单得多,因为在这些其他系统中,你的工作区也是你提议的next提交。 Git不是这样的! Git 增加了一件你必须知道的事情,即使你真的不想知道。这东西是super-important,暴露给你,即使你看不到它。
Git 称这个东西为 index,或 staging area,或者有时——现在很少——缓存。所有三个名称都可以指代同一事物。它以不同的方式使用("cache" 术语现在主要用于内部数据结构,这就是为什么它现在很少见),但是对索引的一个相当不错的简短描述是它拥有您提议的下一次提交。您可以将其视为来自 当前 提交的每个文件的副本。1
当您更改 work-tree 中的文件时,索引 中的副本没有任何变化。它仍然与您选择的提交中的副本匹配。您必须运行git add
将文件从work-tree、复制到索引。现在索引副本不再与提交的副本匹配,因此您建议 下一个 提交与当前提交不同。
运行 git commit
从索引 现在 中的任何内容构建 new 提交。所以在 Git 中,你在 work-tree 中工作,然后使用 git add
将更新的文件复制回索引,然后使用 git commit
从索引中进行新的提交.这是一种痛苦,也是为什么其他系统没有 索引的原因:它们不会让您更新所有文件的 in-between 副本。但是 Git 有,而且最好习惯它,熟悉它。有一些技巧试图隐藏它,2 但它们最终失败了: Git 中的某些东西只能通过指向索引来解释。
从 索引中创建新提交,新提交成为当前提交。现在当前提交和索引匹配。这也是 git checkout
之后的正常情况:当前提交和索引通常匹配。请参阅下面的例外情况。
1从技术上讲,索引持有一个 reference 到内部 Git blob 对象。但是,将文件的索引 "copy" 视为真正的独立副本对于大多数用途来说都很好——只有当您开始进入 Git 内部时,您才需要了解 blob 对象。
2例如,您可以使用 git commit -a
而不是 git commit
。这只是 运行s git add -u
给你的。 add -u
步骤告诉 Git: 对于已经在索引中的所有文件,检查它们是否可以对它们进行 git add
处理。如果是这样,现在就做。 然后提交使用更新的索引。 此处 也有一些额外的并发症,但只有在提交步骤本身失败时它们才会出现。尽管如此,只有当它们出现时,才能通过了解索引来正确解释它们。
在有未提交的更改时签出另一个分支
当你 git checkout
一些特定的提交时——由某个特定的分支名称找到—Git 将 填写 您的索引和来自该提交的 work-tree。这可能会更新一些文件——在索引和 work-tree 中——并保留其他文件,如果它们在旧提交和新提交中都相同。
如果您对索引进行了一些更改 and/or work-tree 并且 没有 提交,但是 Git 会尝试,如果可能,保留该修改。这就是你所看到的。在这种情况下,您当前的提交和索引 不 匹配。 (在 work-tree 中发生的事情在某些情况下甚至更复杂。有关这方面的更多信息,请参阅 Checkout another branch when there are uncommitted changes on the current branch。)
当您进行新提交时,分支名称 会以一种有趣的方式发生变化
Git 中的每个提交都有一个唯一的哈希 ID。这个哈希 ID 是一大串丑陋的字母和数字。从技术上讲,它是提交内容的 SHA 校验和的十六进制表示;但最主要的是 every Git 到处都会同意 this commit gets this 哈希 ID,其他任何提交都不能拥有该哈希 ID。每个其他提交都有一些 other 哈希 ID。
哈希 ID 看起来是随机的,人类不可能记住。 计算机可以帮我们记住它们。这就是分支名称的真正含义。
记住我们在上面说过所有的提交都被永久冻结了。但是 分支名称 并非如此;如果是的话,名字就没那么有用了。
一个分支名称,在Git中,只包含一个提交的哈希ID。根据定义,该提交是分支上的 last 提交。
每个提交都包含一些previous 提交散列 ID。大多数提交只持有一个哈希 ID。这个哈希 ID,在这个提交中(连同所有文件的快照),是这个提交的 parent 提交。
只要一个 Git 项——分支名称或提交——持有 Git 提交的哈希 ID,我们就说该项 指向 提交。所以像 master
这样的分支名称指向一个提交。该提交指向其父级。它的父节点指向另一个父节点,依此类推。
如果我们用大写字母来代表丑陋的大哈希ID,我们可以把这一切都画出来:
... <-F <-G <-H <--master
namemaster
持有哈希 ID H
。 H
是 最后一次 提交。提交 H
通过包含提交 G
的哈希 ID 指向其直接父级 G
。提交 G
因此指向 its 父 F
,后者再次指向,依此类推。
这一切都在继续,这些 backwards-pointing 箭头,直到我们到达有史以来的第一个提交。它没有指向更远的地方,因为它不能。这就是行动最终停止的地方。因此这张图:
A--B--C--D--E--F--G--H <-- master
代表一个 Git 存储库,有八个提交,每个都有自己唯一的哈希 ID,和一个分支 name, master
.
我们可以添加另一个分支名称,同样指向提交H
,像这样:
git branch develop
git checkout develop
现在我们需要以一种方式进行绘制以记住我们正在使用的分支名称。为此,让我们将特殊名称 HEAD
附加到两个分支名称之一:
...--F--G--H <-- master, develop (HEAD)
请注意,所有八个提交都在两个分支上。 (这很不寻常:大多数版本控制系统都不是这样工作的。)
现在让我们以通常的方式进行新的提交:更改 work-tree 中的一些文件,使用 git add
将它们复制到索引中,然后 运行 git commit
.
Git 现在要做的是打包索引中的文件——它们已经是冻结格式,准备提交——到一个新的提交中,把我们的名字和电子邮件地址依此类推到新的提交中,并为这个新的提交计算新的、唯一的、universal-across-all-Gits-everywhere 哈希 ID。我们是唯一 Git with commit,但我们的 hash ID 现在表示 this commit,而 none other , ever.3 让我们简称为这个提交 I
。 Git 以提交 H
作为其父项写出提交 I
:
...--F--G--H <-- master, develop (HEAD)
\
I
git commit
的最后一步是棘手的部分:Git 现在将 I
的哈希 ID 写入 名称,HEAD
附上。在这种情况下是 develop
:
...--F--G--H <-- master
\
I <-- develop (HEAD)
现在 develop
指向提交 I
。 H
之前的 是 在 develop
之前的提交,在 develop
仍然存在。不过,名称 develop
专门选择提交 I
。 Git 现在可以从 I
开始,然后倒退到 H
,然后是 G
,然后是 F
,依此类推——或者它可以从 master
找到 H
,然后反向找到 G
,然后找到 F
,依此类推。
这就是提交在 b 上的意思anch. 分支名称 标识last 提交。 Git 然后使用内部的 backwards-pointing,连接从一个提交到它的父级的箭头来找到 previous 提交,然后继续做直到它到达一个不会再返回的提交。
每次提交都会存储一个 快照——提交时索引中所有文件的完整副本,加上这个元数据: 制作人和制作时间;父哈希 ID(合并提交有两个或多个);和一条日志消息,其中提交的人应该告诉你为什么他们做了那个提交。
因为每个commit都有一个唯一的hash ID,全宇宙所有Git都同意that hash ID表示that 提交,您可以将两个 Git 连接在一起,它们可以检查彼此的哈希 ID 以查看谁有哪些提交。然后,一个 Git 可以给另一个 Git 一个已经拥有的、另一个想要的和没有的任何提交。这使用了大量 CS 图论和其他技巧——例如 delta encoding——使发送方 Git 能够向接收方 Git 发送最少量的实际数据,因此即使每次提交都有所有文件的完整快照,发送方只向接收方发送更改。
3如您所想,这使得哈希 ID 计算成为 Git 中真正的魔法来源。这有点棘手,但它确实在实践中起作用。存在哈希 ID 冲突的可能性,但它从来都不是真正的问题。另见 How does the newly found SHA-1 collision affect Git?
总结:
- A 存储库 是提交和一些名称集的集合。
- 提交 由哈希 ID 标识。每个都包含文件快照以及元数据。
- 每个分支名称或其他名称都包含一次提交的哈希ID。这是链中的 last 提交。
- 每个提交都在其元数据中包含一些先前提交的哈希 ID。至少有一个提交没有以前的提交,因为它是第一次提交。其余大多数人都有一个:他们之前的一次提交。合并提交有两个或更多以前的提交。
- 提交被永久冻结,但是分支名称——挑选出 last 提交——随着时间的推移而移动。要添加一个新的提交,你——或者 Git——让它指向以前的提交,然后移动一些分支名称。
- 传输(
git fetch
和git push
)涉及连接两个 Git 并让它们弄清楚它们共享哪些提交,以及发送者将要发送哪些提交。接收方最终必须将 last 哈希 ID 保存在某处,以便接收方稍后可以再次找到这些提交,但我们尚未介绍其工作原理。 - 同时,index 或 暂存区 是您构建新提交的地方。你看不到里面有什么——无论如何都不能直接和容易地看到——但是
git status
,我们这里没有介绍,会比较里面有什么,并且可以告诉你关于这些事情。您可以在 work-tree 或 工作树 中查看和处理文件。您必须将它们复制回 index/staging-area 以便进行新提交以保存更新文件的新快照。在您这样做之前,您所做的只是更改文件的工作树副本。