我在不同的存储库中有几个项目,我想将它们合并到具有不同孤儿分支的同一存储库下。 为此,我创建了一个新的存储库并在其中启动它。
-
如何获取现有存储库,将其作为孤立分支导入并保留历史记录?
-
是否可以使用作为不同文件夹打开的 2 个孤立分支?假设我有 2 个孤立分支,我想并行处理这两个分支,可能吗?我有 2 个以上,我想使用打开的单个 git UI,这样开发起来会更有效率。今天我检查每个存储库。在同一个存储库下统一它后,我仍然需要并行处理所有分支。
orphan-branch-B
.gitignore
.README.md
and more
orphan-branch-B
.gitignore
.README.md
and more
TL;博士
我有 2 个以上的 [分支],我想使用打开的单个 git UI 工作......
这是否可能,如果是,如何,取决于 UI。 一般来说,询问 Git 不会让你得到答案。 命令行答案是使用git worktree
(非常需要 Git 2.15 或更高版本)。
长
- 如何获取现有存储库,将其作为孤立分支导入并保留历史记录?
你没有,真的。 这个操作和问题可能没有意义,因为我怀疑你的意思是Git所说的孤儿分支。 继续阅读到最后,以确定它是否有意义。
Git 存储库到底是什么?
Git存储库本质上由两个数据库组成。 一个数据库只保存 Git对象,其中最有趣的一个是称为提交的对象,每个提交都代表所有文件的完整快照。1另一个数据库包含名称- 分支名称(如master
)和标记名称(如v2.1
)以及这些名称是您(至少最初是 Git)找到有趣提交的方式。
每个提交(再次表示所有文件的快照;提交不包含更改)由其哈希 ID 唯一标识。 哈希 ID 是一个由字母和数字组成的大字符串,看起来是随机的,但实际上是提交全部内容的加密校验和:快照,以及告诉您谁制作快照(名称和电子邮件地址)、何时(时间戳)、原因(日志消息)等的元数据。 由于每次提交都存储其直接前身或父提交的实际哈希 ID,因此 Git 很容易从最后一次提交开始并向后工作:
... <-F <-G <-H <-- master
因此,像master
这样的分支名称仅包含分支中最后一次提交的哈希 ID。 历史记录本身只是从该提交开始(在本例中H
)并向后工作(一次一个提交)从提交到父级的提交链。
这里有一个轻微的障碍,因为链不一定是线性的。 在进行了像上面这样的一系列提交之后,我们可能还有第二个提交序列:
G--H <-- master
/
...--E--F
I--J <-- develop
在这里,F
及更早的提交位于两个分支上,而提交G-H
仅在master
上,I-J
仅在develop
上。 如果我们然后将J
合并到master
中,我们会得到一个稍微特殊的提交:
G--H
/
...--E--F K <-- master
/
I--J <-- develop
虽然提交K
像往常一样有一个简单的快照,但它现在有两个父级,而不是一个,使其成为合并提交。 要查看K
的历史,我们必须返回提交H
并同时J
两者。 从那里我们回到G
和I
;从那里我们回到F
,历史重新收敛,在合并时分道扬镳。
换句话说,Git 是向后工作的:历史在逻辑上在合并时收敛,并且由于 Git 向后工作,历史实际上在合并时发散。 历史在逻辑上在你剥离第二个分支的点上发散,但在 Git 中它实际上在那个点收敛,因为 Git 是向后工作的。
分支名称master
特别之处在于,它始终指向我们希望说的最后一个提交是在分支上。 这一点尤其重要,因为您询问的是孤立分支。
1其他三种对象类型是树(树保存文件名)、blob(每个blob都是文件的内容)和标记(如v2.1
)的注释标记。 Git 使用提交 + 树 + blob 组合来构造每个提交表示的快照。
Git 如何进行新提交:索引和工作树
- 是否可以使用作为不同文件夹打开的 2 个孤立分支?
如果你有 Git 2.5 或更高版本(由于 Git 2.5 中初始实现中的一些错误,2.15 或更高版本是个好主意),则可以使用git worktree
在两个不同的工作树中同时使用两个不同的分支。 现在是时候讨论 Git 的索引和工作树概念了,之后我们将讨论孤立分支的定义。
Git 提交快照中的所有内容都将永久冻结。 任何提交的任何部分(不是其日志消息、用户名、父哈希 ID 以及作为该提交的一部分存储的任何已保存文件的任何部分)都不能更改。任何现有提交(由某些现有哈希 ID 标识)都无法更改。它的所有文件都被及时冻结。 (它们也是压缩的,有时非常压缩。 如果你愿意,你可以把它们想象成冷冻干燥的。 这对于存档非常有用:您可以随时及时回到任何以前的提交。 但是对于完成任何新工作都是没有用的。
为了让你完成工作,Git 让你能够签出提交。 签出提交会做三件事:
第一个也是最明显的是,它有点"重新水化"一个冻干的提交,将其所有文件提取到某种工作区域,在那里它们具有正常的、非冻结的、非 Git 化的形式。 这个工作区,通常紧挨着仓库本身,是你的工作树(或工作树,有时是工作目录或这种拼写的某种变体)。
第二个,一旦你考虑一下,也很明显,如果你使用
git checkout master
或git checkout develop
或其他任何东西,它会记住你用来从该分支获取最新提交的分支名称。 或者,如果您使用git checkout <hash-id>
回到过去,它会记住哈希 ID。 无论哪种方式(通过分支名称或哈希 ID),它也会记住您已经完成的提交。git checkout
在这里做的第三件几乎不可见的事情是填写 Git 的索引。
称这个东西为索引是一个无用的名字——索引到底传达了什么?——所以它还有另外两个名字:它有时被称为暂存区,有时被称为缓存,这取决于 Git 的哪个人或哪个部分在做这个调用。 不过,这三个名字都是为了同一件事。 索引是什么和做什么在合并过程中变得有点复杂,但它的主要事情是它以 Git 化的形式保存提交中的所有文件,准备冻结,但与真正的提交不同 -实际上并没有冻结。
这意味着索引包含将进入下一次提交的所有文件。 换句话说,这是一种提议的下一次提交。 您从以下方面开始:
git checkout master
对于提交中由名称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
其他分支,则将下一个提交提案替换为与您刚刚签出的提交匹配的其他提案。 (此规则有一些例外,这是故意的;请参阅当当前分支上有未提交的更改时签出另一个分支。 反过来,这意味着索引和工作树实际上是一对:索引索引工作树。 当您通过更改一些文件来更改工作树时,您需要通过git add
这些文件来更新索引。
当你运行git commit
时,Git 所做的是这样的:
- 保存您的姓名和电子邮件地址;
- 保存当前时间(新提交的时间戳);
- 从您那里收集日志消息,以进入新的提交;
- 使用当前提交的哈希 ID 作为父哈希 ID;
- 将所有这些以及索引中的 Git 化文件保存到一个新的提交中,该提交会自动获得新的哈希唯一哈希 ID(通过计算所有这些数据的加密校验和)
- 将新提交的哈希 ID 写入当前分支
也就是说,如果您有:
...--F--G--H <-- master
您现在拥有:
...--F--G--H--I <-- master
名称master
现在记录您刚刚进行的新提交的哈希 IDI
。 该新提交的父级是提交H
的哈希ID,这是您在进行此新提交之前已签出的哈希ID。
历史就是这样形成的! 当你运行git commit
时,Git 刚刚从索引中的任何内容进行一个新的提交,创建我们的新提交I
。 新提交的父级是你让 Git 签出的提交。 因为 Git 从索引、索引和新匹配项进行了提交,就像您第一次运行git checkout master
以获取提交H
时所做的那样。 现在一切看起来都很好,您可以修改工作树中的内容,使用git add
将其复制回索引,然后运行git commit
创建一个新J
,其父级是I
的,其保存的快照来自索引。
创建新分支
现在您已经了解了现有分支的工作原理,让我们看一下创建新分支的过程。假设我们从您刚刚在master
上所做的提交I
开始:
...--F--G--H--I <-- master
让我们创建一个名为feature/short
的新分支:
git checkout -b feature/short
我们现在拥有的看起来像这样:
...--F--G--H--I <-- master, feature/short (HEAD)
也就是说,两个名称(master
和feature/short
)都标识现有的提交I
。 特殊名称HEAD
,Git 用来记住我们在哪个分支上,附加到名称feature/short
上。
现在我们将像往常一样弄乱工作树,像往常一样运行git add
,然后运行git commit
。 Git 将收集我们的姓名、电子邮件、时间、日志消息等,并使用索引中的快照和父I
进行新的提交J
。 然后它会将J
的实际哈希 ID(无论是什么)写入名称feature/short
中:
...--F--G--H--I <-- master
J <-- feature/short (HEAD)
从J
开始的历史可以追溯到I
,然后是H
,依此类推。 新提交位于新分支的顶端,feature/short
. 我们的索引现在与我们的提交J
和我们的工作树匹配,并且HEAD
仍然附加到我们的分支feature/short
。
您现在知道了有关分支的所有信息,好吧,除了孤立分支,我们稍后会谈到。
添加工作树
如果您一直密切关注,您现在就会意识到,"索引"不仅索引工作树,它和工作树也与特殊名称HEAD
有着密切的关系。 我们使用git checkout
将我们的HEAD
附加到某个分支名称,在这个过程中,我们用一个特定提交的所有内容填充我们的索引和工作树,该分支顶端的提交- 名称指向的提交。 所有这些实体(HEAD
、索引、工作树和分支名称)同时更改。
git worktree add
所做的是创建一个新的三元组 - 一个新的<HEAD,索引,工作树>组 - 并在该新组中运行git checkout
。 新的工作树必须位于计算机的不同区域:不同的文件夹(如果您喜欢术语文件夹)。 新添加的工作树位于不同的分支上。 所有工作树必须位于不同的分支上,即使这些分支名称标识相同的提交! 每个工作树都有自己的索引和HEAD
,如果你从一个工作树切换到另一个工作树,你必须改变你对HEAD
和索引的想法。
每个提交中的文件都是冻结干燥的:Git 化和压缩,没有用。 提取到工作树中的文件已解除冻结且有用。 因此,添加更多工作树的能力意味着您可以同时进行不同的提交,只要它们在不同的工作树中即可。
(作为特例,任何工作树都可以有一个分离的 HEAD,您可以在其中通过哈希 ID 提取特定的提交。 因此,如果您需要查看 16 个不同的历史提交,您可以添加 16 个工作树,例如,每个工作树位于该历史提交的不同分离 HEAD 上。
孤立分支
现在我们已经解决了所有这些问题,我们终于可以看看什么是孤立分支了。 这比你想象的要少!
我们已经知道HEAD
通常附加到一些现有的分支名称,现有的分支名称存储单个提交的哈希ID,我们称之为该分支的尖端。 当以这种方式设置时,进行新的提交会更新分支名称,以便现有分支名称现在存储我们刚刚进行的新提交的新的唯一提交哈希 ID。
我们还顺便提到,HEAD
可以存储提交的哈希 ID——Git 称之为分离的 HEAD。这里HEAD
不附属于分支名称,因此有"分离"一词。 索引和工作树在这里以通常的方式工作:索引保存来自分离的 HEAD 提交哈希 ID 的所有文件,以冻结干燥的形式,但实际上不再冻结,工作树保存该提交中的所有文件。 您也可以通过这种方式进行新的提交:如果您这样做,Git 只需将新提交的哈希 ID 存储到名称HEAD
中。 没有分支名称会记住此哈希 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 会将索引的内容保存为新快照。 如果我们没有更改任何内容,则这些内容与提交J
匹配。 因此,我们将获得一个新的提交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
或I
开始,然后倒推,那里就有历史。但它与我们从K
开始并倒退的历史无关。 所以feature/tall
是一个孤儿分支。 它只是一个与一切无关的分支。
这个特殊的属性在一个新的、完全空的存储库中非常有用。 这样的存储库没有提交,也没有分支,我们所做的第一次提交 - 通过创建一些文件,将它们复制到我们最初为空的索引中,然后提交 - 应该是这个仍然新的但现在不是空的存储库中的第一个也是唯一一个提交。 如果我们的HEAD
附加到分支名称master
(当然是这样),这将创建我们的第一个分支名称master
,指向第一个也是唯一一个提交,我们可以调用它A
但它有一个唯一的哈希 ID,它是我们创建的文件内容的加密校验和加上我们的名字加上我们的电子邮件地址加上我们输入的日志消息加上我们运行了git commit
,所有这些加起来使这个提交在宇宙中独一无二。
使用git checkout --orphan
设置类似的条件,只是索引和工作树可能不为空。 为此孤立分支进行第一次提交是创建孤立分支的原因。 与往常一样,传入的快照是运行git commit
时索引中的任何内容。 日志消息是您输入的任何内容。 新提交没有父提交,这就是为什么 Git 称其为孤立提交。
结论
如果你想要一个孤儿提交,这就是你得到它的方式。 但根据定义,它没有历史,因为历史是父母的链条。 如果你想要一个孤儿,你就没有历史;如果你想要历史,你不能使用孤儿。
您正在寻找"git worktree"...
-
(可选)创建一个裸存储库;例如
mkdir .repo/ git clone --bare .../project .repo/project.git
-
从此存储库中创建工作树
git -C .repo/project.git worktree add `pwd`/project-A branch-A git -C .repo/project.git worktree add `pwd`/project-B branch-B
您可以跳过步骤 1 并从现有的非裸存储库创建工作树,但恕我直言,它简化了分支项目长期存在的操作。
您可以删除所有内容,提交它,然后从提示开始。或者挑选一根孤立的树枝。
git rm -rf .
git commit -m 'remove everything'
git worktree: 但是使用现有源代码中的孤立分支呢? 您能否在命令上提供更多信息?
这尚不受支持,但正在积极讨论/实施中
将孤立分支功能(如
git checkout
中所示)添加到git-worktree add
添加对添加新工作树时创建孤立分支的支持。 此功能等效于 git 结帐的
--orphan
标志。实现此功能的最初原因是允许用户 仅使用面向工作树来初始化新存储库 工作流。
下面包括的示例用法。
$ GIT_DIR=".git" git init --bare $ git worktree add --orphan master master/
它列在最新的"git.git 中的烹饪"中,但尚未正式合并到master
中。