ElyTree: an unborn HEAD is told from a broken one (G-03, in review)
born() takes a HEAD as new only when it names a branch under refs/heads/ that no ref holds yet; a broken ref, a tag target or any other state is an error in Git's words. The branch name keeps every byte. Round 3 is fixed; round 4 waits until 0.1.3 ships. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d1373fe16
commit
30801ad24b
5 files changed
+49
-3
No files matched your search
@@ -76,9 +76,25 @@ impl Git {
|
||||
|
||||
/// HEAD names a commit; a new repository has none yet.
|
||||
pub fn born(&self, dir: &Path) -> io::Result<bool> {
|
||||
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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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/<branch>` 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`.
|
||||
@@ -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`.
|
||||
@@ -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<U+00A0>` 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`.
|
||||
Reference in new issue
Block a user