Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion src/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,12 @@ const MAX_BRANCH_LEN: usize = 256;
/// (NFS 등)에서 status 렌더가 블록될 수 있다(알려진 트레이드오프). 완화(timeout/캐시)는 future work.
fn read_branch_from_git_dir(base_path: &str) -> Option<String> {
use std::path::Path;
// 표준 워크트리는 `<base>/.git/HEAD`. (linked worktree의 gitfile 케이스는 v1 범위 밖.)
// 표준 워크트리는 `<base>/.git/HEAD`(디렉터리). linked worktree(`git worktree add`)와 서브모듈은
// `<base>/.git`가 `gitdir: <path>`를 담은 정규 파일이고 실제 HEAD는 그 gitdir(보통 main repo 하위
// = `<base>` 밖)에 있다. 이 gitfile 추종은 v1 범위 밖 — `<base>/.git`가 정규 파일이라 그 하위
// `<base>/.git/HEAD` 탐색이 불가(ENOTDIR)해 canonicalize가 Err를 내고 None으로 안전 저하한다
// (의도된 false-negative=branch 미표시). gitdir 추종은 임의 위치 파일 read를 열어 공격면을
// 키우므로(추종을 추가하면 gitfile 내용이 신뢰 불가 입력이 되어 공격면이 열리므로) 의도적으로 미지원.
let head_path = Path::new(base_path).join(".git").join("HEAD");
// 외부 입력 경로 검증(심볼릭 차단): canonicalize로 심볼릭 링크/`.` 등을 해소한 실제
// 경로가 여전히 `.git/HEAD`로 끝나는지 확인한다. 심볼릭 링크가 다른 파일을 가리키면
Expand Down Expand Up @@ -1733,4 +1738,72 @@ mod tests {
"유효 git cwd → branch 도출"
);
}

/// 회귀 가드(Task3 No-go, mutation 저항): `<cwd>/.git`가 `gitdir:` 정규 파일이고 그 gitdir에
/// 유효 HEAD(sentinel ref)가 실재해도, 현재는 추종하지 않아 None(안전 FN)이다. 누가 gitfile
/// 추종을 추가하면 Some("<sentinel>")을 반환해 이 None 단언이 깨진다 = 추종 회귀를 실제로 포착.
#[test]
fn derive_from_cwd_gitfile_not_followed_none() {
// (a) 절대 gitdir — 타깃 실재화 + sentinel HEAD
let gitdir_abs = unique_test_dir("gitfile-target-abs");
std::fs::create_dir_all(&*gitdir_abs).expect("gitdir 생성 실패");
std::fs::write(gitdir_abs.join("HEAD"), "ref: refs/heads/sentinel-abs\n")
.expect("타깃 HEAD 생성 실패");
let base_abs = unique_test_dir("gitfile-abs");
std::fs::create_dir_all(&*base_abs).expect("cwd 생성 실패");
std::fs::write(
base_abs.join(".git"),
format!("gitdir: {}\n", gitdir_abs.to_string_lossy()),
)
.expect(".git 파일 생성 실패");
assert_eq!(
derive_git_branch_from_cwd(&base_abs.to_string_lossy()),
None,
"절대 gitdir gitfile 미추종 → None(추종 구현 시 Some(sentinel-abs)로 깨짐)"
);

// (b) 상대 gitdir — base 하위 타깃 실재화 + sentinel HEAD
let base_rel = unique_test_dir("gitfile-rel");
std::fs::create_dir_all(base_rel.join("realgit")).expect("상대 gitdir 생성 실패");
std::fs::write(
base_rel.join("realgit").join("HEAD"),
"ref: refs/heads/sentinel-rel\n",
)
.expect("타깃 HEAD 생성 실패");
std::fs::write(base_rel.join(".git"), "gitdir: realgit\n").expect(".git 파일 생성 실패");
assert_eq!(
derive_git_branch_from_cwd(&base_rel.to_string_lossy()),
None,
"상대 gitdir gitfile 미추종 → None(추종 구현 시 Some(sentinel-rel)로 깨짐)"
);
}

/// 회귀 가드(Task3 No-go, mutation 저항): workspace git_worktree가 gitfile 워크트리를 가리키고
/// 그 gitdir에 유효 HEAD가 실재해도 미추종 → None. cwd 경로와 대칭으로 양 진입점 고정.
/// 추종이 구현되면 Some("sentinel-ws")를 반환해 이 None 단언이 깨진다 = 추종 회귀를 실제로 포착.
#[test]
fn derive_git_branch_gitfile_worktree_not_followed_none() {
let gitdir = unique_test_dir("gitfile-ws-target");
std::fs::create_dir_all(&*gitdir).expect("gitdir 생성 실패");
std::fs::write(gitdir.join("HEAD"), "ref: refs/heads/sentinel-ws\n")
.expect("타깃 HEAD 생성 실패");
let base = unique_test_dir("gitfile-ws");
std::fs::create_dir_all(&*base).expect("워크트리 생성 실패");
std::fs::write(
base.join(".git"),
format!("gitdir: {}\n", gitdir.to_string_lossy()),
)
.expect(".git 파일 생성 실패");
let ws = RawWorkspace {
git_worktree: Some(base.to_string_lossy().into_owned()),
repo: None,
current_dir: None,
project_dir: None,
};
assert_eq!(
derive_git_branch(&ws),
None,
"gitfile 워크트리 미추종 → None(추종 구현 시 Some(sentinel-ws)로 깨짐)"
);
}
}