如何在Git中拥有多个工作目录?

289

我不确定Git是否支持这样的操作,但理论上看起来应该可行。

我的工作流程经常涉及同时在多个分支中编辑文件。换句话说,我经常想要在一个分支中打开几个文件,并在另一个分支中编辑另一个文件的内容。

通常我会通过两次检出来解决这个问题,但是我无法共享这些分支和引用。我希望能够有两个由同一个.git文件夹管理的工作目录。

我知道有本地git clone的解决方案(默认情况下,使用硬链接共享对象和使用--shared选项设置备用对象存储区以及原始仓库),但这些解决方案只减少了磁盘空间的使用,特别是--shared的情况似乎存在一些风险。

是否有一种方法可以使用一个.git文件夹,并拥有由它支持的两个工作目录呢?或者Git是否硬编码为每次只能检出一个工作目录?


3
在Git 2.5中,git-new-workdir将会被git checkout --to=<path>替代。请参见下面的答案:https://dev59.com/SW025IYBdhLWcg3wAg_p#30185564 - VonC
5
实际上,命令将是 git worktree add <路径> [<分支>](Git 2.5 rc2)。请参见我下面编辑的答案 - VonC
你应该更改已接受的答案VonC的答案,因为自你最初提出问题以来情况已经发生了变化。 - xaxxon
感谢您更新的答案! - jtolds
我认为你可以引用自己的这个问题作为答案,回答这个问题。这样可以很好地说明使用情况。 - Guildenstern
4个回答

347

自2015年7月起,Git 2.5提出了一个替代contrib/workdir/git-new-workdir的方案:git worktree

