Git голые репозитории, рабочие деревья и отслеживание веток

Я работаю с базой кода, где мне нужно работать над несколькими ветками одновременно для разных целей. Поэтому я клонирую в пустой репозиторий, а затем настраиваю несколько рабочих деревьев:

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 worktree не настроен на отслеживание удаленного дерева и, пытаясь заставить его это сделать, я попадаю в ужасный беспорядок.

$ 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.

Какая правильная магия, чтобы заставить это работать?

4 ответа

Решение

Во-первых, примечание: если вы собираетесь использовать git worktree add в течение нетривиальных периодов (более двух недель одновременно) убедитесь, что ваш Git имеет версию не ниже 2.15.1

Для вашей конкретной цели я бы рекомендовал не использовать git clone --bare, Вместо этого используйте обычный клон, а затем git worktree addс, что вы собираетесь делать. Вы отмечаете в комментарии, что:

... в конечном итоге вам необходимо создать фиктивную ветвь для размещения самого репозитория, поскольку невозможно одновременно разместить и репозиторий, и рабочее дерево в одной и той же ветке.

Для этого есть несколько простых способов:

  • Выберите одно из N рабочих деревьев, которые вы добавите, и используйте его в качестве ветви основного рабочего дерева:

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

    Недостаток в том, что теперь у вас есть специальная выделенная "основная" ветвь, которую вы не можете просто удалить в любое время - все остальные ветки зависят от нее.

  • Или, после клона, используйте git checkout --detach в рабочем дереве, чтобы получить отделенный заголовок в ветви по умолчанию:

    git clone ssh://git@git.example.com/project/repo repo.git
    cd repo.git
    git checkout --detach
    
  • Единственным недостатком этого второго подхода является то, что рабочее дерево полно файлов, возможно, это пустая трата места. Есть решение и для этого: создайте пустой коммит, используя пустое дерево, и проверьте это:

    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 создает идентификатор хэша пустого дерева, которое уже существует в каждом хранилище. git commit-tree делает коммит, чтобы обернуть это пустое дерево - его нет, поэтому мы должны создать его, чтобы проверить его - и распечатать хэш-идентификатор этого нового коммита, и git checkout проверяет это как отдельную ГОЛОВУ. В результате мы освобождаем наш индекс и рабочее дерево, так что единственное, что есть в рабочем дереве хранилища, это .git каталог. Пустой коммит, который мы сделали, не принадлежит ни одной ветви и не имеет родительского коммита (это однокорневой коммит), который мы никогда нигде не запустим.


1 Причиной этого является то, что Git 2.5, где git worktree впервые появился, есть ошибка, которую я считаю очень плохой: git gc никогда не сканирует добавленные рабочие деревья HEAD файлы, ни их индексные файлы. Если добавленные рабочие деревья всегда находятся на некоторой ветви и никогда не имеют git addРедкая, но не совершенная работа, это никогда не вызывает никаких проблем. Если незавершенная работа не выполняется в течение по крайней мере 14 дней, для защиты от удаления из нее достаточно времени защиты по умолчанию. Но если ты git add какую-то работу или поручить отдельную ГОЛОВУ, уйти в отпуск на месяц или иным образом оставить это добавленное рабочее дерево без изменений, а затем вернуться к нему, и git gc --auto после двухнедельного льготного периода, файлы, которые вы сохранили, были уничтожены!

Эта ошибка была исправлена ​​в Git 2.15.


Зачем --bare пойдет не так

Корень проблемы здесь в том, что git clone --bare делает две вещи:

  • это делает голое хранилище (core.bare установлен в true) который не имеет рабочего дерева и не имеет начального git checkout; а также
  • это меняет значение по умолчанию fetch refspec от +refs/heads/*:refs/remotes/origin/* в +refs/heads/*:refs/heads/*,

Этот второй пункт означает, что нет refs/remotes/origin/ имена, как вы обнаружили. Это не так легко исправить из-за (в основном скрытого / внутреннего) концепции refmaps, которые очень кратко проявляются в git fetch документация (см. ссылку).

Хуже, это означает, что refs/heads/* будет обновляться на каждом git fetch, Есть причина git worktree add отказывается создавать второе рабочее дерево, которое ссылается на ту же ветку, которая извлечена в любом существующем рабочем дереве, и это то, что Git принципиально предполагает, что никто не будет связываться с refs/heads/name ссылка, что HEAD прикреплен к этому рабочему дереву. Так что даже если вы работали над проблемой refmap, при запуске git fetch и он обновляет - или даже удаляет, из-за --prune и удаление вверх по течению с тем же именем - refs/heads/name имя, добавленное дерево работ HEAD ломается, и само дерево работы становится проблематичным. (См. Почему Git позволяет отправлять извлеченную ветку в добавленном рабочем дереве? Как мне восстановить?)

Есть еще одна вещь, которую вы можете попробовать, которую я вообще не тестировал, а именно: сделать пустой клон, как вы делаете, но затем измените refspec и повторите выборку, и удалите все существующие имена ветвей, так как они не имеют набор восходящих потоков (или, что то же самое, установить их восходящие потоки):

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/{} {}).

Я считаю, что есть очень простой способ;

1. клонировать репо без проверки (без лишнего места)

      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

Это все. Это чисто, легко, не тратя лишнего места

Надеюсь это поможет ;-)

Я долго боролся с этим, никогда не вспоминая шаги, которые я предпринял, чтобы создать голое репо, которое не действовало бы как репо. В конце концов я написал следующий сценарий под названиемgit-clone-bare-for-worktreesдля создания голых репозиториев для использования с рабочими деревьями. Кажется, он работает довольно хорошо, но будьте осторожны, он не обрабатывает много ошибок.

#! /bin/env bash
set -e

url=$1
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 указывает на первую фиксацию в репо и устанавливает для него значение HEAD, гарантируя, что все "настоящие" ветви доступны для безопасной проверки в рабочих деревьях. Помимо этого, репо не будет содержать никаких локальных веток, дажеmasterно ветки удаленного отслеживания будут созданы точно так же, как и в случае с обычным, не пустым клоном. Так что просто беги быстроgit worktree add ../master-worktree master и вы должны быть в рабочем состоянии.

Другим решением было бы использование --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каталог ведет себя как несинхронизированное рабочее дерево, если вы попытаетесь запустить там команды 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")

Но все остальное работает именно так, как задумано.

Для меня это выглядит как лучший компромисс из всех ответов здесь.

Другие вопросы по тегам