diff --git a/apps/ely-tree/src/git.rs b/apps/ely-tree/src/git.rs index 9c3a3ac..9a4340b 100644 --- a/apps/ely-tree/src/git.rs +++ b/apps/ely-tree/src/git.rs @@ -76,9 +76,25 @@ impl Git { /// HEAD names a commit; a new repository has none yet. pub fn born(&self, dir: &Path) -> io::Result { - Ok(!self - .run_or_one(dir, &["rev-parse", "--verify", "-q", "HEAD"])? - .is_empty()) + if !self.run_or_one(dir, &["rev-parse", "--verify", "-q", "HEAD"])?.is_empty() { + return Ok(true); + } + // Unborn: HEAD names a branch with no ref at all. + let branch = String::from_utf8(self.run(dir, &["symbolic-ref", "-q", "HEAD"])?) + .map_err(|_| io::Error::other("HEAD names a ref that is not UTF-8"))?; + // Only Git's line end goes; a name keeps every byte. + let branch = branch.strip_suffix('\n').unwrap_or(&branch).to_owned(); + let found = self.command(dir, &["for-each-ref", "--format=%(refname)", &branch]).output()?; + let unborn = branch.starts_with("refs/heads/") && found.status.success(); + if unborn && found.stdout.is_empty() && found.stderr.is_empty() { + return Ok(false); + } + let mut why = format!("HEAD names {branch}: no commit, and no branch yet to begin"); + let said = String::from_utf8_lossy(&found.stderr); + if !said.trim().is_empty() { + why = format!("{why}; Git says {}", said.trim()); + } + Err(io::Error::other(why)) } /// Exit 1 is an answer: files differ, key unset. diff --git a/apps/ely-tree/src/tests/places.rs b/apps/ely-tree/src/tests/places.rs index bca92d6..3fe1b8c 100644 --- a/apps/ely-tree/src/tests/places.rs +++ b/apps/ely-tree/src/tests/places.rs @@ -447,6 +447,27 @@ fn a_new_repository_lists_no_moves_and_no_lfs_files() { crate::structure::lfs(&git(), &root).expect("no LFS error"), Vec::new() ); + // A new branch whose name ends in a no-break space. + let named = committed(&dir.path().join("named")); + sh(&named, &["branch", "topic"]); + sh(&named, &["checkout", "-q", "--orphan", "topic\u{a0}"]); + assert_eq!(crate::recover::reflog(&git(), &named).expect("no reflog error"), Vec::new()); +} + +#[test] +fn a_broken_head_is_an_error_not_an_empty_repository() { + let dir = tempfile::tempdir().expect("a temp dir"); + let root = committed(dir.path()); + let branch = sh(&root, &["symbolic-ref", "--short", "HEAD"]).trim().to_owned(); + std::fs::write(root.join(".git/refs/heads").join(branch), "not-an-object-id +").expect("breaks"); + assert!(crate::recover::reflog(&git(), &root).is_err(), "the reflog says why"); + assert!(crate::structure::lfs(&git(), &root).is_err(), "LFS says why"); + // HEAD at a missing tag is no new branch either. + let other = dir.path().join("other"); + crate::repo::init(&git(), &other).expect("init"); + sh(&other, &["symbolic-ref", "HEAD", "refs/tags/missing"]); + assert!(crate::recover::reflog(&git(), &other).is_err(), "a tag HEAD says why"); } #[test] diff --git a/tasks/reviews/G-03-r1.md b/tasks/reviews/G-03-r1.md new file mode 100644 index 0000000..f1faffe --- /dev/null +++ b/tasks/reviews/G-03-r1.md @@ -0,0 +1,3 @@ +FAIL + +- apps/ely-tree/src/git.rs:80 — The HEAD check also treats corrupt references as an empty repository, swallowing real Git errors (rule 2). Reproduction: create a repository with one commit, then replace its current `.git/refs/heads/` file with `not-an-object-id\n`. On both native Windows and WSL Linux, `rev-parse --verify -q HEAD` exits 1 without output; `born()` returns `Ok(false)`, and both `recover::reflog()` and `structure::lfs()` return `Ok([])`, although `git reflog show HEAD --` reports `fatal: bad revision 'HEAD'`. Recognize only a valid unborn symbolic HEAD as empty; propagate corrupt/unreadable-reference errors and add a regression for this case. Evidence: `.work/G-03-r1/windows-probe.log`, `.work/G-03-r1/linux-probe.log`. diff --git a/tasks/reviews/G-03-r2.md b/tasks/reviews/G-03-r2.md new file mode 100644 index 0000000..71f7325 --- /dev/null +++ b/tasks/reviews/G-03-r2.md @@ -0,0 +1,3 @@ +FAIL + +- apps/ely-tree/src/git.rs:86 — `born()` accepts an absent symbolic reference outside `refs/heads/` as an unborn branch, still hiding real Git errors (rule 2). Reproduction on native Windows and WSL Linux: initialize a repository, then run `git symbolic-ref HEAD refs/tags/missing`. `rev-parse --verify -q HEAD` exits 1, `symbolic-ref -q HEAD` succeeds, and `for-each-ref` finds nothing; `born()` returns `Ok(false)` and both reflog and LFS return `Ok([])`, although `git reflog show HEAD --` fails with `fatal: bad revision 'HEAD'`. Require the resolved symbolic target to be a branch under `refs/heads/` before accepting it as unborn; otherwise surface Git's diagnostic, and add a regression for this case. Evidence: `.work/G-03-r2/windows-probe.log`, `.work/G-03-r2/linux-probe.log`. diff --git a/tasks/reviews/G-03-r3.md b/tasks/reviews/G-03-r3.md new file mode 100644 index 0000000..f4f8f40 --- /dev/null +++ b/tasks/reviews/G-03-r3.md @@ -0,0 +1,3 @@ +FAIL + +- apps/ely-tree/src/git.rs:84 — `.trim()` changes valid branch names before checking whether their refs exist, so a valid unborn HEAD still produces repository-page errors (TREE-01, TREE-12, TREE-13). Reproduced on native Windows and WSL Linux: in a committed repository, create branch `topic`, then run `git checkout --orphan` with the name `topic` followed by U+00A0 (non-breaking space). Git accepts the name; `for-each-ref` for its exact `refs/heads/topic` target succeeds silently with no results. `born()` instead queries the existing `refs/heads/topic` and returns an error, causing both reflog and LFS to return errors instead of empty lists. Remove only Git's output line terminator, preserve the ref name, and add a regression covering this valid unborn branch. Evidence: `.work/G-03-r3/windows-probe.log`, `.work/G-03-r3/linux-probe.log`.