参见提交记录68a2e6a,作者是Junio C Hamano(gitster

发布说明提到

替代contrib/workdir/git-new-workdir的方案不再依赖符号链接,使对象和引用的共享更加安全,通过让借用者和被借用者相互感知来实现。

查看 提交 799767cc9(Git 2.5rc2)

这意味着您现在可以 执行 git worktree add <path> [<branch>] 命令

创建一个名为 <path> 的目录,并把工作空间切换到其中。新的工作目录与当前仓库链接,共享除了 HEAD、index 等工作目录特定文件以外的一切内容。git worktree 部分补充说明: 一个 Git 仓库可以支持多个工作树,允许您同时检出多个分支。通过 git worktree add 命令,可以将新的工作树与该仓库关联。 这个新的工作树称为“链接的工作树”,与由 "git init" 或 "git clone" 准备的“主工作树”相对应。一个仓库有一个主工作树(如果它不是裸仓库),以及零个或多个链接的工作树。
每个链接的工作树都在存储库的$GIT_DIR/worktrees目录下拥有一个私有子目录。
私有子目录的名称通常是链接的工作树路径的基本名称,可能附加一个数字以使其唯一。
例如,当$GIT_DIR=/path/main/.git时,命令git worktree add /path/other/test-next next创建了:
  • 链接的工作树在/path/other/test-next
  • 还创建了一个$GIT_DIR/worktrees/test-next目录(如果test-next已被占用,则为$GIT_DIR/worktrees/test-next1)。
在链接的工作树内部:
  • $GIT_DIR设置为指向此私有目录(例如,在示例中为/path/main/.git/worktrees/test-next
  • $GIT_COMMON_DIR设置为指回主工作树的$GIT_DIR(例如,/path/main/.git)。
这些设置在链接的工作树顶级目录中的.git文件中进行。
完成链接的工作树后,可以将其简单地删除。
存储库中的工作树管理文件最终会自动删除(请参见git config中的gc.pruneworktreesexpire),或者您可以在主工作树或任何链接的工作树中运行git worktree prune以清除任何过时的管理文件。

警告:仍然需要注意git worktree "BUGS" 部分。

子模块的支持不完整
不建议对超级项目进行多次检出。



注意:从 git 2.7rc1(2015 年 11 月)开始,您可以列出您的工作树。
请参见 提交 bb9c03b提交 92718b7提交 5193490提交 1ceb7f9提交 1ceb7f9提交 5193490提交 1ceb7f9提交 1ceb7f9 (2015 年 10 月 8 日),提交 92718b7提交 5193490提交 1ceb7f9提交 1ceb7f9 (2015 年 10 月 8 日),提交 5193490提交 1ceb7f9 (2015 年 10 月 8 日),提交 1ceb7f9 (2015 年 10 月 8 日) 和 提交 ac6c561(2015 年 10 月 2 日),由 Michael Rappazzo (rappazzo) 提交。
(由 Junio C Hamano -- gitster -- 合并于 提交 a46dcfb,2015 年 10 月 26 日)

worktree: 添加 'list' 命令

'git worktree list' 遍历工作树列表,并输出工作树的详细信息,包括工作树路径、当前检出的修订版本和分支,以及工作树是否为裸库。

$ git worktree list /path/to/bare-source (bare) /path/to/linked-worktree abcd1234 [master] /path/to/other-linked-worktree 1234abc (detached HEAD)

此外,还提供了瓷砖格式选项。

瓷砖格式每个属性一行。

  • 属性用标签和值分隔的单个空格列出。
  • 布尔属性(如“bare”和“detached”)仅列出标签,并且仅在值为true时存在。
  • 空行表示工作树的结束

例如:

$ git worktree list --porcelain

worktree /path/to/bare-source
bare

worktree /path/to/linked-worktree
HEAD abcd1234abcd1234abcd1234abcd1234abcd1234
branch refs/heads/master

worktree /path/to/other-linked-worktree
HEAD 1234abc1234abc1234abc1234abc1234abc1234a
detached

注意:如果您移动了工作树文件夹,则需要手动更新gitdir文件。

请参阅提交 618244e(2016年1月22日)和提交 d4cddd6(2016年1月18日),作者为Nguyễn Thái Ngọc Duy(pclouds
协助者为:Eric Sunshine(sunshineco
(由Junio C Hamano -- gitster --提交 d0a1cbc合并,日期为2016年2月10日)

git 2.8(2016年3月)的新文档将包括以下内容:

如果您移动了链接的工作树,您需要更新条目目录中的'gitdir'文件。
例如,如果链接的工作树被移动到/newpath/test-next并且它的 .git 文件指向/path/main/.git/worktrees/test-next,那么请更新 /path/main/.git/worktrees/test-next/gitdir,将其引用改为/newpath/test-next


删除分支时要小心:在git 2.9之前(即2016年6月),您可以删除另一个工作树中正在使用的分支。

当使用“git worktree”功能时,“git branch -d”允许删除在另一个工作树中检出的分支。

请参见提交 f292244 (2016年3月29日) 由Kazuki Yamaguchi (rhenium)完成。
协助者:Eric Sunshine (sunshineco)
(由Junio C Hamano -- gitster --合并于提交 4fca4e3,2016年4月13日)

branch -d: 拒绝删除当前正在使用的分支

当一个分支被当前工作树检出时,删除该分支是被禁止的。但是,当该分支仅被其他工作树检出时,错误地删除将会成功。使用find_shared_symref()来检查分支是否正在使用,而不仅仅是与当前工作树的HEAD进行比较。
同样地,在git 2.9之前(2016年6月),重命名在另一个工作树中检出的分支没有调整该其他工作树中的符号HEAD。

请参阅提交 18eb3a9(2016年4月8日),提交 70999e9提交 2233066(2016年3月27日),由Kazuki Yamaguchi (rhenium)撰写的内容。
(由Junio C Hamano -- gitster --提交 741a694中合并,2016年4月18日)

branch -m: 更新所有工作树的HEAD

When renaming a branch, currently only the HEAD of current working tree is updated, but it must update HEADs of all working trees which point at the old branch.

This is the current behavior, /path/to/wt's HEAD is not updated:

 % git worktree list
 /path/to     2c3c5f2 [master]
 /path/to/wt  2c3c5f2 [oldname]
 % git branch -m master master2
 % git worktree list
 /path/to     2c3c5f2 [master2]
 /path/to/wt  2c3c5f2 [oldname]
 % git branch -m oldname newname
 % git worktree list
 /path/to     2c3c5f2 [master2]
 /path/to/wt  0000000 [oldname]

This patch fixes this issue by updating all relevant worktree HEADs when renaming a branch.


锁定机制在官方支持的git 2.10版本中(2016年Q3)推出。

请查看 提交 080739b, 提交 6d30862, 提交 58142c0, 提交 346ef53, 提交 346ef53, 提交 58142c0, 提交 346ef53, 提交 346ef53 (2016年6月13日), 和 提交 984ad9e, 提交 6835314 (2016年6月3日) 由 Nguyễn Thái Ngọc Duy (pclouds) 提出。
建议者为: Eric Sunshine (sunshineco)
(由Junio C Hamano -- gitster --提交 2c608e0合并, 2016年7月28日)

git worktree lock [--reason <string>] <worktree>
git worktree unlock <worktree>

如果一个链接的工作树存储在便携设备或网络共享上,而且该设备不总是被挂载,您可以通过发出“git worktree lock”命令来防止其管理文件被修剪,并可选择指定“--reason”来解释为什么要锁定工作树。 :如果工作树路径中的最后一部分在工作树之间是唯一的,那么它可以用来标识工作树。例如,如果您只在“/abc/def/ghi”和“/abc/def/ggg”两个地方有两个工作树,则“ghi”或“def/ghi”足以指向前者的工作树。

Git 2.13 (Q2 2017)在提交507e6e9(2017年4月12日)中添加了一个lock选项,作者是Nguyễn Thái Ngọc Duy(pclouds。建议者为David Taylor(dt,帮助者为Jeff King(peff(由Junio C Hamano -- gitster --提交e311597合并,2017年4月26日) 允许在创建工作树后立即锁定它。这有助于防止“git worktree add; git worktree lock”和“git worktree prune”之间的竞争。git worktree add --lock相当于在git worktree add之后执行git worktree lock,但没有竞态条件。
Git 2.17+(2018年第二季度)增加了git worktree move / git worktree remove功能:查看此答案
Git 2.19(2018年第三季度)增加了一个“--quiet”选项,使“git worktree add”更加简洁。

请看提交 371979c (2018年8月15日) ,作者是Elia Pinto (devzero2000)
协助者:Martin Ågren martin.agren@gmail.com,Duy Nguyen (pclouds), 和 Eric Sunshine (sunshineco)
(由Junio C Hamano -- gitster --提交 a988ce9中合并,2018年8月27日)

worktree: 添加--quiet选项

git worktree添加'--quiet'选项,与其他git命令一样。
只有'add'命令受其影响,因为除了'list'之外的所有其他命令当前默认都是静默的。


请注意,"git worktree add" 以前是使用 "查找可用名称并使用 stat 和 mkdir" 的方式,这种方式容易出现竞争问题。
从 Git 2.22(2019 年第二季度)开始,通过使用 mkdir 并在循环中处理 EEXIST 来解决此问题。
请参见 提交 7af01f2(2019 年 2 月 20 日),作者为 Michal Suchanek (hramrach)
(由 Junio C Hamano -- gitster --提交 20fe798 中合并)

worktree:修复worktree add竞争问题

Git运行stat循环来查找可用的工作树名称,然后在找到的名称上执行mkdir
将其转换为mkdir循环以避免另一个worktree add调用查找相同的空闲名称并首先创建目录。
Git 2.22(2019年第二季度)修复了逻辑,以判断Git存储库是否具有工作树,并保护"git branch -D"以防止错误地删除当前检出的分支。实施此逻辑时,对于具有不寻常名称的存储库存在问题,而这种情况恰恰是子模块的常态。请参见提交f3534c9(2019年4月19日),作者为Jonathan Tan (jhowtan)(由Junio C Hamano -- gitster --提交ec2642a中合并,2019年5月8日)

worktree: 更新 is_bare 启发式算法

当运行“git branch -D <name>”时,Git通常会首先检查该分支当前是否被检出。
但是,如果该存储库的Git目录不在“<repo>/.git”中,则不执行此检查,例如,如果该存储库是作为子模块存在且其Git目录存储为“super/.git/modules/<repo>”。
这将导致即使已检出该分支也被删除。

这是因为worktree.c中的get_main_worktree()仅使用启发式方法设置工作树上的is_bare,即如果工作树路径不以“/.git”结尾,则认为存储库是裸的,否则认为不是裸的。
is_bare代码是在92718b7(“worktree:add details to the worktree struct”,2015-10-08,Git v2.7.0-rc0)中引入的,遵循pre-core.bare启发式方法。

此补丁执行两个操作:

  • get_main_worktree()使用is_bare_repository()代替,该函数在7d1864c(“Introduce is_bare_repository() and core.bare configuration variable”,2007-01-07,Git v1.5.0-rc1)中引入并在e90fdc3(“Clean up work-tree handling”,2007-08-01,Git v1.5.3-rc4)中进行了更新。
    这解决了上述描述的“git branch -D <name>”问题。
    但是...
  • 如果存储库具有core.bare=1,但从其中一个辅助工作树运行“git”命令,则is_bare_repository()返回false(这是可以的,因为有可用的工作树)。
    而且,当将主工作树视为非裸时,会出现问题:例如,无法从由主工作树的HEAD引用的辅助工作树中删除分支,即使该主工作树是裸的。

为了避免这种情况,在设置is_bare时还要检查core.bare
如果core.bare=1,则信任它;否则,使用is_bare_repository()


在 Git 2.29 (2020年第四季度) 中,"worktree" API 提供了更好的工作树路径确定。

请查看 提交 918d8ff, 提交 1c4854e, 提交 246756f, 提交 62573a5 (2020年7月31日) 由Eric Sunshine (sunshineco)提交。
(由Junio C Hamano -- gitster --提交 197253e中合并,2020年8月10日)

worktree:删除虚假和不必要的路径修改

签名作者:Eric Sunshine

The content of .git/worktrees/<id>/gitdir must be a path of the form "/path/to/worktree/.git".
Any other content would be indicative of a corrupt "gitdir" file.

To determine the path of the worktree itself one merely strips the "/.git" suffix, and this is indeed how the worktree path was determined from inception.

However, 5193490442 ("worktree: add a function to get worktree details", 2015-10-08, Git v2.7.0-rc0 -- merge listed in batch #7) extended the path manipulation in a mysterious way.
If it is unable to strip "/.git" from the path, then it instead reports the current working directory as the linked worktree's path:

if (!strbuf_strip_suffix(&worktree_path, "/.git")) {
    strbuf_reset(&worktree_path);
    strbuf_add_absolute_path(&worktree_path, ".");
    strbuf_strip_suffix(&worktree_path, "/.");
}  

This logic is clearly bogus; it can never be generally correct behavior. It materialized out of thin air in 5193490442 with neither explanation nor tests to illustrate a case in which it would be desirable.

It's possible that this logic was introduced to somehow deal with a corrupt "gitdir" file, so that it returns some sort of meaningful value, but returning the current working directory is not helpful. In fact, it is quite misleading (except in the one specific case when the current directory is the worktree whose "gitdir" entry is corrupt).
Moreover, reporting the corrupt value to the user, rather than fibbing about it and hiding it outright, is more helpful since it may aid in diagnosing the problem.

Therefore, drop this bogus path munging and restore the logic to the original behavior of merely stripping "/.git".


2
这是他们创建的最酷的东西,正是我在寻找的。谢谢你! - user4713908
1
如何仅删除工作树并保留分支。 - Randeep Singh
@DotnetRocks,您可以删除任何工作树(计算机上的本地文件夹):这不会对分支产生任何影响:主要的.git存储库仍将包括完整的提交历史记录,以及其所有分支,无论工作树是否已被删除。 - VonC
哦,我明白了,即使没有运行git worktree prune,它最终也会删除链接!谢谢。 - Randeep Singh
1
@Jayan 谢谢。我已经更清楚地表明2.5和2.7现在已经发布了。 - VonC
显示剩余6条评论

113

git发布版中自带一个名为git-new-workdir贡献脚本。 你可以按照以下方式使用它:

git-new-workdir project-dir new-workdir branch

其中 project-dir 是包含您的 .git 存储库的目录名称。 此脚本创建另一个 .git 目录,并使用许多符号链接连接到原始目录,但不包括无法共享的文件(如当前分支),允许您在两个不同的分支中工作。

听起来有点脆弱,但这是一种选择。


3
我改正自己,这真的很棒。它似乎可以立即在两个不同的检出存储库之间共享历史记录和分支,而无需推拉,只需符号链接即可。我完全不知道 Git 可以处理这个功能。但遗憾的是,它没有包含在我的发行版中。 - user229044
2
对于使用 msysgit(Windows)的用户,您可以使用此脚本的移植版本:https://github.com/joero74/git-new-workdir - amos
9
通常情况下它运作良好,但如果不小心在不同的地方编辑了同一分支,将其修复回来就不那么简单了。 - Maxim Razin
对于那些卡在 Git < 2.5 并且有子模块的人,请尝试 git-new-workdir-recursive,它是 git-new-workdir 的包装器。 - Walf

14
我在寻找解决方案时遇到了这个问题,但是没有找到答案。现在我已经找到了所需的内容,并决定在这里发布以供他人参考。
注意:如果您需要同时编辑多个分支(如原帖所述),那么这可能不是一个好的解决方案。它适用于同时检出多个分支,而您不打算进行编辑的情况。(由一个.git文件夹支持的多个工作目录。)
自我第一次遇到这个问题以来,我学到了一些东西:
  1. "裸仓库"是什么。它基本上是.git目录的内容,而不位于工作树中。

  2. 您可以使用git选项--git-dir=在命令行上指定您正在使用的存储库(即您的.git目录)的位置。

  3. 您可以使用--work-tree=指定您的工作副本的位置。

  4. "镜像存储库"是什么。

这最后一个是非常重要的区别。实际上,我并不想在存储库上进行工作,我只需要同时拥有不同分支和/或标签的副本。事实上,我需要保证这些分支与我的远程分支不会不同。所以镜像对我来说非常完美。

因此,针对我的用例,我通过执行以下操作得到了我需要的内容:

git clone --mirror <remoteurl> <localgitdir> # Where localgitdir doesn't exist yet
mkdir firstcopy
mkdir secondcopy
git --git-dir=<localgitdir> --work-tree=firstcopy checkout -f branch1
git --git-dir=<localgitdir> --work-tree=secondcopy checkout -f branch2

这里有一个需要注意的地方,就是这两个副本没有单独的HEAD。所以在执行上述操作后,运行git --git-dir=<localgitdir> --work-tree=firstcopy status会显示从branch2到branch1的所有差异作为未提交的更改 - 因为HEAD指向branch2。(这就是我使用-f选项来检出的原因,因为我实际上并不打算在本地进行任何更改。只要我使用-f选项,就可以为任何工作树检出任何标签或分支。)
对于我的用例,在同一台计算机上存在多个副本而不需要编辑它们,这个方法完美地解决了问题。我不知道是否有任何方法可以在多个工作树中拥有多个HEAD,而不需要像其他答案中涉及的脚本,但我希望这对其他人有所帮助。

这正是我在寻找的,但我无法使其工作... 我收到了“fatal: Not a git repository: '<localgitdir>'”错误信息。有任何想法吗? - Dan R
算了,原来我在目录名中使用了“~”,而git不喜欢那个符号。当我使用完整路径时,它就正常工作了。 - Dan R
@DanR,很高兴能帮到你。 :) 你也可以使用 $HOME。上述方法还有一个小注意点,涉及到在某个分支中不存在的文件。如果你将A检出到dir1,然后将B检出到dir2,然后强制将C检出到dir1,如果一个文件存在于A中但不存在于B或C中,则该文件不会被强制检出以删除dir1中的该文件。因此,在这种情况下,您可能需要尝试使用 git clean 或像我所做的那样,只使用此方法来填充新创建的目录。 - Wildcard
谢谢你的建议。我打算将它作为 ruby 中 CLI 工具的一部分来打包,因此我会确保它始终从头开始。 - Dan R
@DanR,你可能会对查看我当时正在编写的代码感兴趣。其中大部分适用于任何git暂存脚本,除了CFEngine语法检查之外。(也许你可以将其替换为Ruby语法检查。):) - Wildcard

3
我能想到的唯一解决方案是克隆两个目录,并将它们添加为彼此的远程存储库。然后,您可以从更改的一个中不断拉出内容到另一个中,而实际上不需要将任何内容推送到远程存储库。
我假设您想要两个工作目录而不是远程克隆,因为您不想将某些分支推送到远程。否则,两个远程克隆将正常工作 - 您只需要进行一些推送和拉取以保持所有三个同步。

嗨。上面的人分享了一个很酷的解决方案,即 git worktree 命令。它比多次克隆同一存储库更好用。试试它,你会喜欢这个新功能的。 - user4713908

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