Рабочие деревья
Git рабочие деревья позволяют нескольким рабочим каталогам использовать один репозиторий. У каждого рабочего дерева есть собственная рабочая копия (HEAD, индекс, рабочие файлы), но хранилище объектов, конфигурация и packed-refs общие с основным репозиторием. Pitmaster позволяет создавать, перечислять, блокировать и удалять связанные рабочие деревья, а также открывать их как репозитории. Очистка, перемещение и восстановление рабочих деревьев не реализованы.
Основные понятия
Обычный репозиторий git имеет одно рабочее дерево:
project/
.git/ (directory)
src/
README.md
С рабочими деревьями можно создать дополнительные рабочие копии:
project/ (main worktree)
.git/ (the actual git directory)
worktrees/
review/ (metadata for linked worktree)
HEAD
commondir
gitdir
/tmp/review/ (linked worktree)
.git (file, not directory)
src/
README.md
.git связанного рабочего дерева — это файл, а не каталог; он содержит относительный путь к рабочему дереву:
gitdir: ../../path/to/project/.git/worktrees/review
Создание рабочего дерева
$repo = Pitmaster::open('/path/to/project');
// Create a linked worktree on a new branch
$wt = $repo->addWorktree('/tmp/review', 'feature/review');
echo $wt->path; // '/tmp/review'
echo $wt->branch; // 'feature/review'
echo $wt->gitDir; // '/path/to/project/.git/worktrees/review'
var_dump($wt->isMain); // bool(false)
var_dump($wt->isDetached); // bool(false)
addWorktree() выполняет следующие действия:
- Создает ветку, если ее еще нет (на основе HEAD или указанного коммита
$from) - Создает каталог метаданных по пути
.git/worktrees/<name>/ - Записывает файл
.gitв каталог рабочего дерева - Записывает файлы метаданных
HEAD,commondirиgitdir - Извлекает дерево ветки в каталог рабочего дерева
- Записывает индекс для рабочего дерева
Если ветка уже существует, $from игнорируется. В отличие от git, addWorktree() не отказывает, если ветка уже извлечена в другом рабочем дереве, и не запускает хук post-checkout.
С явным именем метаданных
Используйте необязательный аргумент name, если у нескольких связанных рабочих деревьев может совпадать имя каталога рабочей копии.
$wt = $repo->addWorktree(
'/tmp/sandboxes/app',
'feature/review',
name: 'sandbox-app',
);
Это имя метаданных используется в .git/worktrees/<name>/ вместо basename($path).
На основе определенного коммита
$wt = $repo->addWorktree('/tmp/hotfix', 'hotfix/urgent', $repo->resolve('v1.0.0'));
Список рабочих деревьев
$worktrees = $repo->worktrees();
foreach ($worktrees as $wt) {
$type = $wt->isMain ? 'main' : 'linked';
$branch = $wt->branch ?? 'detached';
$locked = $wt->isLocked ? ' [locked]' : '';
echo "{$wt->path} ({$type}) [{$branch}]{$locked}\n";
}
В список всегда сначала входит основное рабочее дерево, а затем связанные рабочие деревья. Вызовите worktrees() для дескриптора основного репозитория: для дескриптора связанного рабочего дерева основной элемент сейчас указывает собственный путь связанного рабочего дерева (#14).
Удаление рабочего дерева
$repo->removeWorktree('review'); // by metadata name
$repo->removeWorktree('/tmp/review'); // or by worktree path
// Force remove (even if locked)
$repo->removeWorktree('review', force: true);
Удаление рабочего дерева:
- Проверяет, заблокировано ли рабочее дерево (операция завершается ошибкой, если
$forceне равно true) - Удаляет файл
.gitиз каталога рабочего дерева - Удаляет каталог метаданных из
.git/worktrees/
Файлы рабочего дерева не удаляются автоматически. Ветка продолжает существовать.
Открытие связанного рабочего дерева
Связанное рабочее дерево можно открыть так же, как любой репозиторий:
$repo = Pitmaster::open('/tmp/review');
// Pitmaster detects the .git file and follows the gitdir reference
echo $repo->isLinkedWorktree(); // true
echo $repo->gitDir(); // '/path/to/project/.git/worktrees/review'
echo $repo->commonGitDir(); // '/path/to/project/.git'
echo $repo->workDir(); // '/tmp/review'
При выполнении операций с репозиторием пути разрешаются следующим образом:
- Объекты: чтение и запись в общий каталог git (
project/.git/objects/) - Конфигурация: чтение из общего каталога git (
project/.git/config) - Packed-refs: чтение из общего каталога git
- HEAD: отдельный для каждого рабочего дерева (каждое отслеживает собственную ветку), как и
ORIG_HEAD,MERGE_HEADи другие ссылки операций - Индекс: отдельный для каждого рабочего дерева (у каждого своя область подготовки)
- Непакованные ссылки: ветки и теги общие для всех и хранятся в общем каталоге (
project/.git/refs/) - Хуки: читаются из отдельного каталога git рабочего дерева, поэтому общие хуки не запускаются (#12)
Stash пока не поддерживает связанные рабочие деревья: он читает данные об авторе из отдельного каталога рабочего дерева, где нет конфигурации, и записывает туда reflog (#14).
Объект значения рабочего дерева
use Pitmaster\Worktree\Worktree;
$wt->name; // Metadata name (null for the main worktree)
$wt->path; // Working directory path
$wt->gitDir; // Per-worktree git dir
$wt->branch; // Branch name (null if detached)
$wt->head; // ObjectId of HEAD commit
$wt->isMain; // true for the main worktree
$wt->isDetached; // true if HEAD is detached
$wt->isLocked; // true if worktree is locked
$wt->lockReason; // Lock reason, or null when there is none (an empty reason is also null)
WorktreeManager
Для низкоуровневого управления рабочими деревьями. WorktreeManager::add() записывает только метаданные и файл .git: он не создает ветку, не извлекает файлы и не записывает индекс. Для полноценной рабочей копии используйте $repo->addWorktree(). Ветка уже должна существовать (или следует указать полное имя ссылки, например refs/tags/v1.0.0; в этом случае HEAD отсоединяется).
use Pitmaster\Worktree\WorktreeManager;
$manager = new WorktreeManager($repo->commonGitDir(), $mainWorkDir);
// List
$worktrees = $manager->list();
// Add
$wt = $manager->add('/tmp/review', 'feature/review');
$wt = $manager->add('/tmp/review', 'feature/review', 'review-app');
// Remove
$manager->remove('review');
$manager->remove('review', force: true);
// Lock/unlock (metadata name only)
$manager->lock('review', 'Do not delete, code review in progress');
$manager->unlock('review');
Блокировка рабочих деревьев
// Lock (prevents removal)
$manager->lock('review', 'Code review in progress');
// Attempt to remove locked worktree
try {
$manager->remove('review');
} catch (\RuntimeException $e) {
// "Worktree is locked: review (Code review in progress)"
}
// Force remove ignores the lock
$manager->remove('review', force: true);
// Unlock
$manager->unlock('review');
Блокировка хранится в виде файла по пути .git/worktrees/<name>/locked. Содержимое файла — причина блокировки (может быть пустым).
Обнаружение рабочих деревьев
// Check if a path is a linked worktree
if (Pitmaster::isWorktree('/tmp/review')) {
echo "This is a linked worktree\n";
}
// Resolve the common git dir from any path
$common = Pitmaster::commonGitDir('/tmp/review');
// '/path/to/project/.git'
// Resolve a .git file to the actual git directory
$gitDir = WorktreeManager::resolveGitFile('/tmp/review');
// '/path/to/project/.git/worktrees/review'
Структура файлов
Метаданные основного рабочего дерева для связанных рабочих деревьев
.git/worktrees/<name>/
HEAD Per-worktree HEAD (symbolic ref or detached hash)
commondir Relative path to the shared git dir ("../..")
gitdir Path back to the worktree's .git file, as passed to addWorktree()
locked Present if locked (content = reason)
index Per-worktree index/staging area
Файл .git связанного рабочего дерева
gitdir: <relative path to>/project/.git/worktrees/<name>
Совместное использование ресурсов
| Ресурс | Расположение | Общий? |
|---|---|---|
| Объекты | $commonDir/objects/ |
Да |
| Конфигурация | $commonDir/config |
Да |
| Packed-refs | $commonDir/packed-refs |
Да |
| Хуки | $gitDir/hooks/ |
Нет: Pitmaster не запускает общие хуки в связанных рабочих деревьях |
| HEAD | $gitDir/HEAD |
Нет (отдельный для каждого рабочего дерева) |
| Индекс | $gitDir/index |
Нет (отдельный для каждого рабочего дерева) |
| Непакованные ссылки | $commonDir/refs/ |
Да (HEAD и ссылки операций, например MERGE_HEAD, отдельны для каждого рабочего дерева) |