git 裸存储库、工作树和跟踪分支

git bare repositories, worktrees and tracking branches

我正在处理一个代码库,我需要同时在几个分支上工作以用于不同的目的。所以我克隆到一个裸存储库,然后设置一些工作树:

git clone --bare ssh://git@git.example.com/project/repo repo.git
cd repo.git
git worktree add ../branch-1 branch-1
git worktree add ../branch-2 branch-2
... someone else creates branch-3 and pushes is ...
git fetch origin +refs/heads/*:refs/heads/* --prune
git worktree add ../branch-3 branch-3

现在 branch-3 工作树未设置为跟踪远程树并试图让它这样做,我陷入了可怕的混乱。

$ cd ../branch-3
$ git branch -u origin/branch-3
error: the requested upstream branch 'origin/refs/heads/feature/SW-5884-move-database-container-to-alpine-base-2' does not exist
hint: ...<snip>
$ git fetch +refs/heads/*:refs/remotes/origin/* --prune
$ git branch -u origin/branch-3
fatal: Cannot setup tracking information; starting point 'origin/feature/SW-5884-move-database-container-to-alpine-base-2' is not a branch.

使它起作用的正确魔法是什么?

首先,附注:如果您打算在非试用期(一次超过两周)使用 git worktree add,请确保您的 Git 至少是 2.15 版。1

出于您的特定目的,我建议不要 使用git clone --bare。相反,使用常规克隆,然后是您打算执行的 git worktree adds。您在 中注意到:

... you end up having to create a dummy branch to sit the repository itself on, because it's not possible to have both the repository and a worktree on the same branch at the same time.

有几种简单的解决方法:

  • 选择您要添加的 N 个工作树之一,并将其​​用作主工作树中的 b运行ch:

    git checkout -b branch-1 ssh://git@git.example.com/project/repo branch-1
    

    缺点是您现在有一个特殊的 "main" b运行ch,您不能随时将其删除 - 所有其他 b运行ch 都依赖于它。

  • 或者,在克隆之后,在工作树中使用 git checkout --detach,在默认 b运行ch:

    上获得分离的 HEAD
    git clone ssh://git@git.example.com/project/repo repo.git
    cd repo.git
    git checkout --detach
    
  • 第二种方法的唯一缺点是工作树充满了文件,可能是 space 的浪费。还有一个解决方案:使用空树创建一个空白提交,然后检查它:

    git clone ssh://git@git.example.com/project/repo repo.git
    cd repo.git
    git checkout $(git commit-tree $(git hash-object -t tree /dev/null) < /dev/null)
    

好的,最后一个并不完全显而易见。不过这真的很简单。 git hash-object -t tree /dev/null 生成每个存储库中已存在的 empty tree 的哈希 ID。 git commit-tree 提交来包装那棵空树——没有一个,所以我们必须创建一个来检查它——并打印出这个新提交的哈希 ID,然后 git checkout 检查作为一个独立的 HEAD。效果是清空我们的索引和工作树,这样存储库工作树中唯一的东西就是 .git 目录。我们所做的空提交不在 b运行ch 上,也没有我们永远不会推送到任何地方的父提交(这是一个单独的根提交)。


1原因是Git2.5,git worktree最先出现的地方,有一个我认为很严重的bug:git gc 从不扫描 added 工作树的 HEAD 文件,也不会扫描它们的索引文件。如果添加的工作树总是在一些 b运行ch 上并且从来没有任何 git added 但未提交的工作,这永远不会导致任何问题。如果未提交的工作至少保留 14 天,默认的 p运行e 保护时间足以防止它被破坏。但是,如果你 git add 一些工作或在分离的 HEAD 上提交,请去度假一个月或以其他方式让这个添加的工作树不受干扰,然后再回来, a git gc --auto 运行 在两周的宽限期 运行 之后,您保存的文件已被销毁!

此错误已在 Git 2.15 中修复。


为什么--bare出错

这里问题的根源在于 git clone --bare 做了 两个 事情:

  • 它创建了一个没有工作树且没有初始 git checkout 的裸存储库(core.bare 设置为 true);
  • 它从 +refs/heads/*:refs/remotes/<em>origin</em>/*[ 更改了默认的 <code>fetch refspec =123=] 到 +refs/heads/*:refs/heads/*.

第二项意味着没有 refs/remotes/origin/ 名称,正如您发现的那样。由于 refmaps 的(主要是隐藏/内部)概念,这不容易修复,它在 git fetch 文档中非常简短地出现(参见 link)。

更糟糕的是,这意味着 refs/heads/* 将在每个 git fetch 上更新。 git worktree add 拒绝创建第二个工作树是有原因的,该工作树引用在任何现有工作树中检出的 same b运行ch,那就是 Git 从根本上假设没有人会弄乱 refs/heads/<em>name</em> 引用 HEAD 附加在 this 工作树中。因此,即使您确实解决了 refmap 问题,当您 运行 git fetch 并且它更新或什至删除时,由于 --prune 和上游删除了相同的名称 - refs/heads/<em>name</em> 名称,添加的工作树的 HEAD 中断,工作树本身也有问题。 (参见

还有一件事你可以尝试,我根本没有测试过,那就是:像你现在做的那样做裸克隆,然后 改变 refspec 和重新获取并删除所有现有的 b运行ch 名称,因为它们没有设置上游(或者,等效地,设置它们的上游):

git clone --bare ssh://git@git.example.com/project/repo repo.git
cd repo.git
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git fetch
git for-each-ref --format='%(refname:short)' refs/heads | xargs git branch -d

(或将 xargs 替换为 xargs -n1 -I{} git branch --set-upstream-to=origin/{} {})。

我为此苦苦挣扎了很长时间,从来不记得我为创建一个不像一个的裸仓库所采取的步骤。最终,我编写了以下名为 git-clone-bare-for-worktrees 的脚本来创建用于工作树的裸存储库。它似乎工作得很好,但要注意它没有做很多错误处理。

#! /bin/env bash
set -e

url=
name=${url##*/}

