Worktree
Git worktree umożliwiają współdzielenie jednego repozytorium przez wiele katalogów roboczych. Każde worktree ma własną kopię roboczą (HEAD, indeks, pliki robocze), ale współdzieli z głównym repozytorium magazyn obiektów, konfigurację i spakowane refs. Pitmaster może tworzyć, wyświetlać, blokować i usuwać połączone worktree oraz otwierać je jako repozytoria. Przycinanie, przenoszenie i naprawianie worktree nie jest zaimplementowane.
Pojęcia
Zwykłe repozytorium git ma jedno drzewo robocze:
project/
.git/ (directory)
src/
README.md
Dzięki worktree możesz mieć dodatkowe kopie robocze:
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
Plik .git powiązanego worktree zawiera ścieżkę względną względem worktree:
gitdir: ../../path/to/project/.git/worktrees/review
Tworzenie worktree
$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() wykonuje następujące czynności:
- Tworzy gałąź, jeśli jeszcze nie istnieje (na podstawie HEAD lub wskazanego commita
$from) - Tworzy katalog metadanych w
.git/worktrees/<name>/ - Zapisuje plik
.gitw katalogu worktree - Zapisuje pliki metadanych
HEAD,commondirigitdir - Wypisuje drzewo gałęzi do katalogu worktree
- Zapisuje indeks worktree
Jeśli gałąź już istnieje, $from jest ignorowane. W przeciwieństwie do gita, addWorktree() nie odmawia użycia gałęzi, która jest już wypisana w innym worktree, i nie uruchamia hooka post-checkout.
Z jawną nazwą metadanych
Użyj opcjonalnego argumentu name, gdy wiele powiązanych worktree może współdzielić tę samą nazwę bazową kopii roboczej.
$wt = $repo->addWorktree(
'/tmp/sandboxes/app',
'feature/review',
name: 'sandbox-app',
);
Ta nazwa metadanych jest używana w .git/worktrees/<name>/ zamiast basename($path).
Z określonego commita
$wt = $repo->addWorktree('/tmp/hotfix', 'hotfix/urgent', $repo->resolve('v1.0.0'));
Wyświetlanie worktree
$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";
}
Lista zawsze zawiera najpierw główne worktree, a następnie powiązane worktree. Wywołaj worktrees() na uchwycie głównego repozytorium: w przypadku uchwytu powiązanego worktree główny wpis obecnie wskazuje własną ścieżkę powiązanego worktree (#14).
Usuwanie worktree
$repo->removeWorktree('review'); // by metadata name
$repo->removeWorktree('/tmp/review'); // or by worktree path
// Force remove (even if locked)
$repo->removeWorktree('review', force: true);
Usuwanie worktree:
- Sprawdza, czy worktree jest zablokowane (kończy się niepowodzeniem, chyba że
$forcema wartość true) - Usuwa plik
.gitz katalogu worktree - Usuwa katalog metadanych z
.git/worktrees/
Pliki drzewa roboczego nie są automatycznie usuwane. Gałąź nadal istnieje.
Otwieranie powiązanego worktree
Powiązane worktree można otworzyć tak samo jak każde repozytorium:
$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'
Operacje na repozytorium rozwiązują ścieżki w następujący sposób:
- Obiekty: odczytywane ze wspólnego katalogu git i do niego zapisywane (
project/.git/objects/) - Konfiguracja: odczytywana ze wspólnego katalogu git (
project/.git/config) - Packed-refs: odczytywane ze wspólnego katalogu git
- HEAD: osobno dla każdego worktree (każde worktree śledzi własną gałąź), wraz z
ORIG_HEAD,MERGE_HEADi innymi referencjami operacji - Indeks: osobno dla każdego worktree (każde worktree ma własny obszar przygotowania)
- Luźne referencje: gałęzie i tagi są współdzielone we wspólnym katalogu (
project/.git/refs/) - Hooki: odczytywane z katalogu git danego worktree, więc współdzielone hooki nie są uruchamiane (#12)
Stash nie obsługuje jeszcze powiązanych worktree: odczytuje tożsamość z katalogu danego worktree, który nie zawiera konfiguracji, i zapisuje tam reflog (#14).
Obiekt wartości worktree
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
Do niskopoziomowego zarządzania worktree. WorktreeManager::add() zapisuje tylko metadane i plik .git: nie tworzy gałęzi, nie wypisuje plików ani nie zapisuje indeksu. Użyj $repo->addWorktree(), aby uzyskać gotową kopię roboczą. Gałąź musi już istnieć (lub być podana jako pełna nazwa referencji, taka jak refs/tags/v1.0.0, która odłącza 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');
Blokowanie worktree
// 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');
Blokada jest przechowywana w pliku .git/worktrees/<name>/locked. Zawartość pliku stanowi powód blokady (może być pusty).
Wykrywanie worktree
// 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'
Układ plików
Metadane głównego worktree dla powiązanych worktree
.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
Plik .git powiązanego worktree
gitdir: <relative path to>/project/.git/worktrees/<name>
Współdzielenie zasobów
| Zasób | Lokalizacja | Współdzielony? |
|---|---|---|
| Obiekty | $commonDir/objects/ |
Tak |
| Konfiguracja | $commonDir/config |
Tak |
| Packed-refs | $commonDir/packed-refs |
Tak |
| Hooki | $gitDir/hooks/ |
Nie: Pitmaster nie uruchamia współdzielonych hooków w powiązanych worktree |
| HEAD | $gitDir/HEAD |
Nie (osobno dla każdego worktree) |
| Indeks | $gitDir/index |
Nie (osobno dla każdego worktree) |
| Luźne referencje | $commonDir/refs/ |
Tak (HEAD i referencje operacji, takie jak MERGE_HEAD, są osobne dla każdego worktree) |