TL;博士
我有超过 2 个 [分支],我想使用打开的单个 git UI...
这是否可能,如果可能,如何实现,取决于用户界面。一般地询问 Git 不会让你得到答案。命令行答案是使用git worktree
(非常需要 Git 2.15 或更高版本)。
长
- 如何获取现有的 repo,将其作为孤立分支导入并保留历史记录?
你没有,真的。这个操作——和问题——可能没有意义,因为我怀疑你是指Git所说的孤立分支。请继续阅读以决定它是否有意义。
到底什么是 Git 存储库?
Git存储库本质上由两个数据库组成。一个数据库只保存 Git对象,其中最有趣的是一个称为提交的数据库,每个提交都代表所有文件的完整快照。1 另一个数据库包含名称——分支名称master
和标签名称v2.1
——这些名称是您(至少最初是 Git)如何找到有趣的提交的方式。
每次提交——再次代表所有文件的快照;提交不包含更改——由其哈希 ID 唯一标识。哈希 ID 是一个看起来很随机的大而丑陋的字母和数字字符串,但实际上是提交的全部内容的加密校验和:快照,加上告诉您创建快照的元数据(姓名和电子邮件地址),何时(时间戳)、为什么(日志消息)等等。因为每个提交都存储了它的前一个提交或父提交的实际哈希 ID,所以 Git 很容易从最后一个提交开始并向后工作:
... <-F <-G <-H <-- master
因此,像这样的分支名称只是保存了分支中最后一次提交master
的哈希 ID 。历史本身只是从提交开始形成的提交链——在这个例子中是 <code>H——然后向后工作,一次一个提交,从提交到父提交。
这里有一个小问题,因为链条不一定是线性的。在完成了与上述类似的一系列提交之后,我们可能还会有第二个提交序列:
G--H <-- master
/
...--E--F
\
I--J <-- develop
在这里,commitsF
和更早版本都在两个分支上,而commitsG-H
仅on且仅on 。如果我们然后合并到,我们会得到一个稍微特别的提交:master
I-J
develop
J
master
G--H
/ \
...--E--F K <-- master
\ /
I--J <-- develop
虽然 commitK
像往常一样有一个简单的快照,但它现在有两个父级,而不是一个,使它成为一个合并提交。要查看历史记录K
,我们必须同时返回提交H
和 J
两者。从那里我们回到G
and I
; 从那里我们回到F
,历史重新汇聚的地方,在合并时出现分歧。
换句话说,Git 向后工作:历史在合并时逻辑上收敛,并且由于 Git 向后工作,历史实际上在合并时发散。从逻辑上讲,历史在你分出第二个分支的那一点发生分歧,但在 Git 中它实际上在这一点上收敛,因为 Git 是向后工作的。
分支名称的特别之处master
在于它总是指向我们想说的最后一个提交在分支上。这一点特别重要,因为您正在询问孤儿分支。
1其他三种对象类型是树(树保存文件名)、blob(每个 blob 是文件的内容)和带注释的标记,例如v2.1
. Git 使用 commit + tree + blob 组合来构造每个提交所代表的快照。
Git 如何进行新的提交:索引和工作树
- 是否可以使用作为不同文件夹打开的 2 个孤立分支?
如果你有 Git 2.5 或更高版本——由于 Git 2.5 的初始实现中存在一些错误,2.15 或更高版本是一个好主意——你可以在两个不同的工作树git worktree
中同时使用两个不同的分支。现在是讨论 Git 的索引和工作树概念的时候了,之后我们将讨论孤立分支的定义。
Git 提交快照中的所有内容都被永久冻结。任何提交的任何部分(无论是其日志消息、用户名、父哈希 ID,以及作为该提交的一部分存储的任何已保存文件的任何部分)都不能更改。 任何现有的提交,由一些现有的哈希 ID 标识,都不能被更改。 它的所有文件都被及时冻结。(它们也被压缩了,有时非常压缩。如果你愿意,你可以把它们想象成冻干的。)这对归档来说非常有用:你可以随时回到任何以前的提交。但它对完成任何新工作毫无用处。
为了让你完成工作,Git 让你能够检查提交。签出提交会做三件事:
第一个也是最明显的一点是,它会“重新水化”一个冻干的提交,将其所有文件提取到某种工作区域,在那里它们具有正常的、非冻结的、非 Git 化的形式。这个工作区通常就在存储库本身旁边,是您的工作树(或工作树,有时是工作目录或这种拼写的某种变体。)
第二个,一旦你想到它也很明显,如果你使用git checkout master
orgit checkout develop
或其他什么,它会记住你用来从那个分支获取最新提交的分支名称。或者,如果您曾经git checkout <hash-id>
回到过去,它会记住哈希 ID。无论哪种方式——通过分支名称或哈希 ID——它也会记住你有哪个提交。
第三个,大部分是不可见的,git checkout
在这里做的是填写 Git 的index。
称这个东西为索引是一种无用的名称——索引到底传达了什么?——所以它还有另外两个名称:它有时称为暂存区,有时称为缓存区,这取决于 Git 的是谁或哪一部分做这个电话。不过,这三个名字都是为了同一件事。索引是什么以及所做的在合并过程中会变得有点复杂,但它的主要作用是它以 Git 化的形式保存来自提交的所有文件,准备冻结,但是 - 与真正的提交不同 - <em>实际上并没有冻结。
这意味着索引包含将进入下一次提交的所有文件。换句话说,这是一种提议的下一次提交。你开始:
git checkout master
对于由 name 标识的提交中的每个文件master
,您现在拥有的不是两个而是三个该文件的副本:
HEAD:file
是存储在提交中的文件。它无法更改:它是 Git 化的、冻结的和只读的。用git show HEAD:file
来看。
:file
是存储在索引中的文件。它可以改变!它是 Git 化的,但您可以随时将其替换为新副本。用git show :file
来看。
file
是存储在您的工作树中的文件。这是一个普通的文件,你可以用它做任何你想做的事情。使用普通(非 Git)命令来查看或更改它或做任何你想做的事情。
如果您更改了一些类似 的文件file
,并且您希望 Git在下一次提交中存储新版本,您现在必须更新您提议的下一次提交:
git add file
这会将工作树文件复制到索引中,并用工作树:file
中文件的新 Git 化副本覆盖file
。
因此,索引始终包含建议的下一次提交。您使用 更新此提案git add
。
请注意,如果您是git checkout
其他分支,则将 next-commit 提案替换为与您刚刚签出的提交匹配的不同提案。(此规则有一些例外,故意的;请参阅Checkout another branch when there are uncommitted changes on the current branch。)这反过来意味着索引和工作树实际上是一对:索引索引工作树。当您对工作树进行更改时,通过更改一些文件,您需要git add
通过这些文件来更新您的索引。
当你运行git commit
时,Git 所做的是:
- 保存您的姓名和电子邮件地址;
- 保存当前时间(新提交的时间戳);
- 从您那里收集一条日志消息,以进入新的提交;
- 使用当前提交的哈希 ID 作为父哈希 ID;
- 将所有这些以及索引中的 Git 化文件保存到一个新的提交中,该提交会自动获取一个新的哈希唯一哈希 ID(通过计算所有这些数据的加密校验和)
- 将新提交的哈希 ID 写入当前分支
也就是说,如果你有:
...--F--G--H <-- master
你现在有:
...--F--G--H--I <-- master
该名称 现在记录了您刚刚进行的新提交master
的哈希 ID 。I
该新提交的父项是 commit 的哈希 ID H
,这是您在进行此新提交之前签出的那个。
历史就是这样形成的!做一个新的提交,当你运行时,Git刚刚从索引中的任何内容进行git commit
,创建我们的新提交I
。新提交的父提交是您让 Git 签出的提交。因为 Git从索引、索引和新匹配进行提交,就像他们第一次运行git checkout master
获取 commit时所做的一样H
。现在一切看起来都很好,您可以修改工作树中的内容,将git add
其复制回索引中,然后运行git commit
以创建一个新J
的,其父级是I
并且其保存的快照来自索引。
建立一个新分支
现在您知道现有分支是如何工作的,让我们看看创建新分支的过程。假设我们从I
您刚刚提交的提交开始master
:
...--F--G--H--I <-- master
让我们创建一个名为的新分支feature/short
:
git checkout -b feature/short
我们现在的样子是这样的:
...--F--G--H--I <-- master, feature/short (HEAD)
也就是说,这两个名称-bothmaster
和feature/short
-identify existing commit I
。HEAD
Git 用来记住我们在哪个分支上的特殊名称附加在名称后面feature/short
。
现在我们将照常处理工作树,照常运行git add
,然后运行git commit
. Git 将收集我们的姓名和电子邮件以及时间、我们的日志消息等,并J
使用我们索引中的快照和 parent进行新的提交I
。然后它将J
' 的实际哈希 ID,无论是什么,写入 name feature/short
:
...--F--G--H--I <-- master
\
J <-- feature/short (HEAD)
开始的历史J
可以追溯到I
然后H
等等。新提交位于新分支的顶端,feature/short
. 我们的索引现在匹配我们的提交J
和我们的工作树,并且HEAD
仍然连接到我们的分支feature/short
。
你现在知道了关于分支的所有知识——好吧,除了孤儿分支,我们稍后会谈到。
添加工作树
如果您一直关注,您现在就会意识到“索引”不仅索引工作树,而且它和工作树都与特殊名称有密切的关系HEAD
。我们git checkout
习惯将我们的分支名称附加到某个分支名称上,在此过程中,我们用来自一个特定提交HEAD
的所有内容填充索引和工作树,该提交位于该分支的顶端——名称指向的提交。所有这些实体——<code>HEAD、索引、工作树和分支名称——同时发生变化。
要做git worktree add
的是创建一个新的三元组——一个新的 <HEAD, index, work-tree> 组——并git checkout
在该新组中运行。新的工作树必须位于计算机的不同区域:不同的文件夹,如果您喜欢术语文件夹。新添加的工作树位于不同的分支上。所有工作树必须在不同的分支上,即使这些分支名称标识相同的提交!每个工作树都有自己的索引 和HEAD
,如果你从一个工作树切换到另一个工作树,你必须改变你对你HEAD
和你的索引的想法。
每个提交中的文件都是冻干的:Git 化和压缩,没有用处。提取到工作树中的文件被重新水化并且有用。因此,添加更多工作树的能力意味着您可以同时进行不同的提交,只要它们位于不同的工作树中。
(作为一种特殊情况,任何工作树都可以有一个分离的 HEAD,您可以在其中通过哈希 ID 提取特定的提交。因此,如果您需要查看 16 个不同的历史提交,您可以添加 16 个工作树,每个工作树位于不同的分离例如,在那个历史性的提交上 HEAD。)
孤儿枝
现在我们已经解决了所有这些问题,我们可以——终于!——看看孤儿分支是什么。它比你想象的要少!
我们已经知道它HEAD
通常附加到一些现有的分支名称,并且现有的分支名称存储一个单一提交的哈希 ID,我们称之为该分支的尖端。当事情以这种方式设置时,进行新的提交会更新分支名称,因此现有的分支名称现在存储我们刚刚提交的新提交的新的、唯一的提交哈希 ID。
我们还顺便提到过,它HEAD
可以存储提交的哈希 ID——Git 称之为分离的 HEAD。这里HEAD
不附加分支名称,因此使用“分离”一词。索引和工作树在这里以通常的方式工作:索引保存来自 detached-HEAD 提交哈希 ID 的所有文件,以冻干形式但实际上不再冻结,工作树保存所有文件从那个提交。你也可以通过这种方式进行新的提交:如果你这样做了,Git 只会将新提交的哈希 ID 存储到 nameHEAD
中。没有分支名称会记住此哈希 ID。仅HEAD
保存该哈希 ID。这些提交很容易被错误地丢失!如果你git checkout
用来移动你的HEAD
,你已经丢失了你所做的新提交的哈希 ID——所以至少对分离的 HEAD 小心一点,以免你失去理智。:-)
不过,还有另一种模式,用于HEAD
. Git 允许您将您的附加HEAD
到一个不存在的分支名称。为此,您使用git checkout --orphan
:
git checkout --orphan feature/tall
这很像git checkout -b
. 但-b
首先创建分支名称,然后附加HEAD
到分支名称。这是在名称中存储哈希 ID 的分支名称的创建!当我们在feature/short
上面进行时,我们创建了指向现有提交的名称I
,即master
已经记住的相同提交。
当我们使用git checkout --orphan
时,Git不会创建分支名称。我们最终得到这样的图片:
...--F--G--H--I <-- master
\
J <-- feature/short
feature/tall (HEAD)
索引和工作树的内容保持不变,与以前完全相同,但名称 feature/tall
根本不作为分支名称存在。它只是HEAD
依附于它。由于它不作为分支名称存在,因此它不指向任何现有的提交。
如果我们现在提交,Git 会将索引的内容保存为新快照。如果我们没有更改任何内容,则这些内容与 commit 匹配J
。所以我们会得到一个新的 commit K
。新提交的父K
提交应该是我们现在签出的任何提交——由我们HEAD
所附加的分支名称标识的提交。但是那个分支不存在!
Git 在这里所做的就是对你在一个新的、完全空的、还没有提交的存储库中进行的第一次提交做同样的事情。Git 只是在没有父母的情况下进行提交。这样的提交称为根提交,我们可以这样绘制:
K
完成新的提交后,Git 现在更新我们HEAD
所附加的分支名称。那个名字是feature/tall
,所以现在我们有:
...--F--G--H--I <-- master
\
J <-- feature/short
K <-- feature/tall (HEAD)
新分支feature/tall
, 现在存在。它的出现是因为我们做了一个新的提交——和往常一样,从索引中——而且这个新的提交没有历史。
毕竟,历史只是提交链,从任何地方开始,然后向后工作。我们从头开始K
并向后工作——嗯,没有其他地方可去。所以我们开始K
并显示提交,我们就完成了。历史终结!那里没有别的东西。
现在,当然,如果我们从J
or开始并向后工作,那里就有I
历史。但这与我们开始和倒退的历史无关。孤儿分支也是如此。它只是一个与一切无关的分支。K
feature/tall
这个特殊的属性在一个新的、完全空的存储库中非常有用。这样的存储库没有提交也没有分支,我们所做的第一个提交——通过创建一些文件,将它们复制到我们最初为空的索引中,然后提交——应该是这个仍然是新的但现在不是的第一个也是唯一的提交-空存储库。如果我们HEAD
附加到分支名称master
——当然是——这将创建我们的第一个分支名称,master
指向第一个也是唯一的提交,我们可以调用A
它,但它有一个唯一的哈希 ID,它是内容的加密校验和我们创建的文件加上我们的名字加上我们的电子邮件地址加上我们输入的日志消息加上我们运行的时间git commit
,所有这些加起来使这个提交在宇宙中独一无二。
使用git checkout --orphan
设置类似的条件,除了索引和工作树可能不是空的。为这个孤立分支进行第一次提交是创建孤立分支的原因。与往常一样,进入的快照是运行时索引git commit
中的任何内容。日志消息是您输入的任何内容。新提交没有父提交,这就是 Git 将其称为孤儿的原因。
结论
如果你想要一个孤儿提交,这就是你得到它的方式。但根据定义,它没有历史,因为历史是父母的链条。如果你想要一个孤儿,你就没有历史;如果你想要历史,你可能不会使用孤儿。