git init --bare "${name}"
cd "${name}"
git config remote.origin.url "$url"
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git fetch

firstCommit=$(git rev-list --all --max-parents=0 --date-order --reverse | head -n1)
git branch bare-dummy $firstCommit
git symbolic-ref HEAD refs/heads/bare-dummy

它创建了一个名为 bare-dummy 的分支,指向 repo 中的第一个提交并将其设置为 HEAD,确保所有“真实”分支都可以在工作树中安全地签出。除此之外,repo 将不包含任何本地分支,甚至 master 也不包含,但远程跟踪分支将完全像正常的非裸克隆一样创建。所以 运行 快速 git worktree add ../master-worktree master 你应该起床 运行 宁。

我认为有一个非常简单的方法;

1.克隆 repo 而不结帐(不浪费 space)

git clone --no-checkout ssh://git@git.example.com/project/repo repo.git

2。去回购

cd repo.git

3。创建一个虚拟分支,以便可以在每个现有分支上创建一个工作树

git switch -c dummy

4.现在根据需要创建工作树

git worktree add branch-1

git worktree add pathtobr1 branch-1

就是这样。干净、简单又不浪费space

希望这对您有所帮助 ;-)

另一个解决方案是在克隆时使用 --separate-git-dir 参数:

$ mkdir project && cd project

$ git clone http://****/wt.git main --separate-git-dir=.git
Cloning into 'main'...
remote: Enumerating objects: 6, done.
remote: Counting objects: 100% (6/6), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (6/6), done.

$ git worktree add wt1 b1
Preparing worktree (new branch 'b1')
branch 'b1' set up to track 'origin/b1'.
HEAD is now at 23b2efc Added file

$ ls -a
.  ..  .git  main  wt1

此技术的主要缺点是根 project 目录的行为类似于 out-of-sync 工作树,如果您尝试在那里执行 运行 git 命令:

project$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        deleted:    README.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        main/
        wt1/

no changes added to commit (use "git add" and/or "git commit -a")

但其他一切看起来都完全按预期工作。

对我来说,这似乎是这里所有答案的最佳折衷方案。