본문으로 건너뛰기

스태시

Git 스태시는 작업 디렉터리와 인덱스의 변경 사항을 저장해 다른 작업으로 전환한 뒤 나중에 복원할 수 있게 합니다. Pitmaster는 git의 저장 구조에 따라 refs/stash와 reflog를 사용해 스태시를 구현합니다. 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()을 실행하는 것과 같습니다.

특정 항목 Pop

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는 시간순으로 읽습니다. 스태시 목록은 이를 역순으로 표시합니다(최신 항목이 먼저). 스태시 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가 없습니다.

제한 사항

  • 전체 작업 디렉터리만 스태시할 수 있습니다(--keep-index 없음). 추적되지 않는 파일은 push($message, includeUntracked: true)을 통해서만 포함됩니다
  • --patch 모드(인터랙티브 청크 선택)는 지원되지 않습니다
  • Pop/Apply는 작업 트리 상태를 복원하지만 인덱스는 복원하지 않습니다. 스테이징된 수정 사항은 스테이징되지 않은 수정 사항으로, 새로 스테이징한 파일은 추적되지 않는 파일로 돌아옵니다
  • Repository::resolve()는 stash@{n}에서 허용되지 않습니다
  • 연결된 워크트리에서 스태시를 사용하면 잘못된 설정과 reflog 위치를 사용합니다(#14)