跳转到正文

储藏

Git 储藏会保存工作目录和索引中的更改,让你可以切换到其他任务,之后再恢复这些更改。Pitmaster 使用 refs/stash 和 reflog,在 git 的存储布局中实现储藏。Repository 没有储藏方法;请使用下面的 Stash 类。

有两个已知问题:当条目多于一个时,drop()(因此 pop() 也是)会将条目留在列表中(#10);apply() 会覆盖储藏未涉及文件中的未提交更改(#11)。请在干净的工作树上应用储藏。

储藏的工作原理

每个储藏条目都是一个有两个父提交的提交:

  • 父提交 0:储藏时的 HEAD 提交(基准状态)
  • 父提交 1:用于记录索引状态(已暂存更改)的提交

储藏提交自身的树记录了工作树状态(包括未暂存的更改)。

储藏“栈”是 refs/stash 的 reflog。最新的储藏就是 refs/stash 本身。较早的条目是 reflog 条目:stash@{0}、stash@{1} 等。

代码
refs/stash -> stash commit (worktree state)
                parent[0] -> HEAD at time of stash
                parent[1] -> index state commit
                               parent[0] -> HEAD at time of stash

创建 Stash 对象

PHP
use Pitmaster\Stash\Stash;

$stash = new Stash(
    $repo->objectDatabase(),
    $repo->refDatabase(),
    $repo->gitDir(),
    $repo->workDir(),
);

Push(保存更改)

PHP
// public function push(string $message = '', bool $includeUntracked = false): ObjectId
$stashId = $stash->push('Work in progress on login');
echo "Saved: {$stashId->hex}\n";

// Also stash untracked files
$stashId = $stash->push('With new files', includeUntracked: true);

Push 会执行以下操作:

  1. 读取当前 HEAD 提交
  2. 根据当前索引构建树(已暂存状态)
  3. 创建以 HEAD 为父提交的“索引提交”
  4. 根据当前工作目录构建树(包括未暂存的更改)
  5. 创建储藏提交(工作树树对象,父提交 = HEAD + 索引提交)
  6. 更新 refs/stash,使其指向储藏提交
  7. 为栈追加一条 reflog 记录
  8. 将工作树和索引重置为 HEAD

Push 后,已跟踪文件与 HEAD 一致。未跟踪文件会保留,除非设置了 includeUntracked;设置后,这些文件会被删除并存储在储藏提交自身的树中(git 则会将它们存储在第三个父提交中,因此 git 会将它们视为已跟踪的更改)。即使没有任何更改,push() 也会创建条目,此时 git 会报告“没有本地更改可保存”。

使用默认消息

PHP
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"

在分离的 HEAD 状态下,<branch> 是 HEAD。

Pop(应用并移除)

PHP
$stash->pop();

将栈顶的储藏条目应用到工作目录,并从栈中移除。等同于先执行 apply(),再执行 drop()。

弹出指定条目

PHP
$stash->pop(1);  // Pop stash@{1}

Apply(恢复但不移除)

PHP
$stash->apply();      // Apply stash@{0}
$stash->apply(2);     // Apply stash@{2}

Apply 会逐个文件将储藏合并到当前 HEAD(储藏基准、当前 HEAD、储藏树)。储藏条目仍保留在栈中,索引也不会被修改。发生冲突时,文件中会留下标记 Updated upstream 和 Stashed changes,并抛出 Pitmaster\Exceptions\MergeConflictException;随后 pop() 会保留该条目。Apply 不会先检查本地更改(见顶部说明)。

列出条目

PHP
$entries = $stash->listEntries();

foreach ($entries as $entry) {
    echo "stash@{{$entry['index']}}: {$entry['message']}\n";
    // stash@{0}: On main: Work in progress on login
    // stash@{1}: WIP on main: f707601 Add login form
}

每个条目包含:

  • index:储藏位置(0 = 最新)
  • message:储藏消息
  • hash:储藏提交哈希

Drop(移除但不应用)

PHP
$stash->drop();       // Drop stash@{0}
$stash->drop(1);      // Drop stash@{1}

如果删除唯一条目,refs/stash 及其 reflog 会被移除。如果还有其他条目,drop() 目前只会移动 refs/stash,而不会更改 reflog,因此该条目仍会留在 listEntries() 中(#10)。

存储详情

储藏提交结构

代码
commit <hash>
tree <worktree-tree-hash>
parent <HEAD-hash>
parent <index-commit-hash>
author <current identity>
committer <current identity>

<stash message>

索引提交结构

代码
commit <hash>
tree <index-tree-hash>
parent <HEAD-hash>
author <current identity>
committer <current identity>

index on <branch>: <short hash> <HEAD subject>

身份信息依次取自仓库配置中的 GIT_COMMITTER_NAME、GIT_AUTHOR_NAME、user.name,然后是 Pitmaster(电子邮件也遵循相同顺序,以 pitmaster@example.invalid 结尾)。日期依次取自 GIT_COMMITTER_DATE、GIT_AUTHOR_DATE,然后是当前时间。与提交不同,储藏不会读取 PITMASTER_* 常量。

Reflog 存储

储藏条目存储在 .git/logs/refs/stash 中:

代码
0000000000000000000000000000000000000000 abc123... Name <email> 1700000000 +0000	WIP on main: f707601 Add login form
abc123... def456... Name <email> 1700000001 +0000	On main: Work in progress on login

reflog 按时间顺序读取。储藏列表会将其倒序排列(最新的在前)。无论 core.logallrefupdates 如何,都会写入储藏 reflog。

CLI 用法

命令行
# Save current changes
./vendor/bin/pitmaster stash
./vendor/bin/pitmaster stash push
./vendor/bin/pitmaster stash push "Work in progress on login"

# Apply and remove top entry
./vendor/bin/pitmaster stash pop

# Apply without removing
./vendor/bin/pitmaster stash apply

# List all entries
./vendor/bin/pitmaster stash list

# Remove top entry without applying
./vendor/bin/pitmaster stash drop

消息是 push 后面的参数(或 save);不支持 -m。pop、apply 和 drop 始终作用于 stash@{0};CLI 没有 --include-untracked。

限制

  • 仅支持完整工作目录储藏(不支持 --keep-index);未跟踪文件只能通过 push($message, includeUntracked: true) 储藏
  • 不支持 --patch 模式(交互式选择代码块)
  • Pop/apply 会恢复工作树状态,但不会恢复索引。已暂存的编辑会以未暂存编辑的形式恢复,新暂存的文件会以未跟踪文件的形式恢复
  • stash@{n} 不能由 Repository::resolve() 接受
  • 在关联工作树中储藏时会使用错误的配置和 reflog 位置(#14)