工作树
Git 工作树允许多个工作目录共用一个仓库。每个工作树都有自己的检出内容(HEAD、索引和工作文件),但与主仓库共用对象存储、配置和 packed-refs。Pitmaster 可以创建、列出、锁定和移除关联工作树,也可以将它们作为仓库打开。目前尚未实现修剪、移动和修复工作树。
概念
常规的 git 仓库只有一个工作树:
project/
.git/ (directory)
src/
README.md
使用 worktree,你可以拥有额外的检出目录:
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
链接 worktree 的 .git 是一个文件(而非目录),其中包含相对于 worktree 的路径:
gitdir: ../../path/to/project/.git/worktrees/review
创建 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() 会执行以下操作:
- 如果分支不存在,则创建该分支(基于 HEAD 或指定的
$from提交) - 在
.git/worktrees/<name>/创建元数据目录 - 在 worktree 目录中写入
.git文件 - 写入
HEAD、commondir和gitdir元数据文件 - 将该分支的树检出到 worktree 目录
- 为该 worktree 写入索引
如果分支已存在,则忽略 $from。与 git 不同,addWorktree() 不会拒绝已在另一个 worktree 中检出的分支,也不会运行 post-checkout 钩子。
使用显式元数据名称
当多个链接 worktree 可以共用相同的检出目录基本名称时,请使用可选的 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'));
列出 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";
}
列表始终先包含主 worktree,随后是链接 worktree。在主仓库的句柄上调用 worktrees():在链接 worktree 的句柄上调用时,主条目目前会报告该链接 worktree 自身的路径(#14)。
移除 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);
移除 worktree 时:
- 检查 worktree 是否已锁定(除非
$force为 true,否则操作失败) - 从 worktree 目录中移除
.git文件 - 从
.git/worktrees/中移除元数据目录
不会自动删除工作树文件。该分支仍然存在。
打开链接 worktree
你可以像打开任何仓库一样打开链接 worktree:
$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:每个 worktree 独有(各 worktree 跟踪自己的分支),
ORIG_HEAD、MERGE_HEAD及其他操作引用也一样 - 索引:每个 worktree 独有(各 worktree 有自己的暂存区)
- 松散引用:分支和标签在公共目录中共享(
project/.git/refs/) - 钩子:从各 worktree 自己的 git 目录读取,因此不会运行共享钩子(#12)
Stash 目前尚不支持链接 worktree:它会从各 worktree 的目录读取身份信息,但该目录没有配置文件,并会在该目录中写入其 reflog(#14)。
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
用于底层 worktree 管理。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');
锁定 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');
锁会以文件形式存储在 .git/worktrees/<name>/locked。文件内容是锁定原因(可以为空)。
检测 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'
文件布局
链接 worktree 的主 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
链接 worktree 的 .git 文件
gitdir: <relative path to>/project/.git/worktrees/<name>
资源共享
| 资源 | 位置 | 是否共享? |
|---|---|---|
| 对象 | $commonDir/objects/ |
是 |
| 配置 | $commonDir/config |
是 |
| Packed-refs | $commonDir/packed-refs |
是 |
| 钩子 | $gitDir/hooks/ |
否:Pitmaster 不会在链接 worktree 中运行共享钩子 |
| HEAD | $gitDir/HEAD |
否(每个 worktree 独有) |
| 索引 | $gitDir/index |
否(每个 worktree 独有) |
| 松散引用 | $commonDir/refs/ |
是(HEAD 和 MERGE_HEAD 等操作引用各 worktree 独有) |