スタッシュ
Git stashは作業ディレクトリとインデックスの変更を保存するため、別のタスクに切り替えてから後で復元できます。Pitmasterはrefs/stashとreflogを使い、gitのストレージレイアウトに沿ってstashを実装します。Repositoryにはstashメソッドがありません。以下のStashクラスを使用してください。
既知の問題が2つあります。エントリーが複数ある場合、drop()(したがってpop()も)はエントリーをリストに残します(#10)。また、apply()はstashが触れないファイルの未コミット変更を上書きします(#11)。クリーンな作業ツリーにstashを適用してください。
stashの仕組み
各stashエントリーは、2つの親を持つコミットです。
- 親0: stash時点のHEADコミット(ベースの状態)
- 親1: インデックスの状態(ステージ済みの変更)を記録するコミット
stashコミット自身のツリーには、作業ツリーの状態(ステージされていない変更を含む)が記録されます。
stashの「スタック」はrefs/stashのreflogです。最新のstashは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オブジェクトの作成
use Pitmaster\Stash\Stash;
$stash = new Stash(
$repo->objectDatabase(),
$repo->refDatabase(),
$repo->gitDir(),
$repo->workDir(),
);
Push(変更を保存)
// 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は次の処理を行います。
- 現在のHEADコミットを読み取る
- 現在のインデックスからツリーを作成する(ステージ済みの状態)
- HEADを親とする「インデックスコミット」を作成する
- 現在の作業ディレクトリからツリーを作成する(ステージされていない変更を含む)
- stashコミットを作成する(作業ツリーのツリー、親はHEADとインデックスコミット)
refs/stashをstashコミットに向ける- スタックのreflogエントリーを追加する
- 作業ツリーとインデックスをHEADにリセットする
Push後、追跡対象ファイルはHEADと一致します。includeUntrackedが設定されていない限り、未追跡ファイルは残ります。設定されている場合は削除され、stashコミット自身のツリーに保存されます(gitでは代わりに第3の親に保存されるため、gitは追跡対象の変更として認識します)。push()は、変更がなくてもエントリーを作成します。その場合、gitは「No local changes to save」と報告します。
デフォルトのメッセージを使う場合
$stashId = $stash->push();
// Message: "WIP on <branch>: <short hash> <HEAD subject>"
// With a message: "On <branch>: <message>"
HEADがデタッチされている場合、<branch>はHEADです。
Pop(適用して削除)
$stash->pop();
スタックの先頭のstashエントリーを作業ディレクトリに適用し、スタックから削除します。apply()を実行してからdrop()を実行するのと同等です。
特定のエントリーをPopする
$stash->pop(1); // Pop stash@{1}
Apply(削除せずに復元)
$stash->apply(); // Apply stash@{0}
$stash->apply(2); // Apply stash@{2}
Applyはstashを現在のHEADのファイルごとにマージします(stashのベース、現在のHEAD、stashツリー)。stashエントリーはスタックに残り、インデックスは変更されません。競合があると、ファイルにUpdated upstreamとStashed changesというラベルの競合マーカーが残り、Pitmaster\Exceptions\MergeConflictExceptionがスローされます。pop()はその後もエントリーを保持します。Applyは事前にローカル変更を確認しません(冒頭の注記を参照)。
エントリー一覧
$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: stashの位置(0が最新)message: stashのメッセージhash: stashコミットのハッシュ
Drop(適用せずに削除)
$stash->drop(); // Drop stash@{0}
$stash->drop(1); // Drop stash@{1}
唯一のエントリーを削除すると、refs/stashとそのreflogが削除されます。複数のエントリーがある場合、drop()は現在refs/stashを移動するだけでreflogを変更しないため、エントリーはlistEntries()に残ります(#10)。
ストレージの詳細
stashコミットの構造
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、それらがなければ現在時刻から取得します。コミットとは異なり、stashはPITMASTER_*定数を読み取りません。
Reflogの保存
stashエントリーは.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は時系列順に読み込まれます。stash一覧では逆順(新しいものが先)になります。stashのreflogはcore.logallrefupdatesに関係なく書き込まれます。
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がありません。
制限事項
- 作業ディレクトリ全体のstashのみ(
--keep-indexなし)。未追跡ファイルはpush($message, includeUntracked: true)経由でのみ保存 --patchモード(対話形式のハンク選択)なし- Pop/Applyは作業ツリーの状態を復元しますが、インデックスは復元しません。ステージ済みの編集はステージされていない編集として戻り、新たにステージされたファイルは未追跡ファイルとして戻ります
stash@{n}はRepository::resolve()では受け付けられません- リンクされたworktree内のstashは、誤った設定とreflogの場所を使用します(#14)