コンテンツへスキップ

スタッシュ

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オブジェクトの作成

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. stashコミットを作成する(作業ツリーのツリー、親はHEADとインデックスコミット)
  6. refs/stashをstashコミットに向ける
  7. スタックのreflogエントリーを追加する
  8. 作業ツリーとインデックスをHEADにリセットする

Push後、追跡対象ファイルはHEADと一致します。includeUntrackedが設定されていない限り、未追跡ファイルは残ります。設定されている場合は削除され、stashコミット自身のツリーに保存されます(gitでは代わりに第3の親に保存されるため、gitは追跡対象の変更として認識します)。push()は、変更がなくてもエントリーを作成します。その場合、gitは「No local changes to save」と報告します。

デフォルトのメッセージを使う場合

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();

スタックの先頭のstashエントリーを作業ディレクトリに適用し、スタックから削除します。apply()を実行してからdrop()を実行するのと同等です。

特定のエントリーをPopする

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

Apply(削除せずに復元)

PHP
$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は事前にローカル変更を確認しません(冒頭の注記を参照)。

エントリー一覧

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: stashの位置(0が最新)
  • message: stashのメッセージ
  • hash: stashコミットのハッシュ

Drop(適用せずに削除)

PHP
$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)