From 185e304751edf8fe08f4d9ff786bae578982dd85 Mon Sep 17 00:00:00 2001 From: Codex GPT-6 Date: Tue, 29 Sep 2026 20:58:42 +0200 Subject: [PATCH 01/28] fix(gix-tix): validate undo changes before updating rebase references A trusted rebase document can intentionally describe reference edits beyond those shown by its generated body. Exposed that edits to the undo queue itself were rejected only after those refs and other refs had already changed, destroying the very history needed to recover. Validate the undo change set before applying the checked, non-dereferencing reference transaction, then record its actual applied changes. Keep the complete document trusted: tag and remote-reference edits remain possible and are recorded for undo. Only unrecordable changes fail before any reference is written. Regression coverage applies edited state anchors to both queue refs and verifies that the queue and other refs survive. A companion test applies explicit tag/remote deletions and restores them through undo. The prior implementation fails the reference-preservation assertion. Git reference: `Documentation/git-update-ref.adoc` at `d38352cd43ab` describes checked reference updates; the Tix undo queue is an additional local invariant. Review correction: a `MustNotExist` update can succeed as a no-op if the ref already points at the requested target. The regression now models creation while editing the document and verifies undo does not delete the existing ref. Validation: all 134 edit tests pass with SHA-1 enabled. --- gix-tix/spec.md | 7 +++ gix-tix/src/edit/rebase.rs | 3 + gix-tix/src/edit/todo.rs | 122 +++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 32c31cdccdf..28de3d83533 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -1907,6 +1907,13 @@ views. state comment follows the complete help at the end of the document. Bottom-up todos use `tix-rebase-state-v3`; older state versions are rejected rather than interpreted with the opposite command order. +- The complete todo, including its state comment, is trusted input. Explicit + state edits may change refs omitted from the generated editable plan, and + those changes participate in undo. Before writing refs, rebase validates the + complete undo change set; attempts to edit the undo queue itself fail without + changing any refs or discarding prior undo history. Undo records the applied + transaction's previous values, so a creation that finds the requested target + already present cannot cause undo to delete that existing ref. - Standalone `(ref, ref)` lines place direct mutable refs at the following fork separator or command result below them. Multiple consecutive lines share that destination. When multiple stacks share a fork destination, its mutable refs diff --git a/gix-tix/src/edit/rebase.rs b/gix-tix/src/edit/rebase.rs index ecfed778d83..b697a2b3362 100644 --- a/gix-tix/src/edit/rebase.rs +++ b/gix-tix/src/edit/rebase.rs @@ -3908,6 +3908,9 @@ fn update_refs( if edits.is_empty() { return Ok(UpdatedRefs::default()); } + // Validate recordability before changing refs, but record the actual previous values: + // a requested creation can be a no-op when the ref already has the requested target. + super::undo::changes_from_edits(edits.iter().cloned())?; let mut time = gix::date::parse::TimeBuf::default(); let applied = repo .edit_references_as(edits, Some(committer.to_ref(&mut time))) diff --git a/gix-tix/src/edit/todo.rs b/gix-tix/src/edit/todo.rs index 0d49d67d20d..7a82f65fca5 100644 --- a/gix-tix/src/edit/todo.rs +++ b/gix-tix/src/edit/todo.rs @@ -2318,6 +2318,128 @@ mod tests { Ok(()) } + #[test] + fn undo_queue_edits_are_rejected_before_changing_any_references() -> TestResult { + use super::super::undo::{self, RefChange, State}; + + for queue_ref in [undo::TIP_REF, undo::CURSOR_REF] { + let (_fixture, repo) = repo()?; + let (base_id, _, tip_id, commits) = commits(&repo)?; + let tag: gix::refs::FullName = "refs/tags/kept".try_into()?; + repo.reference( + tag.clone(), + base_id, + gix::refs::transaction::PreviousValue::MustNotExist, + "create a tag before rebasing", + )?; + undo::record( + &repo, + "create a tag", + &[RefChange { + name: tag.clone(), + before: State::Missing, + after: State::Object(base_id), + }], + )?; + let queue_id = repo.find_reference(queue_ref)?.id().detach(); + let position = undo::position(&repo)?; + let prepared = prepare_test(&repo, base_id, base_id, &commits, Some(tip_id))?; + let document = String::from_utf8(prepared.document)?.replacen( + "edit-refs true\n", + &format!( + "edit-refs true\nref {base_id} {base_id} false true {tag}\nref {queue_id} {base_id} false true {queue_ref}\n" + ), + 1, + ); + let plan = parse_plan(&repo, document.as_bytes())?; + let graph = super::super::loaded_graph(&repo)?; + let err = rebase::perform_plan(&repo, &graph, plan) + .and_then(rebase::PlanPerform::complete) + .err() + .expect("the undo queue cannot be part of its own change set"); + assert!(format!("{err:#}").contains("the undo queue cannot record itself")); + assert_eq!( + repo.try_find_reference(tag.as_ref())? + .map(|reference| reference.id().detach()), + Some(base_id), + "a rejected undo change must leave every reference untouched" + ); + assert_eq!( + repo.find_reference(queue_ref)?.id(), + queue_id, + "validation must preserve the existing undo queue" + ); + assert_eq!(undo::position(&repo)?, position, "prior undo history remains usable"); + } + Ok(()) + } + + #[test] + fn trusted_state_ref_edits_remain_undoable() -> TestResult { + use super::super::undo; + use std::fmt::Write as _; + + let (_fixture, repo) = repo()?; + let (base_id, _, tip_id, commits) = commits(&repo)?; + let refs = ["refs/tags/kept", "refs/remotes/origin/kept"]; + let existing = "refs/heads/already-there"; + for name in refs { + repo.reference( + name, + base_id, + gix::refs::transaction::PreviousValue::MustNotExist, + "create a reference before rebasing", + )?; + } + let prepared = prepare_test(&repo, base_id, base_id, &commits, Some(tip_id))?; + repo.reference( + existing, + tip_id, + gix::refs::transaction::PreviousValue::MustNotExist, + "create a reference while the document is being edited", + )?; + let mut state = String::from("edit-refs true\n"); + for name in refs { + writeln!(state, "ref {base_id} {base_id} false true {name}")?; + } + writeln!(state, "ref - {tip_id} false true {existing}")?; + let document = String::from_utf8(prepared.document)?.replacen("edit-refs true\n", &state, 1); + let document = format!("(already-there)\n{document}"); + let plan = parse_plan(&repo, document.as_bytes())?; + let graph = super::super::loaded_graph(&repo)?; + let outcome = rebase::perform_plan(&repo, &graph, plan)?.complete()?; + assert!( + outcome + .ref_changes + .iter() + .all(|change| change.name.as_bstr() != existing), + "an existing reference at the requested target is a no-op, even when the document expects it missing" + ); + undo::record(&repo, "apply a trusted rebase document", &outcome.ref_changes)?; + for name in refs { + assert!( + repo.try_find_reference(name)?.is_none(), + "the document controls its recorded refs" + ); + } + undo::plan_undo(&repo)? + .expect("the reference changes were recorded") + .apply(&repo)?; + for name in refs { + assert_eq!( + repo.find_reference(name)?.id(), + base_id, + "undo restores each deleted reference" + ); + } + assert_eq!( + repo.find_reference(existing)?.id(), + tip_id, + "undo must not delete a reference that the rebase did not create" + ); + Ok(()) + } + #[test] fn markdown_flows_from_tip_to_base_and_uses_repository_abbreviations() -> TestResult { let (_fixture, repo) = repo()?; From 7a87bba26a5879d4d1774c73f04e4e13bc23ee0f Mon Sep 17 00:00:00 2001 From: Codex GPT-6 Date: Tue, 29 Sep 2026 21:03:27 +0200 Subject: [PATCH 02/28] fix(gix-tix): reject checkout branch names that look like options A cloned repository may contain a local branch named `-f`. Returning through its symbolic Tix pin passed that name to `git checkout` as an option, forcing away uncommitted work while reporting a successful branch return. Reject leading-dash branch operands in `checkout_branch()` and route symbolic pin checkouts through that same guard. Existing checkout failure handling preserves the departure and return pin. A `--` separator would instead change the operand into a pathspec and would not perform the intended branch checkout. An isolated regression starts from `refs/heads/-f`, travels to an ancestor, changes a tracked file, and returns through the HEAD pin. Before the fix it reports success; afterwards it refuses the option and preserves the file, detached HEAD and pin. Explicit branch checkout shares the validation. Git reference: `refs.c::check_branch_ref()` at `d38352cd43ab` rejects leading `-` in porcelain branch names, although plumbing can still create such refs. Validation: all 21 time-travel tests pass with SHA-1 enabled. --- gix-tix/spec.md | 3 ++ gix-tix/src/edit/time_travel.rs | 68 ++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 28de3d83533..3571bd49e3c 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -989,6 +989,9 @@ selection, and submission behavior. acceptance saves them before materializing conflicts. Failure restores the original references and checkout before applying saved departure changes. Failed restoration retains the complete stash and reports its recovery ref. +- Branch checkouts, including returns through symbolic pins, reject local + branch names beginning with `-` before invoking Git, so repository-derived + names cannot become checkout options that discard local changes. - `a h` is available while `HEAD` is detached with a valid symbolic HEAD pin. It atomically moves the remembered local branch to the current `HEAD` commit and attaches `HEAD` without changing the index or worktree. The symbolic HEAD diff --git a/gix-tix/src/edit/time_travel.rs b/gix-tix/src/edit/time_travel.rs index 4fe7d4b8b56..6f65930cd4a 100644 --- a/gix-tix/src/edit/time_travel.rs +++ b/gix-tix/src/edit/time_travel.rs @@ -1499,6 +1499,9 @@ fn checkout_branch(workdir: &Path, name: &gix::refs::FullNameRef) -> Result<()> .as_bstr() .strip_prefix(b"refs/heads/") .ok_or_raise(|| message("the rebase checkout target is not a local branch"))?; + if branch.starts_with(b"-") { + bail!("cannot check out a branch whose name starts with '-'"); + } checkout( workdir, [ @@ -1530,19 +1533,7 @@ fn checkout_reference( fn checkout_pin(workdir: &Path, pin: &history::Pin) -> Result<()> { match pin.target.try_name() { - Some(name) => { - let branch = name - .as_bstr() - .strip_prefix(b"refs/heads/") - .ok_or_raise(|| message("a symbolic tix pin does not point to a local branch"))?; - checkout( - workdir, - [ - OsString::from("--no-guess"), - gix::path::from_bstr(branch.as_bstr()).into_owned().into_os_string(), - ], - ) - } + Some(name) => checkout_branch(workdir, name), None => checkout_detached(workdir, pin.id), } } @@ -2313,6 +2304,57 @@ mod tests { Ok(()) } + #[test] + fn option_like_branch_names_preserve_uncommitted_work_and_the_return_pin() -> TestResult { + if gix_testtools::run_in_isolated_process()? { + return Ok(()); + } + let fixture = gix_testtools::scripted_fixture_writable("rebase_edit.sh")?; + let repository = crate::test_repository::open(fixture.path())?; + let repository_path = repository.git_dir().to_owned(); + let base_id = repository.rev_parse_single("HEAD~2")?.detach(); + let tip_id = repository.head_id()?.detach(); + let branch: gix::refs::FullName = "refs/heads/-f".try_into()?; + repository.reference(branch.clone(), tip_id, PreviousValue::MustNotExist, "test branch")?; + repository.edit_reference(RefEdit::update( + "HEAD".try_into()?, + branch.clone(), + PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?)), + "test the branch created by a hostile clone", + ))?; + drop(repository); + + move_head_to(&repository_path, false, base_id, None, &[], false, Some)?; + let path = fixture.path().join("base"); + std::fs::write(&path, "uncommitted work\n")?; + let err = move_head_to(&repository_path, false, tip_id, None, &[], false, Some) + .expect_err("a branch name must not become a checkout option"); + assert!(format!("{err:#}").contains("starts with '-'"), "{err:#}"); + assert_eq!( + std::fs::read(&path)?, + b"uncommitted work\n", + "checkout must preserve local changes" + ); + let repository = crate::test_repository::open(fixture.path())?; + assert_eq!( + repository.head_id()?, + base_id, + "failed checkout leaves HEAD at its departure" + ); + assert!(repository.head()?.is_detached(), "failed checkout does not attach HEAD"); + assert!( + history::all_pins(&repository)? + .iter() + .any(|pin| pin.is_head() && pin.target == Target::Symbolic(branch.clone())), + "failed checkout keeps the return pin" + ); + assert!( + checkout_branch(fixture.path(), branch.as_ref()).is_err(), + "explicit branch checkout uses the same guard" + ); + Ok(()) + } + #[test] fn returning_to_a_commit_restores_its_manual_stash() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("history.sh")?; From ca3ce41d85be9f1a338d6a2c9f517442462de972 Mon Sep 17 00:00:00 2001 From: Byron Date: Wed, 30 Sep 2026 05:31:27 +0200 Subject: [PATCH 03/28] feat(gix-quote): add reusable display quoting Display quoting was private to `gitoxide-core`, preventing other crates from reusing its established output behavior. Expose it as `gix_quote::for_display()` and migrate the existing index-output callers. The helper returns borrowed display bytes while retaining the reusable scratch buffer. Ordinary Unicode stays readable; control characters, quotes, backslashes, and invalid UTF-8 retain their lossless debug representation. Keep the existing byte-quoting regression coverage with the shared helper. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra --- gitoxide-core/src/lib.rs | 2 - gitoxide-core/src/output.rs | 56 ------------------- gitoxide-core/src/repository/index/entries.rs | 4 +- gix-quote/src/display.rs | 22 ++++++++ gix-quote/src/lib.rs | 3 + gix-quote/tests/quote.rs | 36 ++++++++++++ 6 files changed, 63 insertions(+), 60 deletions(-) delete mode 100644 gitoxide-core/src/output.rs create mode 100644 gix-quote/src/display.rs diff --git a/gitoxide-core/src/lib.rs b/gitoxide-core/src/lib.rs index 961eb854a79..4495c5c5a60 100644 --- a/gitoxide-core/src/lib.rs +++ b/gitoxide-core/src/lib.rs @@ -88,8 +88,6 @@ pub mod repository; #[cfg(feature = "tracing")] pub mod trace; -mod output; - mod discover; pub use discover::discover; diff --git a/gitoxide-core/src/output.rs b/gitoxide-core/src/output.rs deleted file mode 100644 index c2ed0b2a6b4..00000000000 --- a/gitoxide-core/src/output.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::io::Write; - -use gix::{bstr::BStr, utils::AsBStr}; - -/// Write arbitrary bytes losslessly, quoting the entire value with its debug representation if it contains -/// control characters, quotes, backslashes, or invalid UTF-8 that must be escaped. -pub(crate) fn write_bstr(mut out: impl Write, input: &BStr, scratch: &mut Vec) -> std::io::Result<()> { - scratch.clear(); - write!(scratch, "{input:?}").expect("writing to a byte buffer cannot fail"); - let debug_matches_input = scratch - .strip_prefix(b"\"") - .and_then(|debug| debug.strip_suffix(b"\"")) - .is_some_and(|debug| debug == input); - let input = if debug_matches_input { input } else { scratch.as_bstr() }; - out.write_all(input) -} - -#[cfg(test)] -mod tests { - use gix::bstr::ByteSlice; - - use super::write_bstr; - - fn render(input: &[u8], scratch: &mut Vec) -> Vec { - let mut out = Vec::new(); - write_bstr(&mut out, input.as_bstr(), scratch).expect("in-memory writes succeed"); - out - } - - #[test] - fn safe_ascii_and_unicode_remain_unchanged() { - let mut scratch = Vec::new(); - for input in [b"hello world".as_slice(), "hello 💡".as_bytes()] { - assert_eq!( - render(input, &mut scratch), - input, - "safe text should remain easy to read" - ); - } - } - - #[test] - fn terminal_controls_and_ambiguous_bytes_are_quoted_losslessly() { - let mut scratch = Vec::new(); - assert_eq!( - render(b"control-\0\x08\t\n\r\x1b\x7f\"\\end", &mut scratch), - br#""control-\0\x08\t\n\r\x1b\x7f\"\\end""#, - "terminal-active and syntax bytes must be escaped" - ); - assert_eq!( - render(b"invalid-\xff", &mut scratch), - br#""invalid-\xff""#, - "invalid UTF-8 must remain recoverable" - ); - } -} diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 59ed5103d16..9ebaf7de797 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -383,7 +383,7 @@ pub(crate) mod function { path: &BStr, buf: &mut Vec, ) -> std::io::Result<()> { - crate::output::write_bstr(&mut *out, path, buf)?; + out.write_all(gix::quote::for_display(path, buf))?; match attrs { Some(attrs) => out.write_all(print_attrs(Some(attrs), entry.mode).as_bytes()), None => Ok(()), @@ -415,7 +415,7 @@ pub(crate) mod function { entry.mode, entry.id, )?; - crate::output::write_bstr(&mut *out, path, buf)?; + out.write_all(gix::quote::for_display(path, buf))?; out.write_all(print_attrs(attrs, entry.mode).as_bytes())?; out.write_all(b"\n") } diff --git a/gix-quote/src/display.rs b/gix-quote/src/display.rs new file mode 100644 index 00000000000..8fd751e4a90 --- /dev/null +++ b/gix-quote/src/display.rs @@ -0,0 +1,22 @@ +use std::io::Write; + +use bstr::{BStr, ByteSlice}; + +/// Return arbitrary bytes as text safe for display, quoting the entire value with its debug +/// representation if it contains control characters, quotes, backslashes, or invalid UTF-8. +/// +/// Examples (input bytes in Rust syntax → displayed text): +/// - `b"hello world"` → `hello world` (no escaping). +/// - `b"hello\nworld"` → `"hello\nworld"` (newline escaped and the entire value quoted). +/// +/// Safe text borrows `input`; quoted text borrows the reusable `scratch` buffer, which is cleared +/// on each call. This quoting is for display, not for passing arguments to a shell. +pub fn for_display<'a>(input: &'a BStr, scratch: &'a mut Vec) -> &'a BStr { + scratch.clear(); + write!(scratch, "{input:?}").expect("writing to a byte buffer cannot fail"); + let debug_matches_input = scratch + .strip_prefix(b"\"") + .and_then(|debug| debug.strip_suffix(b"\"")) + .is_some_and(|debug| debug == input); + if debug_matches_input { input } else { scratch.as_bstr() } +} diff --git a/gix-quote/src/lib.rs b/gix-quote/src/lib.rs index 6a7518e18cf..285a9f22b7f 100644 --- a/gix-quote/src/lib.rs +++ b/gix-quote/src/lib.rs @@ -21,3 +21,6 @@ pub mod ansi_c; mod single; pub use single::single; + +mod display; +pub use display::for_display; diff --git a/gix-quote/tests/quote.rs b/gix-quote/tests/quote.rs index de4c9b1388e..0e95de01d9b 100644 --- a/gix-quote/tests/quote.rs +++ b/gix-quote/tests/quote.rs @@ -34,6 +34,42 @@ mod single { } } +mod for_display { + use bstr::ByteSlice; + use gix_quote::for_display; + + fn render(input: &[u8], scratch: &mut Vec) -> Vec { + for_display(input.as_bstr(), scratch).to_vec() + } + + #[test] + fn safe_ascii_and_unicode_remain_unchanged() { + let mut scratch = Vec::new(); + for input in [b"hello world".as_slice(), "hello 💡".as_bytes()] { + assert_eq!( + render(input, &mut scratch), + input, + "safe text should remain easy to read" + ); + } + } + + #[test] + fn terminal_controls_and_ambiguous_bytes_are_quoted_losslessly() { + let mut scratch = Vec::new(); + assert_eq!( + render(b"control-\0\x08\t\n\r\x1b\x7f\"\\end", &mut scratch), + br#""control-\0\x08\t\n\r\x1b\x7f\"\\end""#, + "terminal-active and syntax bytes must be escaped" + ); + assert_eq!( + render(b"invalid-\xff", &mut scratch), + br#""invalid-\xff""#, + "invalid UTF-8 must remain recoverable" + ); + } +} + mod ansi_c { mod quote { use bstr::ByteSlice; From e7593a8b51de4b49fb335b36f9e8d1b3b0af2664 Mon Sep 17 00:00:00 2001 From: Codex GPT-6 Date: Tue, 29 Sep 2026 21:06:31 +0200 Subject: [PATCH 04/28] fix(gix-tix): quote terminal controls in history and rebase metadata Repository-derived commit titles and authors reached plain history and rebase todo output with terminal control sequences intact. A cloned commit or mailmap could therefore overwrite output or manipulate the terminal. Use `gix::quote::for_display()` after rendering plain metadata, including mailmapped authors and rebase anchor titles. Keep todo commands and machine-readable reference state intact, and document the display behavior in the Tix specification. Regressions cover ESC, BEL and Unicode control sequences in metadata and anchor titles. Todo parsing continues to accept quoted informational metadata. Git baseline: `pager.c::setup_pager()` at `d38352cd43ab` routes terminal output through a pager; Tix's plain stdout paths instead need explicit quoting. The tree after this commit matches the previously validated sanitizer fix, whose complete 461-test Tix suite passed with isolated Git configuration. The two terminal-control regressions also pass independently with `cargo test -p gix-tix --lib --features gix/sha1 terminal_controls`. --- gix-tix/spec.md | 14 +++++++----- gix-tix/src/command.rs | 3 ++- gix-tix/src/edit/todo.rs | 25 ++++++++++++++++++---- gix-tix/src/ui.rs | 46 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 3571bd49e3c..8820b4fd54a 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -131,6 +131,10 @@ without trading responsiveness for metadata that is not visible. seven-character commit hash is followed by its seven-character reverse-hex change ID. Colliding or duplicated prefixes remain visible and receive a `💥` gutter marker. +- Plain history and rebase-todo metadata quote control characters, quotes, + and backslashes for display. This includes commit titles, + author names from commits or mailmap, and todo anchor titles. Quoting does not + alter the todo's commands or reference state. - `tix travel [--stash] [--materialize-conflicts] (REVSPEC | --to first|parent|child|tip)` performs the same detached checkout, pending-rebase replay, stash handling, and pin reconciliation as TUI time travel. Plain travel carries local changes; @@ -1828,14 +1832,14 @@ views. IDs are shortened through repository configuration; metadata is loaded across the complete todo scope, repeats the full information visible in history, and always includes the subject. Base-level - stacks end with `fork (base) ` in the separator, using the title - exactly as displayed in history without Markdown escaping. Fork points within the editable tree + stacks end with `fork <id> (base) <title>` in the separator, with the title + quoted for display without Markdown escaping. Fork points within the editable tree remain plain `fork <id>` separators. Every separator is centered with at least four `─` characters per side, and all span the widest editable line. - When that boundary shows `⇣N`, `a u` opens the same editor with each base-level stack rooted at the corresponding hidden branch tip. Its otherwise unfamiliar - separator is `fork <id> (updated-base) <title>`, with the raw title exactly as - shown in history, including `[A]` and `[N]`. The hidden branch + separator is `fork <id> (updated-base) <title>`, with the display-quoted title + including `[A]` and `[N]`. The hidden branch itself is not moved. - `merge <source> <side-parent>…` replays an ordinary merge. The surrounding fork supplies its first parent; side parents are ordered commit IDs and can refer to @@ -1852,7 +1856,7 @@ views. adding and removing separators creates and joins branches. `empty <title>` inserts an empty commit. Markdown code spans and equivalent plain commands are accepted; display text after an ID is - informational and emitted verbatim without Markdown escaping. + informational and uses display quoting without Markdown escaping. - Fold groups are materialized eagerly on every fork by applying their source deltas in bottom-to-top todo order. The result retains the first member's author, author time, encoding, and extra headers, starts with its message, receives the operation's committer, diff --git a/gix-tix/src/command.rs b/gix-tix/src/command.rs index bc3262c40f4..c8c81013f08 100644 --- a/gix-tix/src/command.rs +++ b/gix-tix/src/command.rs @@ -3259,7 +3259,8 @@ mod tests { let base = render(&repository, "HEAD")?; assert!( - base.lines().any(|line| line.contains(&format!("base @ {short_head}"))), + base.lines() + .any(|line| line.contains("base @ ") && line.contains(&short_head)), "a HEAD base separator retains the marker: {base:?}" ); Ok(()) diff --git a/gix-tix/src/edit/todo.rs b/gix-tix/src/edit/todo.rs index 7a82f65fca5..866927e070e 100644 --- a/gix-tix/src/edit/todo.rs +++ b/gix-tix/src/edit/todo.rs @@ -801,7 +801,9 @@ fn anchor_title(repo: &gix::Repository, id: ObjectId) -> Result<String> { .summary() .to_str_lossy(), ); - Ok(out) + Ok(gix::quote::for_display(out.as_bytes().as_bstr(), &mut Vec::new()) + .to_str_lossy() + .into_owned()) } fn write_fork_heading( @@ -2440,6 +2442,21 @@ mod tests { Ok(()) } + #[test] + fn anchor_metadata_quotes_terminal_controls() -> gix_testtools::Result { + let (_fixture, repo) = repo()?; + let mut commit = repo.find_commit(repo.head_id()?)?.decode()?.into_owned()?; + commit.message = b"anchor\x1b]0;spoof\x07".as_slice().into(); + let commit_id = repo.write_object(&commit)?.detach(); + let title = anchor_title(&repo, commit_id)?; + assert!( + !title.chars().any(char::is_control), + "todo anchors must not emit terminal controls: {title:?}" + ); + assert!(title.contains("\\x1b"), "the escaped anchor title remains readable"); + Ok(()) + } + #[test] fn markdown_flows_from_tip_to_base_and_uses_repository_abbreviations() -> TestResult { let (_fixture, repo) = repo()?; @@ -2975,13 +2992,13 @@ mod tests { ); assert!( document.contains(&format!( - "fork {} (updated-base) [A] [N] updated * _ [hidden] <base> `raw` \\ base", + r#"fork {} (updated-base) "[A] [N] updated * _ [hidden] <base> `raw` \\ base""#, crate::change_id::display_short(&repo, onto)? )), - "the unfamiliar fork target carries its raw UI title" + "the unfamiliar fork target carries its safely quoted title" ); assert_eq!( - document.matches("updated * _ [hidden] <base> `raw` \\ base").count(), + document.matches(r"updated * _ [hidden] <base> `raw` \\ base").count(), 1, "only the new update target is labelled" ); diff --git a/gix-tix/src/ui.rs b/gix-tix/src/ui.rs index b23eebe9d61..d1a1cbae739 100644 --- a/gix-tix/src/ui.rs +++ b/gix-tix/src/ui.rs @@ -3084,7 +3084,10 @@ pub(crate) fn plain_history_metadata( line.spans .insert(1, Span::raw(format!(" {}", change_id.to_reverse_hex_with_len(7)))); } - line.spans.into_iter().map(|span| span.content.into_owned()).collect() + let text: String = line.spans.into_iter().map(|span| span.content.into_owned()).collect(); + gix::quote::for_display(text.as_bytes().as_bstr(), &mut Vec::new()) + .to_str_lossy() + .into_owned() } pub(crate) fn todo_metadata(app: &App, row: &CommitRow, mailmap: &gix::mailmap::Snapshot) -> String { @@ -3130,7 +3133,9 @@ pub(crate) fn todo_metadata(app: &App, row: &CommitRow, mailmap: &gix::mailmap:: .collect::<String>(); out.push_str(&rendered); } - out + gix::quote::for_display(out.as_bytes().as_bstr(), &mut Vec::new()) + .to_str_lossy() + .into_owned() } fn author_label( @@ -5524,6 +5529,43 @@ mod tests { Ok(()) } + #[test] + fn plain_metadata_quotes_terminal_controls_in_titles_and_authors() { + let commit_id = gix::ObjectId::Sha1([1; 20]); + let mut app = App::new(1); + app.extend_commits(vec![Commit { + id: commit_id, + parent_ids: Default::default(), + author_time: gix::date::Time::default(), + committer_time: gix::date::Time::default(), + author: author(b"author\x1b[31m", b"author@example.com"), + attributions: 0..0, + title: "subject\u{1b}]0;spoof\u{7}\u{9b}2K".into(), + metadata_loaded: true, + has_agent_marker: false, + has_merge_replay: false, + is_review: false, + signature: SignatureState::Unsigned, + }]); + let mailmap = gix::mailmap::Snapshot::from_bytes(b"mapped\x1b[32m <author@example.com>\n"); + for use_mailmap in [false, true] { + app.use_mailmap = use_mailmap; + for text in [ + plain_history_metadata(&app, &app.rows[0], &Decorations::new(), &mailmap, false, None), + todo_metadata(&app, &app.rows[0], &mailmap), + ] { + assert!( + !text.chars().any(char::is_control), + "plain metadata must not contain terminal controls: {text:?}" + ); + assert!( + text.contains("\\x1b"), + "escape bytes remain visible as quoted text: {text}" + ); + } + } + } + #[test] fn advertises_travel_and_return_for_non_head_rows() -> Result<(), Box<dyn std::error::Error>> { let selected = gix::ObjectId::Sha1([1; 20]); From 4e1d3162624cd25c2fbcd25f11e043aae840c43e Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:15:11 +0200 Subject: [PATCH 05/28] fix(gix-command): keep Windows command lookup within PATH <!-- agent --> A report found that an inherited-PATH miss left a bare program name for shebang probing. That opened a same-named file in the current worktree and allowed its contents to choose the interpreter, instead of reporting a missing program. A successful lookup could also be discarded before spawning. Anchor every unresolved bare command before returning, without probing it as a script, and execute the resolved path on successful lookup. Explicit script paths retain their shebang behavior. Existing argument-parsing assertions now account for resolved program names on Windows. Git reference: `compat/mingw.c::path_lookup()` and `mingw_spawnvpe()` at `d38352cd43ab` fail lookup before parsing an interpreter and execute the resolved path. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gix-command/src/prepare.rs | 82 +++++++++++++++---- gix-command/tests/command/prepare.rs | 21 +++-- .../tests/program/from_custom_definition.rs | 50 ++++++++--- gix-diff/tests/diff/blob/platform.rs | 11 ++- gix-transport/src/client/blocking_io/file.rs | 9 +- .../src/client/blocking_io/ssh/tests.rs | 43 +++++----- 6 files changed, 161 insertions(+), 55 deletions(-) diff --git a/gix-command/src/prepare.rs b/gix-command/src/prepare.rs index a15089d6a5e..34719cde4e2 100644 --- a/gix-command/src/prepare.rs +++ b/gix-command/src/prepare.rs @@ -296,7 +296,7 @@ impl From<Prepare> for Command { /// /// The last `PATH` in `inline_env` overrides the last one in `env`, which overrides the inherited value (of this process). /// The selected `PATH` is searched in order. A resolved shebang script is launched through its interpreter, ignoring shebang -/// arguments. If an explicit `PATH` does not resolve a bare command, the missing program remains anchored in its first entry +/// arguments. If `PATH` does not resolve a bare command, the missing program remains anchored in its first entry /// so Rust's broader Windows lookup cannot find it elsewhere. fn windows_command(command: OsString, env: &[(OsString, OsString)], inline_env: &[(String, OsString)]) -> Command { let explicit_joined_paths = inline_env @@ -316,15 +316,17 @@ fn windows_command(command: OsString, env: &[(OsString, OsString)], inline_env: let looked_up = joined_paths .as_deref() .and_then(|joined_paths| win_path_lookup(command.as_ref(), joined_paths)); - let program: Cow<'_, Path> = match (looked_up, explicit_joined_paths) { + let program: Cow<'_, Path> = match looked_up { // Use the manually resolved path. - (Some(program), _) => Cow::Owned(program), - // An explicit `PATH` miss must not fall back to `std::process::Command` broader Windows search. - (None, Some(explicit_joined_paths)) if is_bare_command(Path::new(&command)) => { - Cow::Owned(prevent_further_path_lookup(command.as_ref(), explicit_joined_paths)) + Some(program) => Cow::Owned(program), + // A bare PATH miss must neither probe a worktree file nor use Rust's broader Windows search. + None if is_bare_command(Path::new(&command)) => { + return Command::new(prevent_further_path_lookup( + command.as_ref(), + joined_paths.as_deref().unwrap_or_else(|| OsStr::new("")), + )); } - // Preserve non-bare commands and let `std::process::Command` resolve bare commands without an explicit `PATH`. - (None, _) => Cow::Borrowed(command.as_ref()), + None => Cow::Borrowed(command.as_ref()), }; if let Some(shebang) = extract_interpreter(program.as_ref()) { let mut cmd = Command::new(shebang.interpreter); @@ -332,18 +334,13 @@ fn windows_command(command: OsString, env: &[(OsString, OsString)], inline_env: cmd.arg(program.as_ref()); cmd } else { - match program { - // Process lookup happens before the child's environment is installed, so an explicitly - // configured PATH must be handled here for ordinary executables as well. - Cow::Owned(program) if explicit_joined_paths.is_some() => Command::new(program), - _ => Command::new(command), - } + Command::new(program.as_ref()) } } -/// Represent the failed lookup of `command` in an explicitly assigned `PATH` without permitting another search. +/// Represent the failed lookup of `command` in `PATH` without permitting another search. /// -/// `joined_paths` is the complete value of the explicit `PATH`, not one of its entries. The first non-empty entry is +/// `joined_paths` is the complete value of `PATH`, not one of its entries. The first non-empty entry is /// joined with `command`, producing a path that Rust's Windows resolver will not look up elsewhere. If there is no such /// entry, a trailing separator makes `command` invalid instead. fn prevent_further_path_lookup(command: &Path, joined_paths: &OsStr) -> PathBuf { @@ -362,6 +359,59 @@ fn prevent_further_path_lookup(command: &Path, joined_paths: &OsStr) -> PathBuf mod tests { use super::*; + #[test] + fn inherited_path_is_used_for_both_probing_and_execution() -> gix_testtools::Result { + if gix_testtools::run_in_isolated_process()? { + return Ok(()); + } + let root = gix_testtools::tempfile::tempdir()?; + let bin = root.path().join("bin"); + std::fs::create_dir(&bin)?; + let _cwd = gix_testtools::set_current_dir(root.path())?; + std::fs::write("ssh", b"#!/untrusted/interpreter\n")?; + + for path in [Some(bin.to_str().expect("temporary path is UTF-8")), Some(""), None] { + let environment = gix_testtools::Env::new(); + let _environment = match path { + Some(path) => environment.set("PATH", path), + None => environment.unset("PATH"), + }; + let mut cmd = windows_command("ssh".into(), &[], &[]); + let expected = match path { + Some(path) if !path.is_empty() => bin.join("ssh"), + _ => Path::new("ssh").join(""), + }; + assert_eq!( + cmd.get_program(), + expected, + "a PATH miss cannot probe a worktree file or leave a bare command for another lookup" + ); + assert!(cmd.get_args().next().is_none(), "the planted shebang is never used"); + assert!(cmd.spawn().is_err(), "a missing program fails to spawn"); + } + + let _environment = gix_testtools::Env::new().set("PATH", bin.to_str().expect("temporary path is UTF-8")); + let executable = bin.join("ssh.exe"); + std::fs::write(&executable, b"executable placeholder")?; + assert_eq!( + windows_command("ssh".into(), &[], &[]).get_program(), + executable, + "the executable selected from PATH is also the one passed to the process launcher" + ); + let explicit_script = windows_command("./ssh".into(), &[], &[]); + assert_eq!( + explicit_script.get_program(), + Path::new("/untrusted/interpreter"), + "an explicitly requested script retains shebang support" + ); + assert_eq!( + explicit_script.get_args().collect::<Vec<_>>(), + [OsStr::new("./ssh")], + "the interpreter receives the explicitly requested script" + ); + Ok(()) + } + #[test] fn explicit_path_lookup_failure_stays_within_that_path() -> gix_testtools::Result { let joined_paths = std::env::join_paths(["", "not/a/real/path", "also/not/real"])?; diff --git a/gix-command/tests/command/prepare.rs b/gix-command/tests/command/prepare.rs index 388c5a9d50a..ec716770d82 100644 --- a/gix-command/tests/command/prepare.rs +++ b/gix-command/tests/command/prepare.rs @@ -14,7 +14,14 @@ fn default_shell() -> &'static str { const SH_BASENAME: &str = if cfg!(windows) { "sh.exe" } else { "sh" }; fn quoted(input: &[&str]) -> String { - input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ") + // These assertions cover argument parsing. Windows resolves the program before spawning, so + // compare against the same program prepared without any argument splitting. + let (program, args) = input.split_first().expect("a command always includes its program"); + let cmd = std::process::Command::from(gix_command::prepare(program)); + std::iter::once(format!("{cmd:?}")) + .chain(args.iter().map(|s| format!("\"{s}\""))) + .collect::<Vec<_>>() + .join(" ") } fn quoted_default_shell(input: &[&str]) -> String { @@ -43,7 +50,7 @@ fn empty() { #[test] fn whitespace_only_without_shell() { let cmd = std::process::Command::from(gix_command::prepare(" ")); - assert_eq!(format!("{cmd:?}"), "\" \""); + assert_eq!(format!("{cmd:?}"), quoted(&[" "])); } #[test] @@ -85,7 +92,7 @@ fn shell_assignments_are_applied_during_manual_splitting() { ); assert_eq!( cmd.get_program(), - "command.exe", + std::process::Command::from(gix_command::prepare("command.exe")).get_program(), "non-PATH assignments don't prevent manual splitting" ); assert_eq!(cmd.get_args().collect::<Vec<_>>(), ["arg"], "arguments are retained"); @@ -189,7 +196,11 @@ fn only_unambiguous_shell_assignments_are_applied() { let cmd = std::process::Command::from( gix_command::prepare(input).command_may_be_shell_script_allow_manual_argument_splitting(), ); - assert_eq!(cmd.get_program(), program, "{input:?} is not an assignment prefix"); + assert_eq!( + cmd.get_program(), + std::process::Command::from(gix_command::prepare(program)).get_program(), + "{input:?} is not an assignment prefix" + ); assert_eq!(cmd.get_args().collect::<Vec<_>>(), args, "arguments are retained"); assert_eq!(cmd.get_envs().count(), 0, "the environment is unchanged"); } @@ -320,7 +331,7 @@ fn single_and_complex_arguments_with_auto_split() { ); assert_eq!( format!("{cmd:?}"), - r#""ls" "--foo=a b""#, + quoted(&["ls", "--foo=a b"]), "splitting can also handle quotes" ); } diff --git a/gix-credentials/tests/program/from_custom_definition.rs b/gix-credentials/tests/program/from_custom_definition.rs index b157bf4c3f5..beb0adda9bf 100644 --- a/gix-credentials/tests/program/from_custom_definition.rs +++ b/gix-credentials/tests/program/from_custom_definition.rs @@ -22,11 +22,18 @@ const SH_BASENAME: &str = if cfg!(windows) { "sh.exe" } else { "sh" }; #[test] fn empty() { let prog = Program::from_custom_definition(""); - let git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::path::Path::new(*GIT))); assert!(matches!(&prog.kind, Kind::ExternalName { name_and_args } if name_and_args.is_empty())); + let cmd = prog.to_command(&helper::Action::Store("egal".into())); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{git}" "credential-" "store""#), + std::path::Path::new(cmd.get_program()), + std::path::Path::new( + std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + ), + "Git uses the platform's command resolution" + ); + assert_eq!( + cmd.get_args().collect::<Vec<_>>(), + ["credential-", "store"], "not useful, but allowed, would have to be caught elsewhere" ); } @@ -35,22 +42,36 @@ fn empty() { fn simple_script_in_path() { let prog = Program::from_custom_definition("!exe"); assert!(matches!(&prog.kind, Kind::ExternalShellScript(script) if script == "exe")); + let cmd = prog.to_command(&helper::Action::Store("egal".into())); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - r#""exe" "store""#, + cmd.get_program(), + std::process::Command::from(gix_command::prepare("exe")).get_program(), "it didn't detect anything shell-scripty, and thus doesn't use a shell" ); + assert_eq!( + cmd.get_args().collect::<Vec<_>>(), + ["store"], + "the helper receives its action" + ); } #[test] fn name_with_args() { let input = "name --arg --bar=\"a b\""; let prog = Program::from_custom_definition(input); - let git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::path::Path::new(*GIT))); assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); + let cmd = prog.to_command(&helper::Action::Store("egal".into())); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{git}" "credential-name" "--arg" "--bar=a b" "store""#) + std::path::Path::new(cmd.get_program()), + std::path::Path::new( + std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + ), + "Git uses the platform's command resolution" + ); + assert_eq!( + cmd.get_args().collect::<Vec<_>>(), + ["credential-name", "--arg", "--bar=a b", "store"], + "quoted arguments are split without losing their contents" ); } @@ -74,13 +95,20 @@ fn name_with_special_args() { fn name() { let input = "name"; let prog = Program::from_custom_definition(input); - let git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::path::Path::new(*GIT))); assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); + let cmd = prog.to_command(&helper::Action::Store("egal".into())); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), - format!(r#""{git}" "credential-name" "store""#), + std::path::Path::new(cmd.get_program()), + std::path::Path::new( + std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + ), "we detect that this can run without shell, which is also more portable on windows" ); + assert_eq!( + cmd.get_args().collect::<Vec<_>>(), + ["credential-name", "store"], + "Git receives the helper name and action" + ); } #[test] diff --git a/gix-diff/tests/diff/blob/platform.rs b/gix-diff/tests/diff/blob/platform.rs index 73d7bf90ae6..5f1a7c42c82 100644 --- a/gix-diff/tests/diff/blob/platform.rs +++ b/gix-diff/tests/diff/blob/platform.rs @@ -1,3 +1,5 @@ +use std::process::Command; + use crate::Result; use gix_diff::blob::{Algorithm, Platform, ResourceKind, pipeline, platform, platform::prepare_diff::Operation}; use gix_object::{ @@ -64,6 +66,8 @@ fn resources_of_worktree_and_odb_and_check_link() -> Result { "it ends up with the default, as it's not overridden anywhere" ); + let program = Command::from(gix_diff::command::prepare("test")); + let program = program.get_program().to_string_lossy(); assert_eq!( comparable_ext_diff(platform.prepare_diff_command( "test".into(), @@ -75,7 +79,7 @@ fn resources_of_worktree_and_odb_and_check_link() -> Result { 3 )), format!( - "{}test a <tmp-path> 0000000000000000000000000000000000000000 100644 <tmp-path> {new_id} 100755", + "{}{program} a <tmp-path> 0000000000000000000000000000000000000000 100644 <tmp-path> {new_id} 100755", if !cfg!(windows) { "GIT_DIFF_PATH_COUNTER=3 GIT_DIFF_PATH_TOTAL=3 GIT_DIR=. " } else { @@ -137,7 +141,7 @@ fn resources_of_worktree_and_odb_and_check_link() -> Result { 1 )), format!( - "{}test a <tmp-path> 0000000000000000000000000000000000000000 100644 <tmp-path> {new_id} 120000", + "{}{program} a <tmp-path> 0000000000000000000000000000000000000000 100644 <tmp-path> {new_id} 120000", if !cfg!(windows) { r#"GIT_DIFF_PATH_COUNTER=1 GIT_DIFF_PATH_TOTAL=1 GIT_DIR=. "# } else { @@ -377,6 +381,7 @@ fn source_and_destination_do_not_exist() -> Result { ); insta::assert_debug_snapshot!(err, "source and destination do not exist", @"Tried to diff resources that are both considered removed"); + let program = Command::from(gix_diff::command::prepare("test")); assert_eq!( format!( "{:?}", @@ -393,7 +398,7 @@ fn source_and_destination_do_not_exist() -> Result { .expect("resources set") ), format!( - r#"{}"test" "missing" "/dev/null" "." "." "/dev/null" "." "." "a""#, + r#"{}{program:?} "missing" "/dev/null" "." "." "/dev/null" "." "." "a""#, if !cfg!(windows) { r#"GIT_DIFF_PATH_COUNTER="1" GIT_DIFF_PATH_TOTAL="1" GIT_DIR="." "# } else { diff --git a/gix-transport/src/client/blocking_io/file.rs b/gix-transport/src/client/blocking_io/file.rs index 66767fb8ebc..9ea20b00762 100644 --- a/gix-transport/src/client/blocking_io/file.rs +++ b/gix-transport/src/client/blocking_io/file.rs @@ -484,8 +484,15 @@ mod tests { } fn command_and_args(cmd: gix_command::Prepare) -> Vec<String> { + let program = cmd.command.clone(); let cmd = std::process::Command::from(cmd); - std::iter::once(cmd.get_program()) + let expected_program = std::process::Command::from(gix_command::prepare(&program)); + assert_eq!( + cmd.get_program(), + expected_program.get_program(), + "the selected SSH program follows the platform's command lookup" + ); + std::iter::once(program.as_os_str()) .chain(cmd.get_args()) .map(|arg| arg.to_string_lossy().into_owned()) .collect() diff --git a/gix-transport/src/client/blocking_io/ssh/tests.rs b/gix-transport/src/client/blocking_io/ssh/tests.rs index a97bf6b0dde..d9d4b1d103a 100644 --- a/gix-transport/src/client/blocking_io/ssh/tests.rs +++ b/gix-transport/src/client/blocking_io/ssh/tests.rs @@ -126,7 +126,7 @@ mod program_kind { &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "host"][..], ), ] { - assert_eq!(call_args(ProgramKind::Ssh, url, protocol), joined(expected)); + assert_eq!(call_args(ProgramKind::Ssh, url, protocol), expected); } } @@ -134,14 +134,18 @@ mod program_kind { fn tortoise_plink_has_batch_command() { assert_eq!( call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2), - joined(&["tortoiseplink.exe", "-batch", "-P", "42", "user@host"]) + ["tortoiseplink.exe", "-batch", "-P", "42", "user@host"] ); } #[test] fn port_for_all() { for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] { - assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with("-P 43 user@host")); + assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with(&[ + "-P".into(), + "43".into(), + "user@host".into() + ])); } } @@ -181,7 +185,7 @@ mod program_kind { fn ambiguous_host_is_allowed_with_user_explicit_ssh() { assert_eq!( call_args(ProgramKind::Ssh, "ssh://user@-arg/p", Protocol::V2), - joined(&["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"]) + ["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"] ); } @@ -189,7 +193,7 @@ mod program_kind { fn ambiguous_host_is_allowed_with_user_implicit_ssh() { assert_eq!( call_args(ProgramKind::Ssh, "user@-arg:p/q", Protocol::V2), - joined(&["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"]) + ["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@-arg"] ); } @@ -258,7 +262,7 @@ mod program_kind { ); assert_eq!( call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2), - joined(&["simple", "user@host"]), + ["simple", "user@host"], "simple can only do simple invocations" ); } @@ -296,9 +300,6 @@ mod program_kind { Ok(()) } - fn joined(input: &[&str]) -> String { - input.to_vec().join(" ") - } fn try_call( kind: ProgramKind, url: &str, @@ -311,16 +312,20 @@ mod program_kind { fn call(kind: ProgramKind, url: &str, version: Protocol) -> gix_command::Prepare { try_call(kind, url, version).expect("no error") } - fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> String { - let cmd = std::process::Command::from(call(kind, url, version)); - format!( - "{} {}", - cmd.get_program().to_string_lossy(), - cmd.get_args() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect::<Vec<_>>() - .join(" ") - ) + fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> Vec<String> { + let prepare = call(kind, url, version); + let program = prepare.command.clone(); + let cmd = std::process::Command::from(prepare); + let expected_program = std::process::Command::from(gix_command::prepare(&program)); + assert_eq!( + cmd.get_program(), + expected_program.get_program(), + "the selected SSH program follows the platform's command lookup" + ); + std::iter::once(program.as_os_str()) + .chain(cmd.get_args()) + .map(|arg| arg.to_string_lossy().into_owned()) + .collect() } type Result = std::result::Result<(), ssh::invocation::Error>; From fb4aada65bfda590e4f16bfd979f7fdf9f15e83b Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Wed, 30 Sep 2026 18:24:33 +0200 Subject: [PATCH 06/28] feat(gix-fs)!: add no-follow reads and recursive directory traversal <!-- agent --> Bring file-opening and directory-traversal utilities into `gix-fs` so filesystem consumers can use a common API without requesting filesystem features from `gix-features`. Expose `open_options_no_follow()` and `open_read_only_no_follow()` for Unix and Windows. The read helper returns an opened non-symlink handle or an explicit `FileOrSymlink::Symlink` outcome, including for dangling links. Inspect opened handles before returning them and classify failed opens without retrying; preserve missing paths and other I/O errors. Protection applies to the final path component, while parent-directory symlinks are still followed. Own the Unicode-precomposition adapter shared by `read_dir()` and recursive walking. `read_dir::DirEntry` now uses the `gix-fs` type instead of re-exporting the `gix-features` type, changing its type identity while retaining its methods and normalization behavior. Add the optional `walkdir` feature with depth limits, hidden entries, configurable link following, and Git's ordering of files and directories. Traversal remains serial regardless of the requested `Parallelism`. Basic file access and `read_dir()` remain available without feature flags. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- Cargo.lock | 3 + etc/scripts/cargo-check-all.sh | 3 + gix-fs/Cargo.toml | 18 ++- gix-fs/src/file.rs | 63 ++++++++++ gix-fs/src/lib.rs | 24 +++- gix-fs/src/precompose.rs | 88 ++++++++++++++ gix-fs/src/read_dir.rs | 19 ++- gix-fs/src/walkdir.rs | 110 ++++++++++++++++++ .../fixtures/generated-archives/.gitignore | 3 + gix-fs/tests/fixtures/walkdir.sh | 18 +++ gix-fs/tests/fs/file.rs | 73 ++++++++++++ gix-fs/tests/fs/main.rs | 3 + gix-fs/tests/fs/walkdir.rs | 86 ++++++++++++++ justfile | 1 + 14 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 gix-fs/src/file.rs create mode 100644 gix-fs/src/precompose.rs create mode 100644 gix-fs/src/walkdir.rs create mode 100644 gix-fs/tests/fixtures/generated-archives/.gitignore create mode 100755 gix-fs/tests/fixtures/walkdir.sh create mode 100644 gix-fs/tests/fs/file.rs create mode 100644 gix-fs/tests/fs/walkdir.rs diff --git a/Cargo.lock b/Cargo.lock index cb8be04a2f1..9709d8245b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,6 +1943,7 @@ version = "0.23.0" dependencies = [ "bstr", "crossbeam-channel", + "document-features", "gix-error", "gix-features", "gix-path", @@ -1950,8 +1951,10 @@ dependencies = [ "gix-utils", "insta", "is_ci", + "libc", "serde", "tempfile", + "walkdir", ] [[package]] diff --git a/etc/scripts/cargo-check-all.sh b/etc/scripts/cargo-check-all.sh index 8e21336637e..055a2e5b891 100755 --- a/etc/scripts/cargo-check-all.sh +++ b/etc/scripts/cargo-check-all.sh @@ -84,6 +84,9 @@ cargo check -p gix-features --features progress cargo check -p gix-features --features io-pipe cargo check -p gix-features --features crc32 cargo check -p gix-features --features cache-efficiency-debug +cargo check -p gix-fs --no-default-features +cargo check -p gix-fs --features walkdir +cargo check -p gix-fs --all-features cargo check -p gix-commitgraph --all-features cargo check -p gix-config-value --all-features cargo check -p gix-config --all-features diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml index f3ebf8cf2cf..9786da6e812 100644 --- a/gix-fs/Cargo.toml +++ b/gix-fs/Cargo.toml @@ -15,16 +15,26 @@ include = ["/src/**/*", "/LICENSE-*"] doctest = true [features] -## Data structures implement `serde::Serialize` and `serde::Deserialize`. +## [`Capabilities`] implements `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde"] +## Enable recursive directory traversal with optional Unicode precomposition and Git-style sorting using the `walkdir` crate. +walkdir = ["dep:walkdir"] + [dependencies] bstr = "1.12.0" gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["fs-read-dir"] } +gix-features = { version = "^0.50.0", path = "../gix-features" } gix-utils = { version = "^0.4.0", path = "../gix-utils" } gix-error = { version = "^0.4.0", path = "../gix-error" } + serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } +walkdir = { version = "2.3.2", optional = true } + +document-features = { version = "0.2.0", optional = true } + +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2.186" } [dev-dependencies] gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } @@ -32,3 +42,7 @@ insta = "1.46.3" crossbeam-channel = "0.5.15" is_ci = "1.1.1" tempfile = "3.26.0" + +[package.metadata.docs.rs] +all-features = true +features = ["document-features"] diff --git a/gix-fs/src/file.rs b/gix-fs/src/file.rs new file mode 100644 index 00000000000..5a63164e7f4 --- /dev/null +++ b/gix-fs/src/file.rs @@ -0,0 +1,63 @@ +/// Prepare open options which won't follow a symlink in the final path component on Unix or Windows. +/// +/// On Unix, opening a symlink fails. On Windows, opening can succeed with a handle to the reparse point +/// itself. Callers must inspect its metadata before I/O. In particular, avoid requesting truncation +/// while opening: validate the handle before calling [`std::fs::File::set_len()`]. +/// For reading, prefer [`open_read_only_no_follow()`], which performs the classification for the caller. +/// +/// Symlinks in parent components are still followed. On other platforms, these options do not prevent following symlinks. +pub fn open_options_no_follow() -> std::fs::OpenOptions { + #[cfg_attr(not(any(unix, windows)), allow(unused_mut))] + let mut options = std::fs::OpenOptions::new(); + #[cfg(unix)] + { + /// Make sure that it's impossible to follow through to the target of symlinks. + /// Note that this will still follow symlinks in the path, which is what we assume + /// has been checked separately. + use std::os::unix::fs::OpenOptionsExt; + options.custom_flags(libc::O_NOFOLLOW); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + // Open the reparse point itself so the handle can be checked without following it. + const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; + options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT); + } + options +} + +/// The outcome of [`open_read_only_no_follow()`]. +#[derive(Debug)] +pub enum FileOrSymlink { + /// An opened filesystem handle which isn't itself a symlink. + File(std::fs::File), + /// A symlink was found, without returning a handle to it or its target. + Symlink, +} + +/// Open `path` for reading without following a symlink in its final component on Unix or Windows. +/// +/// Return [`FileOrSymlink::Symlink`] for a live or dangling symlink, or [`FileOrSymlink::File`] for an +/// opened non-symlink handle. Missing paths and other I/O errors are returned as errors. +/// +/// Opened handles are inspected before being returned. If opening fails, the path may be checked for +/// a symlink, but the open is never retried. This lets callers choose how to handle symlinks without +/// depending on platform-specific open errors or accidentally reading through a link. +/// +/// Symlinks in parent components are still followed. No-follow protection is only provided on Unix and Windows. +pub fn open_read_only_no_follow(path: &std::path::Path) -> std::io::Result<FileOrSymlink> { + match open_options_no_follow().read(true).open(path) { + // This is naturally Windows-only as opening a symlink with O_NOFOLLOW fails on linux. + Ok(file) if file.metadata()?.file_type().is_symlink() => Ok(FileOrSymlink::Symlink), + Ok(file) => Ok(FileOrSymlink::File(file)), + // Unix no-follow opens reject symlinks. Classify the failed open without retrying it. + Err(err) + if err.kind() != std::io::ErrorKind::NotFound + && path.symlink_metadata().is_ok_and(|meta| meta.file_type().is_symlink()) => + { + Ok(FileOrSymlink::Symlink) + } + Err(err) => Err(err), + } +} diff --git a/gix-fs/src/lib.rs b/gix-fs/src/lib.rs index 20afe9ae153..5ac13a65ea5 100644 --- a/gix-fs/src/lib.rs +++ b/gix-fs/src/lib.rs @@ -1,5 +1,9 @@ //! A crate with file-system specific utilities. //! +//! File-opening helpers such as [`open_read_only_no_follow()`] and Unicode-aware [`read_dir()`] +//! are available without enabling any features. The read helper returns [`FileOrSymlink`] so callers +//! can choose whether to skip or reject symlinks without platform-specific handling. +//! //! ## Examples //! //! ``` @@ -54,6 +58,13 @@ //! assert_eq!(gix_fs::current_dir(capabilities.precompose_unicode)?, std::env::current_dir()?); //! # Ok(()) } //! ``` +//! +//! ## Feature Flags +#![cfg_attr( + all(doc, feature = "document-features"), + doc = ::document_features::document_features!() +)] +#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs)] #![forbid(unsafe_code)] @@ -87,16 +98,27 @@ pub struct Capabilities { } mod capabilities; +mod file; +pub use file::{FileOrSymlink, open_options_no_follow, open_read_only_no_follow}; + mod snapshot; pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut}; /// pub mod symlink; -/// +mod precompose; + +/// Directory entries with optional Unicode precomposition. pub mod read_dir; pub use read_dir::function::read_dir; +/// Recursive directory traversal with optional Unicode precomposition and Git-style sorting. +#[cfg(feature = "walkdir")] +pub mod walkdir; +#[cfg(feature = "walkdir")] +pub use walkdir::{WalkDir, walkdir_new, walkdir_sorted_new}; + /// pub mod dir; diff --git a/gix-fs/src/precompose.rs b/gix-fs/src/precompose.rs new file mode 100644 index 00000000000..cf91f6e573c --- /dev/null +++ b/gix-fs/src/precompose.rs @@ -0,0 +1,88 @@ +use std::{borrow::Cow, ffi::OsStr, path::Path}; + +#[derive(Debug)] +pub struct DirEntry<T: std::fmt::Debug> { + inner: T, + precompose_unicode: bool, +} + +impl<T: std::fmt::Debug> DirEntry<T> { + /// Create a new instance. + pub fn new(inner: T, precompose_unicode: bool) -> Self { + Self { + inner, + precompose_unicode, + } + } +} + +pub trait DirEntryApi { + fn path(&self) -> Cow<'_, Path>; + fn file_name(&self) -> Cow<'_, OsStr>; + fn file_type(&self) -> std::io::Result<std::fs::FileType>; +} + +impl<T: DirEntryApi + std::fmt::Debug> DirEntry<T> { + /// Obtain the full path of this entry, possibly with precomposed unicode if enabled. + /// + /// Note that decomposing filesystem like those made by Apple accept both precomposed and + /// decomposed names, and consider them equal. + pub fn path(&self) -> Cow<'_, Path> { + let path = self.inner.path(); + if self.precompose_unicode { + gix_utils::str::precompose_path(path) + } else { + path + } + } + + /// Obtain the file name of this entry, possibly with precomposed Unicode if enabled. + pub fn file_name(&self) -> Cow<'_, OsStr> { + let name = self.inner.file_name(); + if self.precompose_unicode { + gix_utils::str::precompose_os_string(name) + } else { + name + } + } + + /// Return the file type for the file that this entry points to. + /// + /// If `follow_links` was `true`, this is the file type of the item the link points to. + pub fn file_type(&self) -> std::io::Result<std::fs::FileType> { + self.inner.file_type() + } +} + +/// A platform over entries in a directory, which may or may not precompose unicode after retrieving +/// paths from the file system. +#[cfg(feature = "walkdir")] +pub struct WalkDir<T> { + pub(crate) inner: Option<T>, + pub(crate) precompose_unicode: bool, +} + +#[cfg(feature = "walkdir")] +pub struct WalkDirIter<T, I, E> +where + T: Iterator<Item = Result<I, E>>, + I: DirEntryApi, +{ + pub(crate) inner: T, + pub(crate) precompose_unicode: bool, +} + +#[cfg(feature = "walkdir")] +impl<T, I, E> Iterator for WalkDirIter<T, I, E> +where + T: Iterator<Item = Result<I, E>>, + I: DirEntryApi + std::fmt::Debug, +{ + type Item = Result<DirEntry<I>, E>; + + fn next(&mut self) -> Option<Self::Item> { + self.inner + .next() + .map(|res| res.map(|entry| DirEntry::new(entry, self.precompose_unicode))) + } +} diff --git a/gix-fs/src/read_dir.rs b/gix-fs/src/read_dir.rs index 0bb65fb0460..ffc7d16b80c 100644 --- a/gix-fs/src/read_dir.rs +++ b/gix-fs/src/read_dir.rs @@ -1,4 +1,21 @@ -pub use gix_features::fs::read_dir::DirEntry; +use std::{borrow::Cow, ffi::OsStr, fs::FileType, path::Path}; + +/// A directory entry adding precompose-unicode support to [`std::fs::DirEntry`]. +pub type DirEntry = crate::precompose::DirEntry<std::fs::DirEntry>; + +impl crate::precompose::DirEntryApi for std::fs::DirEntry { + fn path(&self) -> Cow<'_, Path> { + self.path().into() + } + + fn file_name(&self) -> Cow<'_, OsStr> { + self.file_name().into() + } + + fn file_type(&self) -> std::io::Result<FileType> { + self.file_type() + } +} pub(crate) mod function { use std::path::Path; diff --git a/gix-fs/src/walkdir.rs b/gix-fs/src/walkdir.rs new file mode 100644 index 00000000000..ce095c205e0 --- /dev/null +++ b/gix-fs/src/walkdir.rs @@ -0,0 +1,110 @@ +use std::{borrow::Cow, ffi::OsStr, fs::FileType, path::Path}; + +pub use walkdir::Error; +use walkdir::{DirEntry as DirEntryImpl, WalkDir as WalkDirImpl}; + +/// A directory entry returned by [DirEntryIter]. +pub type DirEntry = crate::precompose::DirEntry<DirEntryImpl>; +/// A platform to create a [DirEntryIter] from. +pub type WalkDir = crate::precompose::WalkDir<WalkDirImpl>; + +impl crate::precompose::DirEntryApi for DirEntryImpl { + fn path(&self) -> Cow<'_, Path> { + self.path().into() + } + + fn file_name(&self) -> Cow<'_, OsStr> { + self.file_name().into() + } + + fn file_type(&self) -> std::io::Result<FileType> { + Ok(self.file_type()) + } +} + +impl IntoIterator for WalkDir { + type Item = Result<DirEntry, walkdir::Error>; + type IntoIter = DirEntryIter; + + fn into_iter(self) -> Self::IntoIter { + DirEntryIter { + inner: self.inner.expect("always set (builder fix)").into_iter(), + precompose_unicode: self.precompose_unicode, + } + } +} + +impl WalkDir { + /// Set the minimum component depth of paths of entries. + pub fn min_depth(mut self, min: usize) -> Self { + self.inner = Some(self.inner.take().expect("always set").min_depth(min)); + self + } + /// Set the maximum component depth of paths of entries. + pub fn max_depth(mut self, max: usize) -> Self { + self.inner = Some(self.inner.take().expect("always set").max_depth(max)); + self + } + /// Follow symbolic links. + pub fn follow_links(mut self, toggle: bool) -> Self { + self.inner = Some(self.inner.take().expect("always set").follow_links(toggle)); + self + } +} + +/// Instantiate a new directory iterator which will not skip hidden files. +/// +/// Use `precompose_unicode` to represent the `core.precomposeUnicode` configuration option. +pub fn walkdir_new(root: &Path, precompose_unicode: bool) -> WalkDir { + WalkDir { + inner: WalkDirImpl::new(root).into(), + precompose_unicode, + } +} + +/// Instantiate new directory iterator which will not skip hidden files and uses Git's directory-aware sorting. +/// +/// Use `precompose_unicode` to represent the `core.precomposeUnicode` configuration option. +/// Use `max_depth` to limit the depth of the recursive walk. +/// * `0` +/// - Returns only the root path with no children +/// * `1` +/// - Root directory and children. +/// * `1..n` +/// - Root directory, children and {n}-grandchildren +pub fn walkdir_sorted_new(root: &Path, max_depth: usize, precompose_unicode: bool) -> WalkDir { + WalkDir { + inner: WalkDirImpl::new(root) + .max_depth(max_depth) + .sort_by(|a, b| { + let storage_a; + let storage_b; + let a_name = match gix_path::os_str_into_bstr(a.file_name()) { + Ok(f) => f, + Err(_) => { + storage_a = a.file_name().to_string_lossy(); + storage_a.as_ref().into() + } + }; + let b_name = match gix_path::os_str_into_bstr(b.file_name()) { + Ok(f) => f, + Err(_) => { + storage_b = b.file_name().to_string_lossy(); + storage_b.as_ref().into() + } + }; + // "common." < "common/" < "common0" + let common = a_name.len().min(b_name.len()); + a_name[..common].cmp(&b_name[..common]).then_with(|| { + let a = a_name.get(common).or_else(|| a.file_type().is_dir().then_some(&b'/')); + let b = b_name.get(common).or_else(|| b.file_type().is_dir().then_some(&b'/')); + a.cmp(&b) + }) + }) + .into(), + precompose_unicode, + } +} + +/// The Iterator yielding directory items +pub type DirEntryIter = crate::precompose::WalkDirIter<walkdir::IntoIter, DirEntryImpl, walkdir::Error>; diff --git a/gix-fs/tests/fixtures/generated-archives/.gitignore b/gix-fs/tests/fixtures/generated-archives/.gitignore new file mode 100644 index 00000000000..c7e7a4d01e4 --- /dev/null +++ b/gix-fs/tests/fixtures/generated-archives/.gitignore @@ -0,0 +1,3 @@ +# These small filesystem fixtures should be generated on the test host. +*.tar +*.tar.xz diff --git a/gix-fs/tests/fixtures/walkdir.sh b/gix-fs/tests/fixtures/walkdir.sh new file mode 100755 index 00000000000..c90c1e393f2 --- /dev/null +++ b/gix-fs/tests/fixtures/walkdir.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Independent layouts keep one traversal scenario from affecting another. +# Hidden files must be included, but the depth limit must exclude nested entries. +mkdir -p hidden/directory +touch hidden/.hidden hidden/directory/nested + +# Git orders these siblings as common., common/, common0, unlike lexical sorting. +mkdir -p sorted/common +touch sorted/common. sorted/common0 sorted/common/child + +# Both components deliberately contain U+0061 U+0308, not precomposed U+00E4. +# This checks normalization of the full path as well as the entry name. +mkdir -p "unicode/ä" +touch "unicode/ä/ä" + +# Leave "missing" absent so traversal can preserve its underlying I/O error. diff --git a/gix-fs/tests/fs/file.rs b/gix-fs/tests/fs/file.rs new file mode 100644 index 00000000000..4a5e37106ca --- /dev/null +++ b/gix-fs/tests/fs/file.rs @@ -0,0 +1,73 @@ +mod open_read_only_no_follow { + + use std::io::Read; + + use gix_fs::{FileOrSymlink, open_read_only_no_follow}; + + #[test] + fn regular_files_can_be_read() -> gix_testtools::Result { + let dir = gix_testtools::tempfile::tempdir()?; + let path = dir.path().join("file"); + std::fs::write(&path, b"contents")?; + + let mut buf = Vec::new(); + let FileOrSymlink::File(mut file) = open_read_only_no_follow(&path)? else { + panic!("a regular file must not be classified as a symlink"); + }; + file.read_to_end(&mut buf)?; + assert_eq!( + buf, b"contents", + "regular files remain readable without further options" + ); + Ok(()) + } + + #[test] + fn missing_paths_are_errors_not_symlinks() -> gix_testtools::Result { + let dir = gix_testtools::tempfile::tempdir()?; + let err = open_read_only_no_follow(&dir.path().join("missing")) + .expect_err("missing paths must not be classified as symlinks"); + assert_eq!( + err.kind(), + std::io::ErrorKind::NotFound, + "callers can distinguish missing paths from skipped symlinks" + ); + Ok(()) + } + + #[test] + #[cfg(any(unix, windows))] + fn symlinks_are_not_followed() -> gix_testtools::Result { + #[cfg(unix)] + use std::os::unix::fs::symlink; + #[cfg(windows)] + use std::os::windows::fs::symlink_file as symlink; + + let dir = gix_testtools::tempfile::tempdir()?; + let target = dir.path().join("file"); + let link = dir.path().join("link"); + std::fs::write(&target, b"contents")?; + if let Err(err) = symlink(&target, &link) { + if cfg!(windows) && err.kind() == std::io::ErrorKind::PermissionDenied { + eprintln!("skipping symlink checks: {err}"); + return Ok(()); + } + return Err(err.into()); + } + + for target_exists in [true, false] { + if !target_exists { + std::fs::remove_file(&target)?; + } + assert!( + link.symlink_metadata()?.file_type().is_symlink(), + "native links remain symlinks whether their targets exist or not" + ); + assert!( + matches!(open_read_only_no_follow(&link)?, FileOrSymlink::Symlink), + "live and dangling symlinks have the same explicit outcome on Unix and Windows" + ); + } + Ok(()) + } +} diff --git a/gix-fs/tests/fs/main.rs b/gix-fs/tests/fs/main.rs index 3d9a07bb13f..6e73fb73afe 100644 --- a/gix-fs/tests/fs/main.rs +++ b/gix-fs/tests/fs/main.rs @@ -2,9 +2,12 @@ type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + mod capabilities; mod dir; +mod file; mod read_dir; mod snapshot; mod stack; +#[cfg(feature = "walkdir")] +mod walkdir; #[test] #[cfg(unix)] diff --git a/gix-fs/tests/fs/walkdir.rs b/gix-fs/tests/fs/walkdir.rs new file mode 100644 index 00000000000..ca06088a458 --- /dev/null +++ b/gix-fs/tests/fs/walkdir.rs @@ -0,0 +1,86 @@ +use std::{ffi::OsStr, io::ErrorKind}; + +use gix_fs::{walkdir_new, walkdir_sorted_new}; + +#[test] +fn hidden_entries_and_depth_limits() -> gix_testtools::Result { + let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("hidden"); + + let mut names = walkdir_new(&dir, false) + .min_depth(1) + .max_depth(1) + .into_iter() + .map(|entry| entry.map(|entry| entry.file_name().into_owned())) + .collect::<Result<Vec<_>, _>>()?; + names.sort(); + assert_eq!( + names, + [".hidden", "directory"], + "hidden entries are included, while the root and descendants beyond max_depth are omitted" + ); + Ok(()) +} + +#[test] +fn sorted_walk_uses_git_directory_order() -> gix_testtools::Result { + let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("sorted"); + + let names = walkdir_sorted_new(&dir, 1, false) + .min_depth(1) + .into_iter() + .map(|entry| entry.map(|entry| entry.file_name().into_owned())) + .collect::<Result<Vec<_>, _>>()?; + assert_eq!( + names, + ["common.", "common", "common0"], + "directories sort as if their names ended in a slash, and the depth limit excludes their children" + ); + Ok(()) +} + +#[test] +fn unicode_precomposition_applies_to_names_and_paths() -> gix_testtools::Result { + let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("unicode"); + let decomposed = "a\u{308}"; + let root = dir.join(decomposed); + + for (precompose_unicode, expected) in [(false, decomposed), (true, "ä")] { + for walk in [ + walkdir_new(&root, precompose_unicode), + walkdir_sorted_new(&root, 1, precompose_unicode), + ] { + let mut entries = walk.min_depth(1).max_depth(1).into_iter(); + let entry = entries.next().expect("the directory contains one file")?; + assert_eq!( + entry.file_name(), + OsStr::new(expected), + "file names follow the requested Unicode precomposition policy" + ); + assert_eq!( + entry.path(), + dir.join(expected).join(expected), + "the same policy applies to every component of the full path" + ); + assert!(entry.file_type()?.is_file(), "entry metadata remains accessible"); + assert!(entries.next().is_none(), "only the direct child is returned"); + } + } + Ok(()) +} + +#[test] +fn missing_roots_yield_io_errors() -> gix_testtools::Result { + let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?; + let mut entries = walkdir_new(&dir.join("missing"), false).into_iter(); + let err = entries + .next() + .expect("an inaccessible root produces an error item") + .expect_err("missing directories cannot be traversed"); + assert_eq!( + err.io_error().map(std::io::Error::kind), + Some(ErrorKind::NotFound), + "the underlying IO error is preserved" + ); + assert!(entries.next().is_none(), "there are no entries below a missing root"); + Ok(()) +} diff --git a/justfile b/justfile index dde468d49c1..367e6a8a302 100755 --- a/justfile +++ b/justfile @@ -88,6 +88,7 @@ unit-tests: env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix-object --no-fail-fast cargo nextest run -p gix-tempfile --features signals --no-fail-fast cargo nextest run -p gix-features --all-features --no-fail-fast + cargo nextest run -p gix-fs --all-features --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha1 cargo nextest run -p gix-ref --all-features --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix-ref --all-features --no-fail-fast cargo nextest run -p gix-odb --all-features --no-fail-fast From cf1ba499f4905d3c43ca6b3367d451dfaba10b25 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Thu, 1 Oct 2026 04:21:58 +0200 Subject: [PATCH 07/28] remove(gix-features)!: replace `fs` with `gix-fs` <!-- agent --> `gix-fs` already provides the filesystem helpers and recursive walkers. Keeping their duplicate implementation in `gix-features` adds unnecessary filesystem dependencies to the foundational feature crate. Remove the `fs` module and the `walkdir` and `fs-read-dir` features. Migrate workspace callers to `gix_fs`, move traversal requests to `gix-fs/walkdir`, and update the manifests, lockfile, traversal documentation, and feature check script together. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- Cargo.lock | 7 +- etc/scripts/cargo-check-all.sh | 1 - gix-dir/src/walk/function.rs | 7 +- gix-features/Cargo.toml | 15 -- gix-features/src/fs.rs | 273 ----------------------- gix-features/src/lib.rs | 1 - gix-glob/Cargo.toml | 1 + gix-glob/src/search/pattern.rs | 2 +- gix-odb/Cargo.toml | 4 +- gix-odb/src/store_impls/loose/find.rs | 14 +- gix-odb/src/store_impls/loose/iter.rs | 22 +- gix-odb/src/store_impls/loose/mod.rs | 4 +- gix-ref/Cargo.toml | 4 +- gix-ref/src/store/file/loose/iter.rs | 10 +- gix-submodule/Cargo.toml | 2 +- gix-submodule/tests/file/baseline.rs | 3 +- gix-worktree-state/src/checkout/entry.rs | 2 +- gix/src/repository/mailmap.rs | 2 +- 18 files changed, 29 insertions(+), 345 deletions(-) delete mode 100644 gix-features/src/fs.rs diff --git a/Cargo.lock b/Cargo.lock index 9709d8245b6..47513fc5244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1897,17 +1897,13 @@ dependencies = [ "crossbeam-channel", "document-features", "gix-error", - "gix-path", "gix-testtools", "gix-trace", - "gix-utils", "insta", - "libc", "once_cell", "parking_lot", "prodash", "tracing-subscriber", - "walkdir", ] [[package]] @@ -1978,6 +1974,7 @@ dependencies = [ "criterion", "document-features", "gix-features", + "gix-fs", "gix-path", "gix-testtools", "gix-utils", @@ -2528,7 +2525,7 @@ dependencies = [ "bstr", "gix-config", "gix-error", - "gix-features", + "gix-fs", "gix-path", "gix-pathspec", "gix-refspec", diff --git a/etc/scripts/cargo-check-all.sh b/etc/scripts/cargo-check-all.sh index 055a2e5b891..b9a3a103c07 100755 --- a/etc/scripts/cargo-check-all.sh +++ b/etc/scripts/cargo-check-all.sh @@ -79,7 +79,6 @@ cargo check -p gix-url --all-features cargo check -p gix-status --all-features cargo check -p gix-features --all-features cargo check -p gix-features --features parallel -cargo check -p gix-features --features fs-read-dir cargo check -p gix-features --features progress cargo check -p gix-features --features io-pipe cargo check -p gix-features --features crc32 diff --git a/gix-dir/src/walk/function.rs b/gix-dir/src/walk/function.rs index 69f943905e2..913d9265800 100644 --- a/gix-dir/src/walk/function.rs +++ b/gix-dir/src/walk/function.rs @@ -30,11 +30,8 @@ use crate::{ /// /// ### Performance Notes /// -/// In theory, parallel directory traversal can be significantly faster, and what's possible for our current -/// `gix_features::fs::WalkDir` implementation is to abstract a `filter_entry()` method so it works both for -/// the iterator from the `walkdir` crate as well as from `jwalk`. However, doing so as initial version -/// has the risk of not being significantly harder if not impossible to implement as flow-control is very -/// limited. +/// Parallel traversal is abandoned as it's not worth managing a thread-pool over this, also given that +/// traversal is rare in typical Git usage. /// /// Thus the decision was made to start out with something akin to the Git implementation, get all tests and /// baseline comparison to pass, and see if an iterator with just `filter_entry` would be capable of dealing with diff --git a/gix-features/Cargo.toml b/gix-features/Cargo.toml index 6007b00a7fe..2cec9a3a636 100644 --- a/gix-features/Cargo.toml +++ b/gix-features/Cargo.toml @@ -26,9 +26,6 @@ progress-unit-human-numbers = ["prodash?/unit-human"] ## Provide human readable byte units for progress bars. progress-unit-bytes = ["dep:bytesize", "prodash?/unit-bytes"] -## Provide utilities suitable for working with the `std::fs::read_dir()`. -fs-read-dir = ["dep:gix-utils"] - ## Implement `tracing` with `tracing-core`, which provides applications with valuable performance details if they opt-in to it. ## ## Note that this may have overhead as well, thus instrumentations should be used stategically, only providing coarse tracing by default and adding details @@ -47,9 +44,6 @@ parallel = ["dep:crossbeam-channel", "dep:parking_lot"] ## If enabled, OnceCell will be made available for interior mutability either in sync or unsync forms. ## Note: This still uses once_cell because std::sync::OnceLock::get_or_try_init() is not yet stable. once_cell = ["dep:once_cell"] -## Makes facilities of the `walkdir` crate partially available. -## In conjunction with the **parallel** feature, directory walking will be parallel instead behind a compatible interface. -walkdir = ["dep:walkdir", "dep:gix-path", "dep:gix-utils"] #* an in-memory unidirectional pipe using `bytes` as efficient transfer mechanism. io-pipe = ["dep:bytes"] ## provide a proven and fast `crc32` implementation. @@ -84,16 +78,10 @@ required-features = ["io-pipe"] gix-error = { version = "^0.4.0", path = "../gix-error" } gix-trace = { version = "^0.2.0", path = "../gix-trace" } -# for walkdir -gix-path = { version = "^0.13.0", path = "../gix-path", optional = true } -gix-utils = { version = "^0.4.0", path = "../gix-utils", optional = true } - # 'parallel' feature crossbeam-channel = { version = "0.5.15", optional = true } parking_lot = { version = "0.12.4", default-features = false, optional = true } -walkdir = { version = "2.3.2", optional = true } # used when parallel is off - # hashing crc32fast = { version = "1.5.0", optional = true } @@ -110,9 +98,6 @@ once_cell = { version = "1.21.3", optional = true } document-features = { version = "0.2.0", optional = true } -[target.'cfg(unix)'.dependencies] -libc = { version = "0.2.186" } - [package.metadata.docs.rs] all-features = true features = ["document-features"] diff --git a/gix-features/src/fs.rs b/gix-features/src/fs.rs deleted file mode 100644 index b89a3de37d9..00000000000 --- a/gix-features/src/fs.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Filesystem utilities -//! -//! These are will be parallel if the `parallel` feature is enabled, at the expense of compiling additional dependencies -//! along with runtime costs for maintaining a global [`rayon`](https://docs.rs/rayon) thread pool. -//! -//! For information on how to use the [`WalkDir`] type, have a look at -// TODO: Move all this to `gix-fs` in a breaking change. - -#[cfg(feature = "walkdir")] -mod shared { - /// The desired level of parallelism. - pub enum Parallelism { - /// Do not parallelize at all by making a serial traversal on the current thread. - Serial, - /// Create a new thread pool for each traversal with up to 16 threads or the amount of logical cores of the machine. - ThreadPoolPerTraversal { - /// The base name of the threads we create as part of the thread-pool. - thread_name: &'static str, - }, - } -} - -#[cfg(any(feature = "walkdir", feature = "fs-read-dir"))] -mod walkdir_precompose { - use std::{borrow::Cow, ffi::OsStr, path::Path}; - - #[derive(Debug)] - pub struct DirEntry<T: std::fmt::Debug> { - inner: T, - precompose_unicode: bool, - } - - impl<T: std::fmt::Debug> DirEntry<T> { - /// Create a new instance. - pub fn new(inner: T, precompose_unicode: bool) -> Self { - Self { - inner, - precompose_unicode, - } - } - } - - pub trait DirEntryApi { - fn path(&self) -> Cow<'_, Path>; - fn file_name(&self) -> Cow<'_, OsStr>; - fn file_type(&self) -> std::io::Result<std::fs::FileType>; - } - - impl<T: DirEntryApi + std::fmt::Debug> DirEntry<T> { - /// Obtain the full path of this entry, possibly with precomposed unicode if enabled. - /// - /// Note that decomposing filesystem like those made by Apple accept both precomposed and - /// decomposed names, and consider them equal. - pub fn path(&self) -> Cow<'_, Path> { - let path = self.inner.path(); - if self.precompose_unicode { - gix_utils::str::precompose_path(path) - } else { - path - } - } - - /// Obtain filen name of this entry, possibly with precomposed unicode if enabled. - pub fn file_name(&self) -> Cow<'_, OsStr> { - let name = self.inner.file_name(); - if self.precompose_unicode { - gix_utils::str::precompose_os_string(name) - } else { - name - } - } - - /// Return the file type for the file that this entry points to. - /// - /// If `follow_links` was `true`, this is the file type of the item the link points to. - pub fn file_type(&self) -> std::io::Result<std::fs::FileType> { - self.inner.file_type() - } - } - - /// A platform over entries in a directory, which may or may not precompose unicode after retrieving - /// paths from the file system. - #[cfg(feature = "walkdir")] - pub struct WalkDir<T> { - pub(crate) inner: Option<T>, - pub(crate) precompose_unicode: bool, - } - - #[cfg(feature = "walkdir")] - pub struct WalkDirIter<T, I, E> - where - T: Iterator<Item = Result<I, E>>, - I: DirEntryApi, - { - pub(crate) inner: T, - pub(crate) precompose_unicode: bool, - } - - #[cfg(feature = "walkdir")] - impl<T, I, E> Iterator for WalkDirIter<T, I, E> - where - T: Iterator<Item = Result<I, E>>, - I: DirEntryApi + std::fmt::Debug, - { - type Item = Result<DirEntry<I>, E>; - - fn next(&mut self) -> Option<Self::Item> { - self.inner - .next() - .map(|res| res.map(|entry| DirEntry::new(entry, self.precompose_unicode))) - } - } -} - -/// -#[cfg(feature = "fs-read-dir")] -pub mod read_dir { - use std::{borrow::Cow, ffi::OsStr, fs::FileType, path::Path}; - - /// A directory entry adding precompose-unicode support to [`std::fs::DirEntry`]. - pub type DirEntry = super::walkdir_precompose::DirEntry<std::fs::DirEntry>; - - impl super::walkdir_precompose::DirEntryApi for std::fs::DirEntry { - fn path(&self) -> Cow<'_, Path> { - self.path().into() - } - - fn file_name(&self) -> Cow<'_, OsStr> { - self.file_name().into() - } - - fn file_type(&self) -> std::io::Result<FileType> { - self.file_type() - } - } -} - -/// -#[cfg(feature = "walkdir")] -pub mod walkdir { - use std::{borrow::Cow, ffi::OsStr, fs::FileType, path::Path}; - - pub use walkdir::Error; - use walkdir::{DirEntry as DirEntryImpl, WalkDir as WalkDirImpl}; - - /// A directory entry returned by [DirEntryIter]. - pub type DirEntry = super::walkdir_precompose::DirEntry<DirEntryImpl>; - /// A platform to create a [DirEntryIter] from. - pub type WalkDir = super::walkdir_precompose::WalkDir<WalkDirImpl>; - - pub use super::shared::Parallelism; - - impl super::walkdir_precompose::DirEntryApi for DirEntryImpl { - fn path(&self) -> Cow<'_, Path> { - self.path().into() - } - - fn file_name(&self) -> Cow<'_, OsStr> { - self.file_name().into() - } - - fn file_type(&self) -> std::io::Result<FileType> { - Ok(self.file_type()) - } - } - - impl IntoIterator for WalkDir { - type Item = Result<DirEntry, walkdir::Error>; - type IntoIter = DirEntryIter; - - fn into_iter(self) -> Self::IntoIter { - DirEntryIter { - inner: self.inner.expect("always set (builder fix)").into_iter(), - precompose_unicode: self.precompose_unicode, - } - } - } - - impl WalkDir { - /// Set the minimum component depth of paths of entries. - pub fn min_depth(mut self, min: usize) -> Self { - self.inner = Some(self.inner.take().expect("always set").min_depth(min)); - self - } - /// Set the maximum component depth of paths of entries. - pub fn max_depth(mut self, max: usize) -> Self { - self.inner = Some(self.inner.take().expect("always set").max_depth(max)); - self - } - /// Follow symbolic links. - pub fn follow_links(mut self, toggle: bool) -> Self { - self.inner = Some(self.inner.take().expect("always set").follow_links(toggle)); - self - } - } - - /// Instantiate a new directory iterator which will not skip hidden files, with the given level of `parallelism`. - /// - /// Use `precompose_unicode` to represent the `core.precomposeUnicode` configuration option. - pub fn walkdir_new(root: &Path, _: Parallelism, precompose_unicode: bool) -> WalkDir { - WalkDir { - inner: WalkDirImpl::new(root).into(), - precompose_unicode, - } - } - - /// Instantiate a new directory iterator which will not skip hidden files and is sorted, with the given level of `parallelism`. - /// - /// Use `precompose_unicode` to represent the `core.precomposeUnicode` configuration option. - /// Use `max_depth` to limit the depth of the recursive walk. - /// * `0` - /// - Returns only the root path with no children - /// * `1` - /// - Root directory and children. - /// * `1..n` - /// - Root directory, children and {n}-grandchildren - pub fn walkdir_sorted_new(root: &Path, _: Parallelism, max_depth: usize, precompose_unicode: bool) -> WalkDir { - WalkDir { - inner: WalkDirImpl::new(root) - .max_depth(max_depth) - .sort_by(|a, b| { - let storage_a; - let storage_b; - let a_name = match gix_path::os_str_into_bstr(a.file_name()) { - Ok(f) => f, - Err(_) => { - storage_a = a.file_name().to_string_lossy(); - storage_a.as_ref().into() - } - }; - let b_name = match gix_path::os_str_into_bstr(b.file_name()) { - Ok(f) => f, - Err(_) => { - storage_b = b.file_name().to_string_lossy(); - storage_b.as_ref().into() - } - }; - // "common." < "common/" < "common0" - let common = a_name.len().min(b_name.len()); - a_name[..common].cmp(&b_name[..common]).then_with(|| { - let a = a_name.get(common).or_else(|| a.file_type().is_dir().then_some(&b'/')); - let b = b_name.get(common).or_else(|| b.file_type().is_dir().then_some(&b'/')); - a.cmp(&b) - }) - }) - .into(), - precompose_unicode, - } - } - - /// The Iterator yielding directory items - pub type DirEntryIter = super::walkdir_precompose::WalkDirIter<walkdir::IntoIter, DirEntryImpl, walkdir::Error>; -} - -#[cfg(feature = "walkdir")] -pub use self::walkdir::{WalkDir, walkdir_new, walkdir_sorted_new}; - -/// Prepare open options which won't follow symlinks when the file is opened. -/// -/// Note: only effective on unix currently. -pub fn open_options_no_follow() -> std::fs::OpenOptions { - #[cfg_attr(not(unix), allow(unused_mut))] - let mut options = std::fs::OpenOptions::new(); - #[cfg(unix)] - { - /// Make sure that it's impossible to follow through to the target of symlinks. - /// Note that this will still follow symlinks in the path, which is what we assume - /// has been checked separately. - use std::os::unix::fs::OpenOptionsExt; - options.custom_flags(libc::O_NOFOLLOW); - } - options -} diff --git a/gix-features/src/lib.rs b/gix-features/src/lib.rs index a881d14a6b3..89644afc86a 100644 --- a/gix-features/src/lib.rs +++ b/gix-features/src/lib.rs @@ -18,7 +18,6 @@ pub mod cache; /// pub mod decode; -pub mod fs; pub mod hash; pub mod interrupt; #[cfg(feature = "io-pipe")] diff --git a/gix-glob/Cargo.toml b/gix-glob/Cargo.toml index 449b939473f..ce46e25ade9 100644 --- a/gix-glob/Cargo.toml +++ b/gix-glob/Cargo.toml @@ -27,6 +27,7 @@ serde = ["dep:serde", "bstr/serde", "bitflags/serde"] gix-utils = { version = "^0.4.0", path = "../gix-utils" } gix-path = { version = "^0.13.0", path = "../gix-path" } gix-features = { version = "^0.50.0", path = "../gix-features" } +gix-fs = { version = "^0.23.0", path = "../gix-fs" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } bitflags = "2" serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } diff --git a/gix-glob/src/search/pattern.rs b/gix-glob/src/search/pattern.rs index 1561053112c..ceaab1df2ba 100644 --- a/gix-glob/src/search/pattern.rs +++ b/gix-glob/src/search/pattern.rs @@ -44,7 +44,7 @@ fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec let file = if follow_symlinks { std::fs::File::open(path) } else { - gix_features::fs::open_options_no_follow().read(true).open(path) + gix_fs::open_options_no_follow().read(true).open(path) }; Ok(match file { Ok(mut file) => { diff --git a/gix-odb/Cargo.toml b/gix-odb/Cargo.toml index 2cfb70183f5..c3e145391b3 100644 --- a/gix-odb/Cargo.toml +++ b/gix-odb/Cargo.toml @@ -27,7 +27,7 @@ parallel = ["gix-features/parallel"] [dependencies] gix-error = { version = "^0.4.0", path = "../gix-error" } -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["walkdir", "crc32"] } +gix-features = { version = "^0.50.0", path = "../gix-features", features = ["crc32"] } gix-zlib = { version = "^0.2.0", path = "../gix-zlib" } gix-hashtable = { version = "^0.17.0", path = "../gix-hashtable" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } @@ -35,7 +35,7 @@ gix-path = { version = "^0.13.0", path = "../gix-path" } gix-quote = { version = "^0.9.0", path = "../gix-quote" } gix-object = { version = "^0.65.0", path = "../gix-object" } gix-pack = { version = "^0.75.0", path = "../gix-pack", default-features = false } -gix-fs = { version = "^0.23.0", path = "../gix-fs" } +gix-fs = { version = "^0.23.0", path = "../gix-fs", features = ["walkdir"] } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } tempfile = "3.26.0" diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index ce1744ce998..e9e049855a4 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -29,15 +29,11 @@ impl Store { mut candidates: Option<&mut HashSet<gix_hash::ObjectId>>, ) -> std::result::Result<Option<crate::store::prefix::lookup::Outcome>, crate::loose::iter::Error> { let single_directory_iter = crate::loose::Iter { - inner: gix_features::fs::walkdir_new( - &self.path.join(prefix.as_oid().to_hex_with_len(2).to_string()), - gix_features::fs::walkdir::Parallelism::Serial, - false, - ) - .min_depth(1) - .max_depth(1) - .follow_links(false) - .into_iter(), + inner: gix_fs::walkdir_new(&self.path.join(prefix.as_oid().to_hex_with_len(2).to_string()), false) + .min_depth(1) + .max_depth(1) + .follow_links(false) + .into_iter(), hash_hex_len: prefix.as_oid().kind().len_in_hex(), }; let mut candidate = None; diff --git a/gix-odb/src/store_impls/loose/iter.rs b/gix-odb/src/store_impls/loose/iter.rs index 9f180c9e961..1c3ab3391be 100644 --- a/gix-odb/src/store_impls/loose/iter.rs +++ b/gix-odb/src/store_impls/loose/iter.rs @@ -1,14 +1,12 @@ -use gix_features::fs; - use crate::store_impls::loose; /// Returned by [`loose::Store::iter()`] -pub type Error = gix_features::fs::walkdir::Error; +pub type Error = gix_fs::walkdir::Error; impl loose::Iter { fn path_to_id( &self, - res: Result<fs::walkdir::DirEntry, fs::walkdir::Error>, + res: Result<gix_fs::walkdir::DirEntry, gix_fs::walkdir::Error>, ) -> Option<Result<gix_hash::ObjectId, Error>> { use std::path::Component::Normal; @@ -65,17 +63,11 @@ impl loose::Store { /// needed if iterators need to be implemented by hand in the absence of generators. pub fn iter(&self) -> loose::Iter { loose::Iter { - inner: fs::walkdir_new( - &self.path, - fs::walkdir::Parallelism::ThreadPoolPerTraversal { - thread_name: "gix_odb::loose::Store::iter: fs-walk", - }, - false, - ) - .min_depth(2) - .max_depth(3) - .follow_links(false) - .into_iter(), + inner: gix_fs::walkdir_new(&self.path, false) + .min_depth(2) + .max_depth(3) + .follow_links(false) + .into_iter(), hash_hex_len: self.object_hash.len_in_hex(), } } diff --git a/gix-odb/src/store_impls/loose/mod.rs b/gix-odb/src/store_impls/loose/mod.rs index e98d1300648..a7ede5997fb 100644 --- a/gix-odb/src/store_impls/loose/mod.rs +++ b/gix-odb/src/store_impls/loose/mod.rs @@ -3,8 +3,6 @@ const HEADER_MAX_SIZE: usize = 64; use std::path::{Path, PathBuf}; -use gix_features::fs; - /// Options for use in [`Store::at_opts()`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Options { @@ -100,7 +98,7 @@ pub mod verify; /// The type for an iterator over `Result<gix_hash::ObjectId, Error>)` pub struct Iter { - inner: fs::walkdir::DirEntryIter, + inner: gix_fs::walkdir::DirEntryIter, hash_hex_len: usize, } diff --git a/gix-ref/Cargo.toml b/gix-ref/Cargo.toml index 8445871c3ad..4d1b3a2f962 100644 --- a/gix-ref/Cargo.toml +++ b/gix-ref/Cargo.toml @@ -27,8 +27,8 @@ parallel = ["gix-features/parallel"] [dependencies] gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["walkdir"] } -gix-fs = { version = "^0.23.0", path = "../gix-fs" } +gix-features = { version = "^0.50.0", path = "../gix-features" } +gix-fs = { version = "^0.23.0", path = "../gix-fs", features = ["walkdir"] } gix-path = { version = "^0.13.0", path = "../gix-path" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-object = { version = "^0.65.0", path = "../gix-object" } diff --git a/gix-ref/src/store/file/loose/iter.rs b/gix-ref/src/store/file/loose/iter.rs index fdef9ab2529..6a3383c380a 100644 --- a/gix-ref/src/store/file/loose/iter.rs +++ b/gix-ref/src/store/file/loose/iter.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use gix_features::fs::walkdir::DirEntryIter; +use gix_fs::walkdir::DirEntryIter; use gix_object::bstr::ByteSlice; use gix_path::RelativePath; @@ -31,13 +31,7 @@ impl SortedLoosePaths { suffix, file_walk: path.is_dir().then(|| { // serial iteration as we expect most refs in packed-refs anyway. - gix_features::fs::walkdir_sorted_new( - path, - gix_features::fs::walkdir::Parallelism::Serial, - depth, - precompose_unicode, - ) - .into_iter() + gix_fs::walkdir_sorted_new(path, depth, precompose_unicode).into_iter() }), } } diff --git a/gix-submodule/Cargo.toml b/gix-submodule/Cargo.toml index e7aa72de315..c481fce0478 100644 --- a/gix-submodule/Cargo.toml +++ b/gix-submodule/Cargo.toml @@ -33,7 +33,7 @@ bstr = { version = "1.12.0", default-features = false } [dev-dependencies] insta = "1.46.3" gix-testtools = { path = "../tests/tools" } -gix-features = { path = "../gix-features", features = ["walkdir"] } +gix-fs = { path = "../gix-fs", features = ["walkdir"] } [package.metadata.docs.rs] features = ["sha1"] diff --git a/gix-submodule/tests/file/baseline.rs b/gix-submodule/tests/file/baseline.rs index 3237c0e9436..576f52841b2 100644 --- a/gix-submodule/tests/file/baseline.rs +++ b/gix-submodule/tests/file/baseline.rs @@ -2,7 +2,6 @@ use crate::Result; use std::{ffi::OsStr, path::PathBuf}; use bstr::ByteSlice; -use gix_features::fs::walkdir::Parallelism; #[test] fn common_values_and_names_by_path() -> Result { @@ -62,7 +61,7 @@ fn common_values_and_names_by_path() -> Result { fn module_files() -> impl Iterator<Item = (PathBuf, PathBuf)> { let dir = gix_testtools::scripted_fixture_read_only("basic.sh").expect("valid fixture"); - gix_features::fs::walkdir_sorted_new(&dir, Parallelism::Serial, usize::MAX, false) + gix_fs::walkdir_sorted_new(&dir, usize::MAX, false) .follow_links(false) .into_iter() .filter_map(move |entry| { diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index a1d9bd81dff..c2b6fdb02c7 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -253,7 +253,7 @@ fn try_unlink_path_recursively(path: &Path, path_meta: &std::fs::Metadata) -> st } fn open_options(destination_is_initially_empty: bool, overwrite_existing: bool) -> std::fs::OpenOptions { - let mut options = gix_features::fs::open_options_no_follow(); + let mut options = gix_fs::open_options_no_follow(); options .create_new(destination_is_initially_empty && !overwrite_existing) .create(!destination_is_initially_empty || overwrite_existing) diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index bfb2aed4644..fd3726cd445 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -42,7 +42,7 @@ impl crate::Repository { }); } Some(root) => { - if let Ok(mut file) = gix_features::fs::open_options_no_follow() + if let Ok(mut file) = gix_fs::open_options_no_follow() .read(true) .open(root.join(".mailmap")) .map_err(|e| { From ed9974aebcc94f2fab244b05ca75b2bccb0f6755 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Sun, 4 Oct 2026 12:43:48 +0200 Subject: [PATCH 08/28] feat(gix-testtools): add symlink preflight for Bash fixtures <!-- Byron --> Careful refackiew, only the tests were mostly waved through as they are known to be super thorough. Only their style needs checking, which was fine here. <!-- agent --> Fixtures that require symbolic links need to distinguish unavailable symlink creation on the host from broken fixture setup. Otherwise, tests either fail on unsupported filesystems or risk hiding unrelated errors when skipping. Inject `gix_testtools_require_symlinks` into Bash fixtures to probe creation of a real link before generating contents. Record the outcome at the fixture root and stop successfully when links are unavailable. Tests can then use `fixture_has_symlinks()` to skip explicitly; missing or malformed results, unexpected `ln` exits, copied files, and later setup failures remain errors. Preserve script arguments and Bash 3.2 source context without exporting the helper to child shells. Include the prelude in cache keys for fixtures using the probe, and document archive and writable-fixture constraints. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- src/shared.rs | 6 +- tests/tools/src/lib.rs | 93 +++++++++++++++- tests/tools/src/scripted_fixture_prelude.sh | 39 +++++++ .../tests/fixtures/make_symlink_preflight.sh | 37 +++++++ tests/tools/tests/tools/main.rs | 1 + tests/tools/tests/tools/symlink_preflight.rs | 104 ++++++++++++++++++ 6 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 tests/tools/src/scripted_fixture_prelude.sh create mode 100755 tests/tools/tests/fixtures/make_symlink_preflight.sh create mode 100644 tests/tools/tests/tools/symlink_preflight.rs diff --git a/src/shared.rs b/src/shared.rs index d58e08eb042..f777ebb5e1a 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -160,7 +160,7 @@ pub mod pretty { use std::io::Write; use anstream::{AutoStream, ColorChoice}; - use gix::error::TestResult; + use gix::{bstr::ByteSlice, error::TestResult}; use super::TraceOutput; @@ -171,7 +171,7 @@ pub mod pretty { tracing::dispatcher::with_default(&dispatch, || tracing::info!("visible event")); let output = output.lock().expect("trace output lock is not poisoned"); assert!( - output.windows(2).any(|bytes| bytes == b"\x1b["), + output.contains_str(b"\x1b["), "forest terminal traces contain ANSI styling" ); @@ -179,7 +179,7 @@ pub mod pretty { let mut stream = AutoStream::new(Vec::new(), choice); stream.write_all(&output)?; assert_eq!( - stream.into_inner().windows(2).any(|bytes| bytes == b"\x1b["), + stream.into_inner().contains_str(b"\x1b["), colored, "terminal adaptation follows its color choice" ); diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index 6d7c2ff3f11..fdee107cb55 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -51,6 +51,7 @@ pub mod signature; pub mod repository; const ARCHIVE_DIR_NAME: &str = "generated-archives"; +const BASH_FIXTURE_PRELUDE: &str = include_str!("scripted_fixture_prelude.sh"); /// A result type to allow using the try operator `?` in unit tests. /// @@ -750,10 +751,64 @@ pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> { /// or more specific `.gitignore` configurations in lower levels of the work tree. /// /// The latter is useful if the script's output is platform specific. +/// +/// ### Bash Symlink Preflight +/// +/// Scripts with the `.sh` extension run in Bash with an injected `gix_testtools_require_symlinks` +/// function. Call it before creating fixture contents: it probes a link to an existing file in the +/// fixture filesystem, records the result, and exits the script successfully if symlink creation is +/// unavailable. Other setup errors still fail the script. Tests must call [`fixture_has_symlinks()`] +/// before accessing the resulting fixture, which may be incomplete when the preflight fails. +/// +/// Ignore archives for these fixtures, as the preflight result is host-dependent. Prefer read-only +/// fixtures when tests only inspect their contents; record Git baselines in the script instead of +/// invoking Git again in Rust. Use [`Creation::Execute`] when tests must mutate the fixture: +/// [`Creation::CopyFromReadOnly`] does not preserve links. +/// The prelude is injected only into the Bash instance running the script, not into child shells +/// or non-shell fixture programs. pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> { scripted_fixture_read_only_with_args(script_name, None::<String>) } +/// Return whether a Bash fixture's `gix_testtools_require_symlinks` preflight succeeded. +/// +/// Pass the fixture root, even if the script changed directories before running the preflight. +/// An unavailable preflight prints its diagnostic and returns `Ok(false)` so the test can return +/// early. Missing, malformed, or unreadable results are errors, not reasons to skip a test. +/// A missing result explains that the Bash script must call `gix_testtools_require_symlinks`. +/// A successful preflight is not a substitute for asserting that the fixture's actual links exist. +pub fn fixture_has_symlinks(fixture_root: impl AsRef<Path>) -> std::io::Result<bool> { + let marker = fixture_root.as_ref().join("__gix_testtools_symlinks__"); + let result = std::fs::read_to_string(&marker).map_err(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + std::io::Error::new( + err.kind(), + format!( + "Missing symlink preflight result at \"{}\": fixture_has_symlinks() requires the Bash fixture script to call gix_testtools_require_symlinks before creating fixture contents. Pass the fixture root, not a repository subdirectory.", + marker.display() + ), + ) + } else { + err + } + })?; + if result == "supported\n" { + return Ok(true); + } + if let Some(diagnostic) = result.strip_prefix("unsupported\n") { + eprintln!( + "Skipping fixture at \"{}\": symlink creation is unavailable: {}", + fixture_root.as_ref().display(), + diagnostic.trim() + ); + return Ok(false); + } + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("Invalid symlink preflight result at \"{}\"", marker.display()), + )) +} + /// Like [`scripted_fixture_read_only()`], but uses a matching existing archive even if /// `GIX_TEST_IGNORE_ARCHIVES` is set. /// @@ -1641,6 +1696,7 @@ where let script_location = script_name.as_ref(); let fixture_base = fixture_base(); let script_path = fixture_path(script_location); + let is_bash_fixture = script_location.extension().is_some_and(|ext| ext == "sh"); // keep this lock to assure we don't return unfinished directories for threaded callers let args: Vec<String> = args.into_iter().map(Into::into).collect(); @@ -1663,13 +1719,18 @@ where .or_insert_with(|| { let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM); let mut crc_digest = crc_value.digest(); - crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| { + let script = std::fs::read(&script_path).unwrap_or_else(|err| { panic!( "file \"{}\" in CWD \"{}\" could not be read: {err}", script_path.display(), env::current_dir().expect("valid cwd").display(), ) - })); + }); + crc_digest.update(&script); + // Only fixtures using the preflight need regeneration when its implementation changes. + if is_bash_fixture && script.contains_str("gix_testtools_require_symlinks") { + crc_digest.update(BASH_FIXTURE_PRELUDE.as_bytes()); + } for arg in &args { crc_digest.update(arg.as_bytes()); } @@ -1732,12 +1793,34 @@ where &format!("using script \"{}\"", script_location.display()), |fixture_state| { if let FixtureState::Uninitialized(dir) = fixture_state { - let mut cmd = command_with_environment_snapshot(&script_absolute_path); + // Bash 3.2 loses BASH_SOURCE after calling functions defined by `-c`. Sourcing a + // temporary prelude file preserves script context without exporting functions. + let prelude_file = if is_bash_fixture { + let mut file = tempfile::NamedTempFile::new()?; + std::io::Write::write_all(file.as_file_mut(), BASH_FIXTURE_PRELUDE.as_bytes())?; + Some(file) + } else { + None + }; + let mut cmd = if let Some(file) = prelude_file.as_ref() { + let mut cmd = command_with_environment_snapshot(bash_program()); + let prelude_path = gix_path::from_bstring( + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(file.path())).into_owned(), + ); + cmd.arg("-c") + .arg("source \"$1\" || exit; shift; source \"$0\" \"$@\"") + .arg(&script_absolute_path) + .arg(prelude_path); + cmd + } else { + command_with_environment_snapshot(&script_absolute_path) + }; let output = match configure_command(&mut cmd, object_hash, &args, dir).output() { Ok(out) => out, Err(err) - if err.kind() == std::io::ErrorKind::PermissionDenied - || err.raw_os_error() == Some(193) /* windows */ => + if !is_bash_fixture + && (err.kind() == std::io::ErrorKind::PermissionDenied + || err.raw_os_error() == Some(193)) /* windows */ => { cmd = command_with_environment_snapshot(bash_program()); configure_command(cmd.arg(&script_absolute_path), object_hash, &args, dir).output()? diff --git a/tests/tools/src/scripted_fixture_prelude.sh b/tests/tools/src/scripted_fixture_prelude.sh new file mode 100644 index 00000000000..ca89ced9092 --- /dev/null +++ b/tests/tools/src/scripted_fixture_prelude.sh @@ -0,0 +1,39 @@ +# Sourced into the Bash instance executing a fixture, not installed in its environment. +__gix_testtools_fixture_root=$PWD + +gix_testtools_require_symlinks() { + local probe status + probe=$(mktemp -d "$__gix_testtools_fixture_root/.gix-symlink-preflight.XXXXXX") || return + if ! printf "probe\n" >"$probe/target" || ! command -v ln >/dev/null; then + rm -rf "$probe" + return 1 + fi + if ln -s target "$probe/link" 2>"$probe/error"; then + if ! test -L "$probe/link"; then + echo "symlink preflight: ln succeeded without creating a symbolic link" >&2 + rm -rf "$probe" + return 1 + fi + printf "supported\n" >"$__gix_testtools_fixture_root/__gix_testtools_symlinks__" || { + rm -rf "$probe" + return 1 + } + rm -rf "$probe" || return + else + status=$? + case "$status" in + 1) ;; + *) + cat "$probe/error" >&2 + rm -rf "$probe" + return 1 + ;; + esac + { printf "unsupported\n"; cat "$probe/error"; } >"$__gix_testtools_fixture_root/__gix_testtools_symlinks__" || { + rm -rf "$probe" + return 1 + } + rm -rf "$probe" || return + exit 0 + fi +} diff --git a/tests/tools/tests/fixtures/make_symlink_preflight.sh b/tests/tools/tests/fixtures/make_symlink_preflight.sh new file mode 100755 index 00000000000..767fa02b810 --- /dev/null +++ b/tests/tools/tests/fixtures/make_symlink_preflight.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Simulate unavailable links and broken fixture setup without changing process-global state. +case "${1:?preflight scenario}" in + supported) ;; + unsupported) + ln() { echo "simulated symlink permission denied" >&2; return 1; } + ;; + copy) + ln() { printf "copied target contents" >"$3"; } + ;; + failure) ;; + crash) + ln() { echo "simulated crashing ln" >&2; return 139; } + ;; + unexpected) + ln() { echo "simulated unexpected ln error" >&2; return 2; } + ;; + *) exit 1 ;; +esac + +# The result belongs to the fixture root, not the directory the script has entered. +mkdir nested +cd nested +gix_testtools_require_symlinks +test "$0" = "${BASH_SOURCE[0]}" +printf "%s" "$1" >scenario + +# The injected helper must not leak into child Bash instances. +bash -c "! declare -F gix_testtools_require_symlinks" +if test "$1" = failure; then + echo "intentional failure after preflight" >&2 + exit 1 +fi +printf "contents" >target +ln -s target link diff --git a/tests/tools/tests/tools/main.rs b/tests/tools/tests/tools/main.rs index 082c034de65..08c6d36e434 100644 --- a/tests/tools/tests/tools/main.rs +++ b/tests/tools/tests/tools/main.rs @@ -136,3 +136,4 @@ mod isolation { mod repository; mod rust_fixture; mod scripted_fixture_with_post; +mod symlink_preflight; diff --git a/tests/tools/tests/tools/symlink_preflight.rs b/tests/tools/tests/tools/symlink_preflight.rs new file mode 100644 index 00000000000..ab3584b8f6b --- /dev/null +++ b/tests/tools/tests/tools/symlink_preflight.rs @@ -0,0 +1,104 @@ +use gix_testtools::TestResult; + +fn fixture(scenario: &str) -> gix_testtools::Result<std::path::PathBuf> { + gix_testtools::scripted_fixture_read_only_with_args("make_symlink_preflight.sh", [scenario]) +} + +#[test] +fn supported_links_preserve_script_arguments_and_do_not_leak_to_child_shells() -> TestResult { + let dir = fixture("supported")?; + if !gix_testtools::fixture_has_symlinks(&dir)? { + return Ok(()); + } + assert_eq!( + std::fs::read_to_string(dir.join("nested/scenario"))?, + "supported", + "sourcing the script preserves its positional arguments and script name" + ); + assert!( + dir.join("nested/link").symlink_metadata()?.file_type().is_symlink(), + "a successful preflight allows real fixture links to be created" + ); + assert_eq!( + std::fs::read_dir(&dir)?.count(), + 2, + "only the result marker and fixture contents remain after the probe" + ); + Ok(()) +} + +#[test] +fn unsupported_links_stop_successfully_before_fixture_generation() -> TestResult { + let dir = fixture("unsupported")?; + assert!( + !gix_testtools::fixture_has_symlinks(&dir)?, + "an unavailable preflight is an explicit reason to skip" + ); + assert!( + !dir.join("nested/scenario").exists(), + "fixture generation must stop at the unavailable preflight" + ); + assert_eq!( + std::fs::read_dir(&dir)?.count(), + 2, + "a failed link probe also cleans its temporary files" + ); + Ok(()) +} + +#[test] +fn copied_links_and_later_failures_are_not_skips() -> TestResult { + assert!( + fixture("copy").is_err(), + "ln succeeding without creating a real symlink is a fixture error" + ); + for scenario in ["crash", "unexpected"] { + assert!( + fixture(scenario).is_err(), + "unexpected ln exits must remain fixture errors: {scenario}" + ); + } + let dir = fixture("supported")?; + if !gix_testtools::fixture_has_symlinks(&dir)? { + return Ok(()); + } + assert!( + fixture("failure").is_err(), + "an unrelated error after a successful preflight must fail fixture generation" + ); + Ok(()) +} + +#[test] +fn missing_and_malformed_results_are_errors() -> TestResult { + let dir = gix_testtools::tempfile::tempdir()?; + let err = + gix_testtools::fixture_has_symlinks(dir.path()).expect_err("a missing marker is not an unsupported preflight"); + assert_eq!( + err.kind(), + std::io::ErrorKind::NotFound, + "fixtures must explicitly run the preflight" + ); + let diagnostic = err.to_string(); + assert!( + diagnostic.contains("requires the Bash fixture script to call gix_testtools_require_symlinks"), + "the error explains the missing Bash-side preflight: {diagnostic}" + ); + assert!( + diagnostic.contains(&dir.path().join("__gix_testtools_symlinks__").display().to_string()), + "the error identifies the expected marker location: {diagnostic}" + ); + assert!( + diagnostic.contains("Pass the fixture root, not a repository subdirectory"), + "the error also explains how to correct a misplaced Rust-side query: {diagnostic}" + ); + std::fs::write(dir.path().join("__gix_testtools_symlinks__"), b"invalid\n")?; + assert_eq!( + gix_testtools::fixture_has_symlinks(dir.path()) + .expect_err("malformed markers are not reasons to skip") + .kind(), + std::io::ErrorKind::InvalidData, + "only recognized preflight outcomes are accepted" + ); + Ok(()) +} From 21b210aab99b30c660d0de497bf2045aefa2fa09 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Sun, 4 Oct 2026 13:11:41 +0200 Subject: [PATCH 09/28] Use `bstr` helpers for byte substring searches <!-- agent --> Replace manual `windows()` scans with `ByteSlice::contains_str()` and `ByteSlice::find()`. Use `contains_str()` where only presence matters instead of checking `find().is_some()`. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- gix-credentials/tests/helper/cascade.rs | 6 +----- gix-credentials/tests/program/main.rs | 3 ++- gix-merge/tests/merge/tree/cartesian.rs | 2 +- gix-object/src/encode.rs | 2 +- gix-object/tests/object/tag/signature.rs | 5 +---- gix-ref/src/store/packed/decode/tests.rs | 7 ++----- gix-refspec/tests/refspec/parse/mod.rs | 7 ++----- gix-tix/src/command/new.rs | 7 +++---- gix-tix/src/edit/create.rs | 5 +---- gix-tix/src/edit/reword.rs | 15 +++------------ gix-tix/src/edit/split.rs | 10 ++-------- gix-tix/src/edit/todo.rs | 5 +---- gix-tix/src/history.rs | 2 +- .../tests/client/blocking_io/http/mock.rs | 7 +++---- gix-transport/tests/http_helpers/mod.rs | 5 +++-- 15 files changed, 27 insertions(+), 61 deletions(-) diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index d2bb17e410c..4ac5e5b7299 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -156,11 +156,7 @@ mod invoke { .store() .send(&mut serialized) .expect("in-memory write succeeds"); - assert!( - serialized - .windows(b"username=user\rname".len()) - .any(|value| value == b"username=user\rname") - ); + assert!(serialized.contains_str("username=user\rname")); } #[test] diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index ccce621e96b..da4fd74d444 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -1,3 +1,4 @@ +use bstr::ByteSlice; use gix_credentials::program::main; use gix_error::Result; use std::{ffi::OsString, io::Cursor}; @@ -58,7 +59,7 @@ fn context_options_apply_to_input_and_output() { ) .expect("carriage returns are allowed"); - assert!(output.windows(9).any(|window| window == b"user\rname")); + assert!(output.contains_str("user\rname")); } #[test] diff --git a/gix-merge/tests/merge/tree/cartesian.rs b/gix-merge/tests/merge/tree/cartesian.rs index 1ad77f2d334..876df0ff574 100644 --- a/gix-merge/tests/merge/tree/cartesian.rs +++ b/gix-merge/tests/merge/tree/cartesian.rs @@ -1196,7 +1196,7 @@ fn contains(tree: &Tree, needle: &[u8], objects: &gix_odb::memory::Proxy<gix_odb let mut buf = Vec::new(); for (_, id) in tree.values() { let blob = objects.find_blob(id, &mut buf)?; - if blob.data.find(needle).is_some() { + if blob.data.contains_str(needle) { return Ok(true); } } diff --git a/gix-object/src/encode.rs b/gix-object/src/encode.rs index 02c98434f36..cb79eaec2d9 100644 --- a/gix-object/src/encode.rs +++ b/gix-object/src/encode.rs @@ -71,7 +71,7 @@ pub(crate) fn header_field(name: &[u8], value: &[u8], out: &mut dyn io::Write) - if value.is_empty() { return Err(io::Error::other(validation("Header values must not be empty"))); } - if value.find(NL).is_some() { + if value.contains_str(NL) { return Err(io::Error::other( validation("Newlines are not allowed in header values").with_input(value), )); diff --git a/gix-object/tests/object/tag/signature.rs b/gix-object/tests/object/tag/signature.rs index a312bb3bf1d..263d9fca942 100644 --- a/gix-object/tests/object/tag/signature.rs +++ b/gix-object/tests/object/tag/signature.rs @@ -119,10 +119,7 @@ fn replaces_the_active_signature_and_tampering_is_invalid() -> Result { ); assert!(verify_ssh(&signed)?.is_valid(), "the replacement signature is valid"); - let message_start = data - .windows(b"signed tag".len()) - .position(|window| window == b"signed tag") - .expect("the signed message is present"); + let message_start = data.find("signed tag").expect("the signed message is present"); data[message_start] = b'S'; let (signature, signed_data) = TagRefIter::signature(&data).expect("the signature remains discoverable"); let outcome = signed_data.verify( diff --git a/gix-ref/src/store/packed/decode/tests.rs b/gix-ref/src/store/packed/decode/tests.rs index 086b583b0d2..b3d9494772c 100644 --- a/gix-ref/src/store/packed/decode/tests.rs +++ b/gix-ref/src/store/packed/decode/tests.rs @@ -82,7 +82,7 @@ mod reference { } mod record_at_offset { - use crate::store_impl::packed::decode; + use crate::{bstr::ByteSlice, store_impl::packed::decode}; const INPUT: &[u8] = b"1111111111111111111111111111111111111111 refs/heads/main 2222222222222222222222222222222222222222 refs/tags/v1 @@ -90,10 +90,7 @@ mod record_at_offset { 4444444444444444444444444444444444444444 refs/tags/v2\n"; fn offset_of(needle: &[u8]) -> usize { - INPUT - .windows(needle.len()) - .position(|window| window == needle) - .expect("needle is present in input") + INPUT.find(needle).expect("needle is present in input") } #[test] diff --git a/gix-refspec/tests/refspec/parse/mod.rs b/gix-refspec/tests/refspec/parse/mod.rs index 8ac64db5ae4..bf3458a54be 100644 --- a/gix-refspec/tests/refspec/parse/mod.rs +++ b/gix-refspec/tests/refspec/parse/mod.rs @@ -75,6 +75,7 @@ mod invalid; mod push; mod util { + use bstr::ByteSlice; use gix_error::{Error, Result}; use gix_refspec::{Instruction, RefSpecRef, parse::Operation}; @@ -120,11 +121,7 @@ mod util { else { panic!("the unsupported pattern is retained as bytes"); }; - assert!( - spec.as_bytes() - .windows(input.len()) - .any(|candidate| candidate == input.as_slice()) - ); + assert!(spec.as_bytes().contains_str(input.as_slice())); err } diff --git a/gix-tix/src/command/new.rs b/gix-tix/src/command/new.rs index a4b720a002a..0f3c4a08319 100644 --- a/gix-tix/src/command/new.rs +++ b/gix-tix/src/command/new.rs @@ -94,6 +94,8 @@ pub(super) fn run(repository: gix::Repository, args: Args) -> Result<()> { mod tests { use std::path::Path; + use gix::bstr::ByteSlice; + use super::*; fn args() -> Args { @@ -327,10 +329,7 @@ mod tests { true, )?; assert!( - prepared - .document - .windows(b"\nTodo\n".len()) - .any(|window| window == b"\nTodo\n"), + prepared.document.contains_str(b"\nTodo\n"), "--todo activates the existing editable header" ); Ok(()) diff --git a/gix-tix/src/edit/create.rs b/gix-tix/src/edit/create.rs index 7b2bf916e48..7ec815912d2 100644 --- a/gix-tix/src/edit/create.rs +++ b/gix-tix/src/edit/create.rs @@ -742,10 +742,7 @@ mod tests { "new-commit editors contain only the configured author" ); assert!( - prepared - .document - .windows(b"tracked | 2 +- 0".len()) - .any(|window| window == b"tracked | 2 +- 0"), + prepared.document.contains_str(b"tracked | 2 +- 0"), "the editor buffer includes a commented per-file diffstat with net lines: {}", prepared.document.as_bstr() ); diff --git a/gix-tix/src/edit/reword.rs b/gix-tix/src/edit/reword.rs index e4861828bbe..7fde1edf264 100644 --- a/gix-tix/src/edit/reword.rs +++ b/gix-tix/src/edit/reword.rs @@ -814,18 +814,11 @@ mod tests { "the configured committer date is shown" ); assert!( - document - .windows(b"CommentChar: ;\n;Todo\n;Message:\n\n".len()) - .any(|line| line == b"CommentChar: ;\n;Todo\n;Message:\n\n"), + document.contains_str(b"CommentChar: ;\n;Todo\n;Message:\n\n"), "the template declares its comment prefix and inactive enrichments" ); assert!( - !document - .windows(b";Assisted-by:".len()) - .any(|line| line == b";Assisted-by:") - && !document - .windows(b";Co-authored-by:".len()) - .any(|line| line == b";Co-authored-by:"), + !document.contains_str(b";Assisted-by:") && !document.contains_str(b";Co-authored-by:"), "existing trailer keys suppress model-specific suggestions regardless of their values" ); Ok(()) @@ -969,9 +962,7 @@ mod tests { crate::enrich::set_note(&repository, id, Some(b"Existing title\n\nexisting body\n"))?; let (_, document) = document(&repository, id)?; assert!( - document - .windows(b"Todo\nMessage: Existing title\n\n".len()) - .any(|window| { window == b"Todo\nMessage: Existing title\n\n" }), + document.contains_str(b"Todo\nMessage: Existing title\n\n"), "active enrichments are shown before the commit message" ); diff --git a/gix-tix/src/edit/split.rs b/gix-tix/src/edit/split.rs index 1f02cc9733a..0cfe3f14452 100644 --- a/gix-tix/src/edit/split.rs +++ b/gix-tix/src/edit/split.rs @@ -163,17 +163,11 @@ mod tests { "split editors contain only the configured author" ); assert!( - prepared - .document - .windows(b"staged".len()) - .any(|window| window == b"staged"), + prepared.document.contains_str(b"staged"), "the upper commit editor describes staged paths" ); assert!( - !prepared - .document - .windows(b"unstaged |".len()) - .any(|window| window == b"unstaged |"), + !prepared.document.contains_str(b"unstaged |"), "the upper commit editor excludes worktree-only paths" ); assert_eq!( diff --git a/gix-tix/src/edit/todo.rs b/gix-tix/src/edit/todo.rs index 866927e070e..632b939918d 100644 --- a/gix-tix/src/edit/todo.rs +++ b/gix-tix/src/edit/todo.rs @@ -2856,10 +2856,7 @@ mod tests { "the first line identifies an unchanged todo as a no-op" ); assert!( - prepared - .document - .windows("↻".len()) - .any(|window| window == "↻".as_bytes()), + prepared.document.contains_str("↻"), "the pending sibling remains visible in the todo" ); Ok(()) diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index 99a2bff28df..b47713715e4 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -2040,7 +2040,7 @@ fn decode_metadata<'a>( pub(crate) fn contains_agent_marker(message: &[u8]) -> bool { [b"--- agent".as_slice(), b"<!-- agent -->".as_slice()] .iter() - .any(|marker| message.windows(marker.len()).any(|window| window == *marker)) + .any(|marker| message.contains_str(*marker)) } fn resolve_tips(repo: &gix::Repository, revisions: &[OsString]) -> Result<Option<Vec<ObjectId>>> { diff --git a/gix-transport/tests/client/blocking_io/http/mock.rs b/gix-transport/tests/client/blocking_io/http/mock.rs index 8d8e533cf24..e9160489cd4 100644 --- a/gix-transport/tests/client/blocking_io/http/mock.rs +++ b/gix-transport/tests/client/blocking_io/http/mock.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use bstr::ByteVec; +use bstr::{ByteSlice, ByteVec}; use gix_transport::{ Protocol, client::{TransportWithoutIO, blocking_io::http}, @@ -34,9 +34,8 @@ pub struct Server { impl Server { fn normalize_http_response(mut response: Vec<u8>) -> Vec<u8> { let split = response - .windows(2) - .position(|window| window == b"\n\n") - .or_else(|| response.windows(4).position(|window| window == b"\r\n\r\n")) + .find(b"\n\n") + .or_else(|| response.find(b"\r\n\r\n")) .expect("HTTP fixture with header/body separator"); let separator_len = if response[split..].starts_with(b"\r\n\r\n") { 4 diff --git a/gix-transport/tests/http_helpers/mod.rs b/gix-transport/tests/http_helpers/mod.rs index 9ca2a866c8f..85d6445bb24 100644 --- a/gix-transport/tests/http_helpers/mod.rs +++ b/gix-transport/tests/http_helpers/mod.rs @@ -1,9 +1,10 @@ use std::{io::BufRead, time::Duration}; +use bstr::ByteSlice; + pub(crate) fn response_with_connection_close(response: &[u8]) -> Vec<u8> { let split = response - .windows(2) - .position(|window| window == b"\n\n") + .find(b"\n\n") .expect("response fixture with header/body separator"); let (headers, body) = response.split_at(split); let body = &body[2..]; From 218db27ae70dc387d70f59f88c9ad205256a9eeb Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:15:15 +0200 Subject: [PATCH 10/28] fix(gix-worktree): skip symlinked ignore files and protect checkout writes <!-- agent --> A tracked `.gitignore` symlink was absent from the skip-worktree ID mapping, so ordinary ignore lookup could parse arbitrary external files. An endless target could also exhaust memory. Always request no-follow reads for worktree `.gitignore` files, regardless of their index entry. Use `gix_fs::open_read_only_no_follow()` when pattern loading disallows symlinks, treating skipped links as absent files so skip-worktree blob fallback remains available. Discard partially read pattern bytes on failure. Use the same helper for the worktree `.mailmap`, reporting symlinks as errors without reading their targets and identifying worktree read errors correctly. Explicitly configured pattern files and `mailmap.file` may still follow symlinks. On Windows, inspect nonexclusive checkout handles for symlinks before truncating with `set_len(0)` or writing. Retain forced unlink-and-replace behavior and route symlinks checked out as ordinary files through the same validated open path. No-follow protection applies to the final path component on Unix and Windows; leading components can still be symlinks. Compare ignore lookup and skip-worktree fallback against isolated Git `check-ignore` calls. Cover mailmap policies and checkout collisions with file, directory, and dangling symlink targets, including links created after the Windows path check and validation before truncation. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- .../fixtures/generated-archives/.gitignore | 1 + gix-fs/tests/fixtures/symlinks.sh | 11 ++ gix-fs/tests/fs/file.rs | 30 +-- gix-glob/src/search/pattern.rs | 28 ++- gix-worktree-state/src/checkout/entry.rs | 152 ++++++++++++++- .../tests/worktree-state/checkout.rs | 177 ++++++++++++------ gix-worktree/src/stack/state/ignore.rs | 6 +- .../fixtures/generated-archives/.gitignore | 5 + .../fixtures/make_symlinked_ignore_repo.sh | 42 +++++ gix-worktree/tests/worktree/stack/ignore.rs | 67 +++++++ gix/src/repository/mailmap.rs | 31 ++- .../fixtures/generated-archives/.gitignore | 3 + .../fixtures/make_symlinked_mailmap_repo.sh | 20 ++ gix/tests/gix/repository/mailmap.rs | 43 +++++ 14 files changed, 505 insertions(+), 111 deletions(-) create mode 100755 gix-fs/tests/fixtures/symlinks.sh create mode 100755 gix-worktree/tests/fixtures/make_symlinked_ignore_repo.sh create mode 100755 gix/tests/fixtures/make_symlinked_mailmap_repo.sh diff --git a/gix-fs/tests/fixtures/generated-archives/.gitignore b/gix-fs/tests/fixtures/generated-archives/.gitignore index c7e7a4d01e4..ee2e1b6de80 100644 --- a/gix-fs/tests/fixtures/generated-archives/.gitignore +++ b/gix-fs/tests/fixtures/generated-archives/.gitignore @@ -1,3 +1,4 @@ # These small filesystem fixtures should be generated on the test host. +# In particular, symlinks.sh records a host-dependent symlink preflight result. *.tar *.tar.xz diff --git a/gix-fs/tests/fixtures/symlinks.sh b/gix-fs/tests/fixtures/symlinks.sh new file mode 100755 index 00000000000..62269853218 --- /dev/null +++ b/gix-fs/tests/fixtures/symlinks.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Prepare live and dangling relative file links without requiring test-time mutation. +# Create the dangling link while its target exists for native-strict Git Bash on Windows. +gix_testtools_require_symlinks +printf "contents" >file +ln -s file link +printf "contents" >removed-file +ln -s removed-file dangling-link +rm removed-file diff --git a/gix-fs/tests/fs/file.rs b/gix-fs/tests/fs/file.rs index 4a5e37106ca..3e8914fd7f3 100644 --- a/gix-fs/tests/fs/file.rs +++ b/gix-fs/tests/fs/file.rs @@ -38,27 +38,17 @@ mod open_read_only_no_follow { #[test] #[cfg(any(unix, windows))] fn symlinks_are_not_followed() -> gix_testtools::Result { - #[cfg(unix)] - use std::os::unix::fs::symlink; - #[cfg(windows)] - use std::os::windows::fs::symlink_file as symlink; - - let dir = gix_testtools::tempfile::tempdir()?; - let target = dir.path().join("file"); - let link = dir.path().join("link"); - std::fs::write(&target, b"contents")?; - if let Err(err) = symlink(&target, &link) { - if cfg!(windows) && err.kind() == std::io::ErrorKind::PermissionDenied { - eprintln!("skipping symlink checks: {err}"); - return Ok(()); - } - return Err(err.into()); + let dir = gix_testtools::scripted_fixture_read_only("symlinks.sh")?; + if !gix_testtools::fixture_has_symlinks(&dir)? { + return Ok(()); } - - for target_exists in [true, false] { - if !target_exists { - std::fs::remove_file(&target)?; - } + for (name, target_exists) in [("link", true), ("dangling-link", false)] { + let link = dir.join(name); + assert_eq!( + link.exists(), + target_exists, + "the fixture covers both live and dangling symlinks" + ); assert!( link.symlink_metadata()?.file_type().is_symlink(), "native links remain symlinks whether their targets exist or not" diff --git a/gix-glob/src/search/pattern.rs b/gix-glob/src/search/pattern.rs index ceaab1df2ba..26ff98b0e5c 100644 --- a/gix-glob/src/search/pattern.rs +++ b/gix-glob/src/search/pattern.rs @@ -4,6 +4,7 @@ use std::{ }; use bstr::{BStr, BString, ByteSlice, ByteVec}; +use gix_fs::FileOrSymlink; use crate::{pattern::Case, search::Pattern}; @@ -39,16 +40,23 @@ pub struct Mapping<T> { pub sequence_number: usize, } +/// Read `path` into `buf`, clearing it first. +/// +/// Return `Ok(true)` if the entire file was read, even if empty, or `Ok(false)` for missing paths, +/// directory-related errors, or symlinks skipped when `follow_symlinks` is `false`. On Windows, +/// permission-denied errors from opening, inspecting, or reading also yield `Ok(false)`. +/// Return other I/O errors as `Err`. Leave `buf` empty unless the entire file was read. fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec<u8>) -> std::io::Result<bool> { buf.clear(); let file = if follow_symlinks { - std::fs::File::open(path) + std::fs::File::open(path).map(FileOrSymlink::File) } else { - gix_fs::open_options_no_follow().read(true).open(path) + gix_fs::open_read_only_no_follow(path) }; Ok(match file { - Ok(mut file) => { - if let Err(err) = file.read_to_end(buf) { + Ok(FileOrSymlink::Symlink) => false, + Ok(FileOrSymlink::File(file)) => { + if let Err(err) = read_in_full(file, buf) { if io_err_is_dir(&err) { false } else { @@ -63,6 +71,16 @@ fn read_in_full_ignore_missing(path: &Path, follow_symlinks: bool, buf: &mut Vec }) } +/// Read all bytes from `reader`, clearing `buf` if reading fails. +fn read_in_full(mut reader: impl Read, buf: &mut Vec<u8>) -> std::io::Result<usize> { + reader.read_to_end(buf).inspect_err(|_| buf.clear()) +} + +/// Return `true` for errors treated as directory-related when loading pattern files, and `false` otherwise. +/// +/// This includes `IsADirectory` and `NotADirectory`. On Windows, it also includes `PermissionDenied`, +/// since opening a directory as a file can report that error. This also catches genuine permission +/// failures on Windows; the result does not prove that the path is a directory. fn io_err_is_dir(err: &std::io::Error) -> bool { matches!( err.kind(), @@ -110,6 +128,8 @@ where /// Create a pattern list from the `source` file, which may be located underneath `root`, while optionally /// following symlinks with `follow_symlinks`, providing `buf` to temporarily store the data contained in the file. + /// When symlinks aren't followed, they are treated as absent files. + /// `buf` is cleared before reading and left empty if the file is skipped or reading fails. /// `parse` is a way to parse bytes to pattern. pub fn from_file( source: impl Into<PathBuf>, diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index c2b6fdb02c7..e4302a613c6 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -173,9 +173,13 @@ where }) .or_error()?; } else { - let mut file = try_op_or_unlink(dest, overwrite_existing, |p| { - open_options(destination_is_initially_empty, overwrite_existing).open(p) - }) + let (mut file, _) = open_file( + dest, + destination_is_initially_empty, + overwrite_existing, + false, + entry.mode, + ) .or_error()?; file.write_all(obj.data).or_error()?; file.close().or_error()?; @@ -205,7 +209,7 @@ where /// Note that this works only because we assume to not race ourselves when symlinks are involved, and we do this by /// delaying symlink creation to the end and will always do that sequentially. -/// It's still possible to fall for a race if other actors create symlinks in our path, but that's nothing to defend against. +/// Other actors replacing leading path components with symlinks can still race these operations. /// /// Without overwrite permission, the worktree stack delegate rejects terminal symlinks for incremental checkout, /// while checkout into an empty destination uses exclusive creation. @@ -258,7 +262,8 @@ fn open_options(destination_is_initially_empty: bool, overwrite_existing: bool) .create_new(destination_is_initially_empty && !overwrite_existing) .create(!destination_is_initially_empty || overwrite_existing) .write(true) - .truncate(true); + // Windows returns a reparse point handle, so open_file() must validate it before truncation. + .truncate(!cfg!(windows)); options } @@ -289,7 +294,20 @@ pub(crate) fn open_file( // not supported on windows #[cfg(windows)] let executable_bit_change = ExecutableBitChange::NoChange; - try_op_or_unlink(path, overwrite_existing, |p| options.open(p)).map(|f| (f, executable_bit_change)) + try_op_or_unlink(path, overwrite_existing, |p| { + let file = options.open(p)?; + #[cfg(windows)] + if !destination_is_initially_empty || overwrite_existing { + // Exclusive creation cannot open an existing symlink and needs no truncation. + // Nonexclusive opens must reject the handle before changing its contents. + if file.metadata()?.file_type().is_symlink() { + return Err(std::io::ErrorKind::AlreadyExists.into()); + } + file.set_len(0)?; + } + Ok(file) + }) + .map(|f| (f, executable_bit_change)) } /// Close `file` and store its stats in `entry`, possibly adjusting whether `file` is executable. @@ -397,6 +415,128 @@ mod tests { Ok(()) } + #[test] + #[cfg(any(windows, unix))] + fn nonexclusive_open_defers_truncation_until_handle_validation() -> gix_testtools::Result { + use std::io::Write; + + let dir = gix_testtools::tempfile::tempdir()?; + let path = dir.path().join("file"); + let original_contents = b"longer original contents"; + for (destination_is_initially_empty, overwrite_existing) in [(false, false), (false, true), (true, true)] { + std::fs::write(&path, original_contents)?; + let file = super::open_options(destination_is_initially_empty, overwrite_existing).open(&path)?; + assert_eq!( + file.metadata()?.len(), + if cfg!(windows) { + original_contents.len() as u64 + } else { + 0 + }, + "Windows defers truncation until handle validation; Unix truncates during the no-follow open" + ); + drop(file); + std::fs::write(&path, original_contents)?; + + let (mut file, _) = super::open_file( + &path, + destination_is_initially_empty, + overwrite_existing, + false, + gix_index::entry::Mode::FILE, + )?; + assert_eq!(file.metadata()?.len(), 0, "validated regular files must be truncated"); + file.write_all(b"new")?; + drop(file); + assert_eq!(std::fs::read(&path)?, b"new", "old trailing contents must not survive"); + } + Ok(()) + } + + #[test] + #[cfg(any(windows, unix))] + fn terminal_symlinks_created_after_precheck_are_rejected_or_replaced() -> gix_testtools::Result { + use std::{ + io::Write, + sync::atomic::{AtomicUsize, Ordering}, + }; + + let dir = gix_testtools::tempfile::tempdir()?; + if !gix_fs::Capabilities::probe_dir(dir.path()).symlink { + return Ok(()); + } + for overwrite_existing in [false, true] { + for target_exists in [false, true] { + let case = gix_testtools::tempfile::tempdir_in(dir.path())?; + let target = case.path().join("target"); + let link = case.path().join("link"); + if target_exists { + std::fs::write(&target, b"untouched")?; + } + let attempts = AtomicUsize::new(0); + let result = super::try_op_or_unlink(&link, overwrite_existing, |path| { + // Insert the link just before opening, after Windows' forced-checkout precheck. + // Unix has no precheck: O_NOFOLLOW must reject the link during the open itself. + if attempts.fetch_add(1, Ordering::Relaxed) == 0 { + gix_fs::symlink::create(&target, path)?; + } + super::open_file(path, false, false, false, gix_index::entry::Mode::FILE).map(|(file, _)| file) + }); + + if overwrite_existing { + let mut file = result?; + assert!( + file.metadata()?.is_file(), + "the retried open must return a regular file" + ); + file.write_all(b"replacement")?; + drop(file); + assert_eq!( + attempts.load(Ordering::Relaxed), + 2, + "the collision must trigger one retry" + ); + assert!( + link.symlink_metadata()?.is_file(), + "forced checkout must replace the link" + ); + assert_eq!(std::fs::read(&link)?, b"replacement", "only the replacement is written"); + } else { + let err = result.expect_err("a terminal symlink must be rejected before truncation or writing"); + assert!( + gix_fs::symlink::is_collision_error(&err), + "Unix no-follow errors and Windows rejected handles must be recognized as collisions: {err}" + ); + #[cfg(windows)] + assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists, "symlinks are collisions"); + assert_eq!( + attempts.load(Ordering::Relaxed), + 1, + "non-forced checkout must not retry" + ); + assert_eq!( + std::fs::read_link(&link)?, + target, + "the rejected link must remain unchanged" + ); + } + if target_exists { + assert_eq!( + std::fs::read(&target)?, + b"untouched", + "the target must not be truncated or written" + ); + } else { + assert!( + !target.try_exists()?, + "opening a dangling link must not create its target" + ); + } + } + } + Ok(()) + } + #[test] fn let_readers_execute() { let cases = [ diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index 72a4ed58254..6036cad426e 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -123,71 +123,126 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { } #[test] -fn nonexclusive_checkout_does_not_follow_terminal_symlinks() -> Result { - let mut opts = opts_from_probe(); - // the test needs filesystem symlink support; - if !opts.fs.symlink { +fn checkout_does_not_follow_terminal_symlinks() -> Result { + use gix_index::entry::Mode; + use gix_object::Write; + + let temp = gix_testtools::tempfile::tempdir()?; + let capabilities = gix_fs::Capabilities::probe_dir(temp.path()); + if !capabilities.symlink { return Ok(()); } - opts.destination_is_initially_empty = false; - - for overwrite_existing in [false, true] { - opts.overwrite_existing = overwrite_existing; - let outside = gix_testtools::tempfile::tempdir_in(std::env::current_dir()?)?; - let canary = outside.path().join("canary"); - std::fs::write(&canary, b"untouched")?; - let (_source, destination, _index, outcome) = checkout_index_in_tmp_dir_opts( - opts.clone(), - "make_mixed_without_submodules_and_symlinks", - None, - |_| true, - |destination| gix_fs::symlink::create(&canary, &destination.join("executable")), - )?; + for mode in [Mode::FILE, Mode::SYMLINK] { + for (destination_is_initially_empty, overwrite_existing) in + [(false, false), (false, true), (true, false), (true, true)] + { + for target_kind in ["file", "directory", "missing"] { + let destination = gix_testtools::tempfile::tempdir_in(temp.path())?; + let outside = gix_testtools::tempfile::tempdir_in(temp.path())?; + let target = outside.path().join("target"); + let canary = if target_kind == "directory" { + std::fs::create_dir(&target)?; + target.join("canary") + } else { + target.clone() + }; + if target_kind != "missing" { + std::fs::write(&canary, b"untouched")?; + } + let checked_out = destination.path().join("entry"); + gix_fs::symlink::create(&target, &checked_out)?; + + let object_hash = gix_testtools::object_hash(); + let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, object_hash); + let content_blob_id = objects.write_buf(gix_object::Kind::Blob, b"content")?; + let mut index = gix_index::State::new(object_hash); + index.dangerously_push_entry( + Default::default(), + content_blob_id, + gix_index::entry::Flags::empty(), + mode, + "entry".into(), + ); + index.sort_entries(); + let outcome = gix_worktree_state::checkout( + &mut index, + destination.path(), + objects, + &progress::Discard, + &progress::Discard, + &AtomicBool::default(), + gix_worktree_state::checkout::Options { + // Exercise regular files and the symlink-as-file fallback with the same collisions. + fs: gix_fs::Capabilities { + symlink: false, + ..capabilities + }, + destination_is_initially_empty, + overwrite_existing, + thread_limit: Some(1), + ..gix_worktree_state::checkout::Options::new(gix_filter::Pipeline::new( + Default::default(), + object_hash, + Default::default(), + )) + }, + )?; - assert!( - outcome.errors.is_empty(), - "checkout should not encounter non-collision errors" - ); - let checked_out = destination.path().join("executable"); - if overwrite_existing { - assert!( - outcome.collisions.is_empty(), - "forced checkout should replace the symlink" - ); - assert!( - checked_out.symlink_metadata()?.is_file(), - "the symlink must become a regular file" - ); - assert_eq!( - std::fs::read(&checked_out)?, - b"content", - "the regular file must be checked out" - ); - } else { - assert_eq!( - outcome.collisions.len(), - 1, - "non-forced checkout should report the terminal symlink as a collision" - ); - assert_eq!( - outcome.collisions[0].path, "executable", - "the collision should identify the terminal symlink" - ); - #[cfg(windows)] - assert_eq!( - outcome.collisions[0].error_kind, AlreadyExists, - "the collision error kind differs by platform and is only stable on Windows" - ); - assert!( - checked_out.symlink_metadata()?.file_type().is_symlink(), - "non-forced checkout must leave the symlink in place" - ); + assert!( + outcome.errors.is_empty(), + "checkout should only encounter collisions (mode={mode:?}, empty={destination_is_initially_empty}, force={overwrite_existing}, target={target_kind})" + ); + if overwrite_existing { + assert!( + outcome.collisions.is_empty(), + "forced checkout should replace the symlink" + ); + assert!( + checked_out.symlink_metadata()?.is_file(), + "the symlink must become a regular file" + ); + assert_eq!( + std::fs::read(&checked_out)?, + b"content", + "the regular file must be checked out" + ); + } else { + assert_eq!( + outcome.collisions.len(), + 1, + "non-forced checkout should report the terminal symlink as a collision" + ); + assert_eq!( + outcome.collisions[0].path, "entry", + "the collision should identify the terminal symlink" + ); + #[cfg(windows)] + if !destination_is_initially_empty { + assert_eq!( + outcome.collisions[0].error_kind, AlreadyExists, + "the stack rejects terminal links before a nonexclusive open" + ); + } + assert_eq!( + std::fs::read_link(&checked_out)?, + target, + "non-forced checkout must leave the symlink unchanged" + ); + } + if target_kind == "missing" { + assert!( + !target.try_exists()?, + "checkout must not create a dangling link's target" + ); + } else { + assert_eq!( + std::fs::read(&canary)?, + b"untouched", + "the symlink target must stay unchanged" + ); + } + } } - assert_eq!( - std::fs::read(&canary)?, - b"untouched", - "the symlink target must stay unchanged" - ); } Ok(()) } diff --git a/gix-worktree/src/stack/state/ignore.rs b/gix-worktree/src/stack/state/ignore.rs index 2d7f8c33915..edddb6cdd4b 100644 --- a/gix-worktree/src/stack/state/ignore.rs +++ b/gix-worktree/src/stack/state/ignore.rs @@ -24,7 +24,8 @@ pub enum Source { /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts /// of the history which aren't checked out. IdMapping, - /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set. + /// Read from the worktree without following symlinks, falling back to id mappings + /// for entries with the skip-worktree bit set. #[default] WorktreeThenIdMappingIfNotSkipped, } @@ -193,11 +194,10 @@ impl Ignore { } } Source::WorktreeThenIdMappingIfNotSkipped => { - let follow_symlinks = ignore_file_in_index.is_err(); let added = gix_glob::search::add_patterns_file( &mut self.stack.patterns, dir.join(".gitignore"), - follow_symlinks, + false, Some(root), buf, self.parse, diff --git a/gix-worktree/tests/fixtures/generated-archives/.gitignore b/gix-worktree/tests/fixtures/generated-archives/.gitignore index 17e033f9346..e01856d410c 100644 --- a/gix-worktree/tests/fixtures/generated-archives/.gitignore +++ b/gix-worktree/tests/fixtures/generated-archives/.gitignore @@ -13,3 +13,8 @@ make_attributes_baseline_sha256.tar # Uses `ln -s` (including a symlink to the base directory); symlinks do not # unpack on Windows without elevated privileges. symlink_stack.tar +# Symlink preflight depends on the host filesystem and privileges; regenerate +# locally rather than archiving either links or an unsupported-host marker. +# Keep both argument variants ignored, including their SHA-256 archives. +/make_symlinked_ignore_repo_indexed*.tar* +/make_symlinked_ignore_repo_symlink*.tar* diff --git a/gix-worktree/tests/fixtures/make_symlinked_ignore_repo.sh b/gix-worktree/tests/fixtures/make_symlinked_ignore_repo.sh new file mode 100755 index 00000000000..04e614a7be7 --- /dev/null +++ b/gix-worktree/tests/fixtures/make_symlinked_ignore_repo.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +gix_testtools_require_symlinks + +# Both variants keep the link target outside the repo, within the fixture. +echo external >external-ignore +mkdir repo +( + cd repo + git init -q + case "$1" in + indexed) + # Preserve a regular index blob with skip-worktree while replacing its worktree file. + echo indexed >.gitignore + git add .gitignore + git update-index --skip-worktree .gitignore + rm .gitignore + ln -s ../external-ignore .gitignore + ;; + symlink) + # An indexed symlink must not provide ignore patterns through index fallback. + ln -s ../external-ignore .gitignore + git add .gitignore + ;; + *) + echo "expected indexed or symlink" >&2 + exit 1 + ;; + esac + + # Record each path's exit status: 0 is ignored, 1 is not ignored, and other exits are errors. + for path in external indexed; do + if git check-ignore "$path" >"../$path.git-check-ignore.out"; then + status=0 + else + status=$? + test "$status" -eq 1 || exit "$status" + fi + printf '%s\n' "$status" >"../$path.git-check-ignore.status" + done +) diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index cc7a5aea579..c85b8aa2311 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -8,6 +8,73 @@ use gix_worktree::{Stack, stack::state::ignore::Source}; use crate::{hex_to_id, stack::probe_case}; +#[test] +#[cfg(any(unix, windows))] +fn symlinked_ignore_files_are_skipped_with_index_fallback() -> Result { + for use_index in [false, true] { + let dir = gix_testtools::scripted_fixture_read_only_with_args( + "make_symlinked_ignore_repo.sh", + [if use_index { "indexed" } else { "symlink" }], + )?; + if !gix_testtools::fixture_has_symlinks(&dir)? { + return Ok(()); + } + let worktree = dir.join("repo"); + let ignore = worktree.join(".gitignore"); + assert!( + ignore.symlink_metadata()?.file_type().is_symlink(), + "the worktree .gitignore must be a symlink, not a copied regular file" + ); + let mut buf = Vec::new(); + assert!( + gix_glob::search::pattern::List::from_file( + &ignore, + None, + true, + &mut buf, + gix_ignore::search::Ignore::default(), + )? + .is_some(), + "explicitly configured pattern files may still follow symlinks" + ); + assert_eq!(buf, b"external\n", "following loads the target's contents"); + + let git_dir = worktree.join(".git"); + let index = gix_index::File::at( + git_dir.join("index"), + gix_testtools::object_hash(), + false, + Default::default(), + )?; + let objects = gix_odb::at(git_dir.join("objects"), gix_testtools::object_hash())?; + let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new( + Default::default(), + Default::default(), + None, + Source::WorktreeThenIdMappingIfNotSkipped, + Default::default(), + )); + let mut stack = Stack::from_state_and_ignore_case(&worktree, false, state, &index, index.path_backing()); + for (path, excluded) in [("external", false), ("indexed", use_index)] { + let baseline_status: i32 = std::fs::read_to_string(dir.join(format!("{path}.git-check-ignore.status")))? + .trim() + .parse()?; + assert_eq!(baseline_status, i32::from(!excluded), "Git skips the symlink"); + assert_eq!( + std::fs::read(dir.join(format!("{path}.git-check-ignore.out")))?, + if excluded { b"indexed\n".as_slice() } else { b"" }, + "Git reports only the path excluded by the skip-worktree blob" + ); + assert_eq!( + stack.at_entry(path, Some(Mode::FILE), &objects)?.is_excluded(), + excluded, + "external patterns are skipped and skip-worktree blobs remain available" + ); + } + } + Ok(()) +} + struct IgnoreExpectations<'a> { lines: bstr::Lines<'a>, } diff --git a/gix/src/repository/mailmap.rs b/gix/src/repository/mailmap.rs index fd3726cd445..4d0e6536005 100644 --- a/gix/src/repository/mailmap.rs +++ b/gix/src/repository/mailmap.rs @@ -1,5 +1,6 @@ use crate::error::{ErrorExt, message}; use crate::{Error, Id, Result, bstr::ByteSlice, config::tree::Mailmap}; +use gix_fs::FileOrSymlink; impl crate::Repository { /// Similar to [`open_mailmap_into()`][crate::Repository::open_mailmap_into()], but ignores all errors and returns at worst @@ -15,7 +16,8 @@ impl crate::Repository { /// Try to merge mailmaps from the following locations into `target`: /// - /// - read the `.mailmap` file without following symlinks from the working tree, if present + /// - read the `.mailmap` file without following symlinks from the working tree, if present; + /// symlinks are reported as errors without reading their targets /// - OR read `HEAD:.mailmap` if this repository is bare (i.e. has no working tree), if the `mailmap.blob` is not set. /// - read the mailmap as configured in `mailmap.blob`, if set. /// - read the file as configured by `mailmap.file`, following symlinks, if set. @@ -41,29 +43,24 @@ impl crate::Repository { }) }); } - Some(root) => { - if let Ok(mut file) = gix_fs::open_options_no_follow() - .read(true) - .open(root.join(".mailmap")) - .map_err(|e| { - if e.kind() != std::io::ErrorKind::NotFound { - err.get_or_insert( - e.and_raise(message("The mailmap file declared in `mailmap.file` could not be read")), - ); - } - }) - { + Some(root) => match gix_fs::open_read_only_no_follow(&root.join(".mailmap")) { + Ok(FileOrSymlink::File(mut file)) => { buf.clear(); std::io::copy(&mut file, &mut buf) .map_err(|e| { - err.get_or_insert( - e.and_raise(message("The mailmap file declared in `mailmap.file` could not be read")), - ) + err.get_or_insert(e.and_raise(message("The worktree .mailmap file could not be read"))) }) .ok(); target.merge(gix_mailmap::parse_ignore_errors(&buf)); } - } + Ok(FileOrSymlink::Symlink) => { + err.get_or_insert(message("The worktree .mailmap file is a symlink").raise()); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => { + err.get_or_insert(e.and_raise(message("The worktree .mailmap file could not be read"))); + } + }, } if let Some(blob) = blob_id.and_then(|id| { diff --git a/gix/tests/fixtures/generated-archives/.gitignore b/gix/tests/fixtures/generated-archives/.gitignore index d659c74b00a..53967aff452 100644 --- a/gix/tests/fixtures/generated-archives/.gitignore +++ b/gix/tests/fixtures/generated-archives/.gitignore @@ -77,3 +77,6 @@ /make_sha256_remote_sha256.tar # These are generated, no need to check them in as regeneration works. /make_worktree_add_repos_* +# Symlink preflight depends on the host filesystem and privileges; regenerate +# locally rather than archiving either links or an unsupported-host marker. +/make_symlinked_mailmap_repo*.tar* diff --git a/gix/tests/fixtures/make_symlinked_mailmap_repo.sh b/gix/tests/fixtures/make_symlinked_mailmap_repo.sh new file mode 100755 index 00000000000..92e2c8f6e37 --- /dev/null +++ b/gix/tests/fixtures/make_symlinked_mailmap_repo.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +gix_testtools_require_symlinks + +# Keep the target outside the repository but inside this disposable fixture. +echo "External Name <external@example.com>" >external-mailmap +mkdir repo +( + cd repo + git init -q + git checkout -b main + echo "Proper Name <proper@example.com>" >.mailmap + git add .mailmap + git commit -q -m "initial mailmap" + + # Replace the tracked regular file only in the worktree. + rm .mailmap + ln -s ../external-mailmap .mailmap +) diff --git a/gix/tests/gix/repository/mailmap.rs b/gix/tests/gix/repository/mailmap.rs index de8250d77cc..697c4f1684c 100644 --- a/gix/tests/gix/repository/mailmap.rs +++ b/gix/tests/gix/repository/mailmap.rs @@ -18,6 +18,49 @@ fn empty_when_no_mailmap_present() -> Result { Ok(()) } +#[test] +#[cfg(any(unix, windows))] +fn worktree_symlinks_are_rejected_but_configured_mailmaps_may_follow_them() -> Result { + let dir = gix_testtools::scripted_fixture_read_only("make_symlinked_mailmap_repo.sh")?; + if !gix_testtools::fixture_has_symlinks(&dir)? { + return Ok(()); + } + let worktree = dir.join("repo"); + let mailmap = worktree.join(".mailmap"); + assert!( + mailmap.symlink_metadata()?.file_type().is_symlink(), + "the worktree .mailmap must be a symlink, not a copied regular file" + ); + let mut repo = gix::open_opts(&worktree, crate::util::restricted())?; + + let mut snapshot = gix_mailmap::Snapshot::default(); + let err = repo + .open_mailmap_into(&mut snapshot) + .expect_err("worktree symlinks must be reported without reading their targets"); + assert!( + err.to_string().contains("symlink"), + "the error identifies the skipped symlink" + ); + assert!(snapshot.entries().is_empty(), "the symlink target must not be loaded"); + assert!( + repo.open_mailmap().entries().is_empty(), + "ignoring the error must not expose the symlink target either" + ); + + repo.config_snapshot_mut() + .set_raw_value("mailmap.file", gix::path::into_bstr(mailmap.as_path()))?; + assert!( + repo.open_mailmap_into(&mut snapshot).is_err(), + "the worktree symlink error is still reported while other sources are loaded" + ); + assert_eq!( + snapshot.entries().len(), + 1, + "an explicitly configured mailmap may follow the same symlink" + ); + Ok(()) +} + #[test] fn reads_existing_mailmap_from_worktree_root() -> Result { let repo = named_repo("make_mailmap_repo.sh")?; From 159fc3125e6dda7afa295daa94da03f9173c1bc4 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:17:27 +0200 Subject: [PATCH 11/28] fix(gitoxide-core): confine organized repositories to their destination <!-- agent --> `ein tool organize --execute` used a repository's origin URL to construct a move destination without resolving parent components or checking containment. It was demonstrated percent-decoded path traversal moving a repository outside the user-selected root. Resolve the completed destination with the existing `gix::path::realpath()` helper, which handles existing symlinks and missing leaf components, then reject paths outside the canonical destination before creating directories or moving anything. The subsequent self-nesting check now also receives a resolved path. Disposable isolated repositories reproduce HTTP percent-encoded traversal, a parent-directory hostname, SSH traversal, and an existing symlink escaping the destination. A control retains the ordinary host/owner/repository layout and `.git` suffix handling. Both escape regressions fail before the fix. Git reference: `urlmatch.c::url_normalize()` at `d38352cd43ab` also normalizes dot segments; organizing repositories is gitoxide-specific and additionally needs filesystem confinement. Validation: all three focused organizer tests pass with the `organize` feature. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- Cargo.lock | 2 + gitoxide-core/Cargo.toml | 4 + gitoxide-core/src/organize.rs | 30 +++- .../fixtures/generated-archives/.gitignore | 3 + gitoxide-core/tests/fixtures/organize.sh | 10 ++ gitoxide-core/tests/organize.rs | 149 ++++++++++++++++++ justfile | 4 +- 7 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 gitoxide-core/tests/fixtures/generated-archives/.gitignore create mode 100755 gitoxide-core/tests/fixtures/organize.sh create mode 100644 gitoxide-core/tests/organize.rs diff --git a/Cargo.lock b/Cargo.lock index 47513fc5244..a70f342e654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1499,6 +1499,7 @@ dependencies = [ "fs-err", "futures-io", "futures-lite", + "gitoxide-core", "gix", "gix-archive", "gix-fsck", @@ -1508,6 +1509,7 @@ dependencies = [ "gix-trace", "gix-transport", "gix-url", + "insta", "layout-rs", "open", "parking_lot", diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 808264e26a6..cac8ed26ef2 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -105,8 +105,12 @@ open = "5.3.5" document-features = { version = "0.2.0", optional = true } [dev-dependencies] +# Exercise optional functionality in tests without enabling it for dependents. +# Networking remains opt-in as its async and blocking implementations are mutually exclusive. +gitoxide-core = { path = ".", features = ["organize", "estimate-hours", "query", "corpus", "archive", "clean", "tracing", "tracing-cpu-time", "serde"] } gix-testtools = { path = "../tests/tools", default-features = false } gix = { path = "../gix", default-features = false, features = ["sha1", "parallel"] } +insta = "1.46.3" [package.metadata.docs.rs] features = ["document-features", "blocking-client", "organize", "estimate-hours", "serde", "gix/sha1", "gix/parallel"] diff --git a/gitoxide-core/src/organize.rs b/gitoxide-core/src/organize.rs index dade6d7408e..73bf4fe07a0 100644 --- a/gitoxide-core/src/organize.rs +++ b/gitoxide-core/src/organize.rs @@ -8,7 +8,7 @@ use std::{ use gix::{ NestedProgress, Progress, Result, - error::{ResultExt, bail}, + error::{Exn, ResultExt, bail, message}, objs::bstr::ByteSlice, progress, }; @@ -145,6 +145,8 @@ fn find_origin_remote(repo: &Path) -> Result<Option<gix_url::Url>> { .transpose() } +/// Confinement validation errors record `destination` (the resolved path) and `destination_root` +/// (the canonicalized allowed root) as path metadata. fn handle( mode: Mode, kind: gix::repository::Kind, @@ -241,6 +243,18 @@ fn handle( } })); + // Unlike canonicalize(), realpath also resolves existing symlinks when the destination leaf + // does not exist yet. Check confinement before creating directories or moving the repository. + let destination = gix::path::realpath(destination)?; + if !destination.starts_with(canonicalized_destination) || destination == canonicalized_destination { + bail!( + "the origin URL resolves outside the repository destination" + .validation() + .with("destination", destination.as_path()) + .with("destination_root", canonicalized_destination) + ); + } + match destination.canonicalize() { Ok(destination) if git_workdir.canonicalize().or_error()? == destination => return Ok(()), _ => {} @@ -294,6 +308,10 @@ pub fn discover<P: NestedProgress>( Ok(()) } +/// Relocate repositories under `source_dir` to paths derived from their origin URLs below `destination`. +/// +/// Errors retain per-repository causes, including validation classifications and the `destination` +/// and `destination_root` path metadata for origin URLs that resolve outside the allowed root. pub fn run<P: NestedProgress>( mode: Mode, source_dir: impl AsRef<Path>, @@ -301,7 +319,7 @@ pub fn run<P: NestedProgress>( mut progress: P, threads: Option<usize>, ) -> Result<()> { - let mut num_errors = 0usize; + let mut errors = Vec::new(); let destination = destination.as_ref().canonicalize().or_error()?; let mut repositories = find_git_repository_workdirs(source_dir, progress.add_child("Searching repositories"), false, threads) @@ -314,12 +332,14 @@ pub fn run<P: NestedProgress>( path_to_move.display(), err )); - num_errors += 1; + errors.push(err); } } - if num_errors > 0 { - bail!("Failed to handle {num_errors} repositories") + if !errors.is_empty() { + let num_errors = errors.len(); + let repositories = if num_errors == 1 { "repository" } else { "repositories" }; + Err(Exn::raise_all(errors, message!("Failed to handle {num_errors} {repositories}")).into()) } else { Ok(()) } diff --git a/gitoxide-core/tests/fixtures/generated-archives/.gitignore b/gitoxide-core/tests/fixtures/generated-archives/.gitignore new file mode 100644 index 00000000000..18510b6866b --- /dev/null +++ b/gitoxide-core/tests/fixtures/generated-archives/.gitignore @@ -0,0 +1,3 @@ +# These small fixtures can be regenerated; no archived output is needed. +*.tar +*.tar.xz diff --git a/gitoxide-core/tests/fixtures/organize.sh b/gitoxide-core/tests/fixtures/organize.sh new file mode 100755 index 00000000000..f7581e9e5d5 --- /dev/null +++ b/gitoxide-core/tests/fixtures/organize.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# A standalone worktree keeps organization tests independent of the source checkout. +# HEAD and config suffice for discovery; the payload verifies that the worktree moves intact. +# Tests override origin only in their disposable copies to exercise path confinement. +git init -q source +git -C source config remote.origin.url https://example.com/owner/repository.git +printf '%s' 'repository contents' > source/payload +mkdir destination diff --git a/gitoxide-core/tests/organize.rs b/gitoxide-core/tests/organize.rs new file mode 100644 index 00000000000..517ff5db8a0 --- /dev/null +++ b/gitoxide-core/tests/organize.rs @@ -0,0 +1,149 @@ +use gitoxide_core::organize::{Mode, run}; +use gix_testtools::TestResult; + +#[cfg(unix)] +use gix::error::MetadataValue; + +#[test] +fn url_components_cannot_move_repositories_outside_the_destination() -> TestResult { + for url in [ + "https://example.com/%2e%2e/%2e%2e/escaped", + "https://../escaped", + "ssh://example.com/../../escaped", + ] { + for mode in [Mode::Execute, Mode::Simulate] { + let fixture = gix_testtools::scripted_fixture_writable("organize.sh")?; + let source = fixture.path().join("source"); + let destination = fixture.path().join("destination"); + gix_testtools::git(&source, &format!("config remote.origin.url {url}"))?; + + let err = run(mode, &source, &destination, gix::progress::Discard, Some(1)) + .expect_err("organizing must reject a URL-derived path escape, even in simulation"); + assert!( + err.is_validation(), + "URL-derived destination escapes are validation failures: {url}" + ); + let diagnostic = gix_testtools::redact_debug_snapshot( + &err, + &[(&fixture.path().canonicalize()?.to_string_lossy(), "<fixture>")], + ); + insta::allow_duplicates! { + insta::assert_debug_snapshot!( + diagnostic, + "execute and simulate preserve the confinement cause and resolved destination for every URL", + @r#" + Failed to handle 1 repository + + Caused by: + 0: the origin URL resolves outside the repository destination, "destination"="<fixture>/escaped", "destination_root"="<fixture>/destination" + "# + ); + } + assert_eq!( + std::fs::read_to_string(source.join("payload"))?, + "repository contents", + "a rejected URL leaves the source contents unchanged: {url}" + ); + assert!( + !fixture.path().join("escaped").exists(), + "no destination is created outside the requested root: {url}" + ); + assert_eq!( + std::fs::read_dir(&destination)?.count(), + 0, + "a rejected URL creates no intermediate destination directories: {url}" + ); + } + } + Ok(()) +} + +#[test] +fn ordinary_urls_still_move_repositories_below_the_destination() -> TestResult { + let fixture = gix_testtools::scripted_fixture_writable("organize.sh")?; + let source = fixture.path().join("source"); + let destination = fixture.path().join("destination"); + run(Mode::Execute, &source, &destination, gix::progress::Discard, Some(1))?; + + let moved = destination.join("example.com/owner/repository"); + assert_eq!( + std::fs::read_to_string(moved.join("payload"))?, + "repository contents", + "normal URL organization retains its layout and repository contents" + ); + assert!( + moved.join(".git/HEAD").is_file(), + "organization moves Git metadata along with the worktree" + ); + assert!(!source.exists(), "successful organization moves the repository"); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn an_existing_symlink_cannot_redirect_the_destination() -> TestResult { + for mode in [Mode::Execute, Mode::Simulate] { + let fixture = gix_testtools::scripted_fixture_writable("organize.sh")?; + let source = fixture.path().join("source"); + let destination = fixture.path().join("destination"); + let outside = fixture.path().join("outside"); + gix_testtools::git(&source, "config remote.origin.url https://example.com/repository.git")?; + std::fs::create_dir(&outside)?; + std::fs::write(outside.join("sentinel"), "outside contents")?; + std::os::unix::fs::symlink(&outside, destination.join("example.com"))?; + + let err = run(mode, &source, &destination, gix::progress::Discard, Some(1)) + .expect_err("existing path components must stay inside the destination, even in simulation"); + assert!(err.is_validation(), "destination escapes are validation failures"); + let diagnostic = gix_testtools::redact_debug_snapshot( + &err, + &[(&fixture.path().canonicalize()?.to_string_lossy(), "<fixture>")], + ); + insta::allow_duplicates! { + insta::assert_debug_snapshot!( + diagnostic, + "execute and simulate report the symlink target outside the allowed destination root", + @r#" + Failed to handle 1 repository + + Caused by: + 0: the origin URL resolves outside the repository destination, "destination"="<fixture>/outside/repository", "destination_root"="<fixture>/destination" + "# + ); + } + let metadata = err + .metadata() + .next() + .expect("confinement errors include path metadata through the public API"); + assert_eq!( + metadata["destination"], + MetadataValue::Path(outside.canonicalize()?.join("repository")), + "metadata identifies the resolved path outside the allowed root" + ); + assert_eq!( + metadata["destination_root"], + MetadataValue::Path(destination.canonicalize()?), + "metadata identifies the canonicalized allowed root" + ); + assert_eq!( + std::fs::read_to_string(source.join("payload"))?, + "repository contents", + "a rejected destination leaves the source contents unchanged" + ); + assert!( + !outside.join("repository").exists(), + "a symlink cannot redirect the move" + ); + assert_eq!( + std::fs::read_to_string(outside.join("sentinel"))?, + "outside contents", + "confinement validation leaves pre-existing outside contents unchanged" + ); + assert_eq!( + std::fs::read_dir(&outside)?.count(), + 1, + "a rejected destination creates nothing outside the allowed root" + ); + } + Ok(()) +} diff --git a/justfile b/justfile index 367e6a8a302..16304062c3a 100755 --- a/justfile +++ b/justfile @@ -141,9 +141,7 @@ unit-tests: env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix --features blocking-network-client --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix --no-fail-fast cargo nextest run -p gix --no-default-features --features sha256 --lib --no-fail-fast - cargo nextest run -p gitoxide-core --lib --no-tests=warn --no-fail-fast - cargo nextest run -p gitoxide-core --features tracing --lib --no-fail-fast - cargo nextest run -p gitoxide-core --features corpus --lib --no-fail-fast + cargo nextest run -p gitoxide-core --no-fail-fast # Run all doctests [group('Tests')] From 89a12d4a2558627524ae55151c39950e1b92973f Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:22:16 +0200 Subject: [PATCH 12/28] fix(gix-pack): reject cyclic delta chains and bound chain allocations <!-- agent --> A report describes delta base cycles that never terminate in header lookups and keep allocating during object decoding. Track a constant-space cycle checkpoint in both loops, without relying on the untrusted pack object count or rejecting valid forward references. Account for heap-backed delta-chain metadata in the existing per-allocation limit and reserve it fallibly, including when delta payloads are empty. The regressions failed before the fix and cover self, multi-entry, mixed REF/OFS cycles, empty deltas, and valid forward chains. Git reference: `t/t5309-pack-delta-cycles.sh` at `d38352cd43ab9745686d697872408bc3249a153f` rejects cycles while allowing forward delta bases. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gix-pack/src/cache/delta/traverse/resolve.rs | 30 ++- gix-pack/src/data/file/decode/entry.rs | 41 ++- gix-pack/src/data/file/decode/header.rs | 2 + gix-pack/src/data/file/decode/mod.rs | 49 ++++ gix-pack/src/data/file/init.rs | 2 + gix-pack/tests/pack/malformed.rs | 266 +++++++++++++++++++ 6 files changed, 367 insertions(+), 23 deletions(-) diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index ef495539dec..270ecaf4f21 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -14,6 +14,7 @@ use crate::{ }, data, data::EntryRange, + data::decode::resize_with_limit, }; mod node { @@ -652,16 +653,6 @@ fn decoded_size_limited(size: u64, alloc_limit_bytes: Option<usize>) -> Result<u Ok(size) } -fn resize_with_limit(out: &mut Vec<u8>, len: usize, alloc_limit_bytes: Option<usize>) -> Result { - if alloc_limit_bytes.is_some_and(|limit| len > limit) { - bail!(allocation_error(ResourceExhaustionKind::AllocationLimit)); - } - out.try_reserve(len.saturating_sub(out.len())) - .or_raise(|| message("Entry too large to fit in memory"))?; - out.resize(len, 0); - Ok(()) -} - #[cfg(test)] mod tests { use std::{ @@ -679,6 +670,25 @@ mod tests { data, }; + #[test] + fn traversal_buffer_growth_respects_alloc_limit() -> gix_error::TestResult { + let mut out = Vec::new(); + let mut inflate = gix_zlib::Inflate::default(); + for size in [64, 65] { + let payload = vec![b'A'; size]; + super::decompress_all_at_once_with(&mut inflate, &deflate(&payload), size, &mut out, Some(65))?; + assert_eq!( + out, payload, + "each entry must decompress correctly into the reused buffer" + ); + assert!( + out.capacity() <= 65, + "traversal buffer growth must not allocate beyond the cap" + ); + } + Ok(()) + } + #[test] fn traversal_resolves_children_lazily() { let mut pack = Vec::new(); diff --git a/gix-pack/src/data/file/decode/entry.rs b/gix-pack/src/data/file/decode/entry.rs index 7dd66a449f0..f610e7ca80b 100644 --- a/gix-pack/src/data/file/decode/entry.rs +++ b/gix-pack/src/data/file/decode/entry.rs @@ -8,7 +8,7 @@ use crate::{ cache, data, data::{ File, delta, - file::decode::{DeltaBaseUnresolved, allocation_error}, + file::decode::{DeltaBaseUnresolved, allocation_error, resize_with_limit}, }, }; @@ -204,11 +204,7 @@ where match entry.header { Tree | Blob | Commit | Tag => { let size = self.decoded_object_size(entry.decompressed_size)?; - if let Some(additional) = size.checked_sub(out.len()) { - out.try_reserve(additional) - .or_raise(|| message("Entry too large to fit in memory"))?; - } - out.resize(size, 0); + resize_with_limit(out, size, self.alloc_limit_bytes)?; self.decompress_entry(&entry, inflate, out.as_mut_slice()) .map(|consumed_input| { Outcome::from_object_entry( @@ -237,6 +233,7 @@ where let mut chain = SmallVec::<[Delta; 10]>::default(); let first_entry = last.clone(); let mut cursor = last; + let mut cycle = super::DeltaCycle::new(cursor.data_offset); let mut base_buffer_size: Option<usize> = None; let mut object_kind: Option<gix_object::Kind> = None; let mut consumed_input: Option<usize> = None; @@ -259,13 +256,34 @@ where total_delta_data_size = total_delta_data_size .checked_add(cursor.decompressed_size) .ok_or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + let chain_capacity = if chain.len() == chain.capacity() { + chain + .len() + .checked_add(1) + .and_then(usize::checked_next_power_of_two) + .ok_or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))? + } else if chain.spilled() { + chain.capacity() + } else { + 0 + }; + let chain_bytes = chain_capacity + .checked_mul(std::mem::size_of::<Delta>()) + .ok_or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + let allocation_size = total_delta_data_size.max(chain_bytes as u64); if self .alloc_limit_bytes - .is_some_and(|limit| total_delta_data_size > limit as u64) + .is_some_and(|limit| allocation_size > limit as u64) { bail!(allocation_error(ResourceExhaustionKind::AllocationLimit)); } let decompressed_size = self.decoded_object_size(cursor.decompressed_size)?; + if chain_capacity > chain.capacity() { + chain + .try_reserve_exact(chain_capacity - chain.len()) + .map_err(|_| allocation_error(ResourceExhaustionKind::AllocationFailure)) + .or_error()?; + } chain.push(Delta { data: Range { start: 0, @@ -298,6 +316,7 @@ where }, _ => unreachable!("cursor.is_delta() only allows deltas here"), }; + cycle.check(cursor.data_offset)?; } // This can happen if the cache held the first entry itself @@ -330,9 +349,7 @@ where .checked_add(total_delta_data_size) .ok_or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?, }; - out.try_reserve(delta_range.end.saturating_sub(out.len())) - .or_raise(|| message("Entry too large to fit in memory"))?; - out.resize(delta_range.end, 0); + resize_with_limit(out, delta_range.end, self.alloc_limit_bytes)?; let mut instructions = &mut out[delta_range.clone()]; let mut relative_delta_start = 0; @@ -387,9 +404,7 @@ where .checked_add(second_buffer_size) .and_then(|size| size.checked_add(total_delta_data_size)) .ok_or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; - out.try_reserve(out_size.saturating_sub(out.len())) - .or_raise(|| message("Entry too large to fit in memory"))?; - out.resize(out_size, 0); + resize_with_limit(out, out_size, self.alloc_limit_bytes)?; // Now 'rescue' the deltas, because in the next step we possibly overwrite that portion // of memory with the base object (in the majority of cases) diff --git a/gix-pack/src/data/file/decode/header.rs b/gix-pack/src/data/file/decode/header.rs index b10b28bc46e..db4ce843373 100644 --- a/gix-pack/src/data/file/decode/header.rs +++ b/gix-pack/src/data/file/decode/header.rs @@ -62,6 +62,7 @@ where use crate::data::entry::Header::*; let mut num_deltas = 0; let mut first_delta_decompressed_size = None::<u64>; + let mut cycle = super::DeltaCycle::new(entry.data_offset); loop { match entry.header { Tree | Blob | Commit | Tag => { @@ -105,6 +106,7 @@ where } } } + cycle.check(entry.data_offset)?; } } diff --git a/gix-pack/src/data/file/decode/mod.rs b/gix-pack/src/data/file/decode/mod.rs index 546dfc98c47..0f2686c304d 100644 --- a/gix-pack/src/data/file/decode/mod.rs +++ b/gix-pack/src/data/file/decode/mod.rs @@ -1,8 +1,57 @@ +use gix_error::{ResourceExhaustionKind, Result, ResultExt, bail}; + /// pub mod entry; /// pub mod header; +/// Check the whole buffer size before growing it, avoiding amortized growth beyond a configured cap. +pub(crate) fn resize_with_limit(out: &mut Vec<u8>, len: usize, alloc_limit_bytes: Option<usize>) -> Result { + if alloc_limit_bytes.is_some_and(|limit| len > limit) { + bail!(allocation_error(ResourceExhaustionKind::AllocationLimit)); + } + let additional = len.saturating_sub(out.len()); + if alloc_limit_bytes.is_some() { + out.try_reserve_exact(additional) + } else { + out.try_reserve(additional) + } + .or_raise(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?; + out.resize(len, 0); + Ok(()) +} + +/// Brent's cycle detection keeps one exponentially spaced checkpoint instead of allocating +/// a visited-offset set on every header or object lookup. +struct DeltaCycle { + checkpoint: u64, + distance: usize, + interval: usize, +} + +impl DeltaCycle { + fn new(offset: u64) -> Self { + Self { + checkpoint: offset, + distance: 0, + interval: 1, + } + } + + fn check(&mut self, offset: u64) -> Result { + if offset == self.checkpoint { + bail!(gix_error::corruption("A pack delta chain contains a cycle")); + } + self.distance += 1; + if self.distance == self.interval { + self.checkpoint = offset; + self.interval = self.interval.saturating_mul(2); + self.distance = 0; + } + Ok(()) + } +} + /// A ref-delta base that could not be resolved. /// /// Its source is a classification-only [`gix_error::ClassificationMarker`]. diff --git a/gix-pack/src/data/file/init.rs b/gix-pack/src/data/file/init.rs index 449936ec4cd..f8a3d86a55f 100644 --- a/gix-pack/src/data/file/init.rs +++ b/gix-pack/src/data/file/init.rs @@ -64,6 +64,8 @@ where /// Use `None` to disable the limit, which is also the default. /// /// This is currently enforced when decoding pack entries and resolving delta chains. + /// The limit applies to the combined size of buffers that hold both work data and delta instructions. + /// Buffer growth uses exact reservations when a limit is set, and retains existing capacity. /// Callers that allocate from pack metadata directly should consult [`File::alloc_limit_bytes()`][crate::data::File::alloc_limit_bytes] /// and apply the same limit themselves. pub fn with_alloc_limit_bytes(mut self, alloc_limit_bytes: Option<usize>) -> Self { diff --git a/gix-pack/tests/pack/malformed.rs b/gix-pack/tests/pack/malformed.rs index 0fcdd8dde63..22741f8cfe7 100644 --- a/gix-pack/tests/pack/malformed.rs +++ b/gix-pack/tests/pack/malformed.rs @@ -9,6 +9,272 @@ use gix_pack::{cache, data}; const FIRST_ENTRY_OFFSET: data::Offset = data::header::SIZE as data::Offset; +#[test] +fn plain_object_buffer_growth_respects_alloc_limit() -> Result { + let bytes = blob_pack_with_declared_size(&[b'A'; 65], 65)?; + for limit in [Some(65), None] { + let pack = data::File::from_data(bytes.clone(), PathBuf::from("allocation.pack"), gix_hash::Kind::Sha1)? + .with_alloc_limit_bytes(limit); + let mut out = vec![0; 64]; + pack.decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut out, + &mut Default::default(), + &|_, _| None, + &mut cache::Never, + )?; + assert_eq!(out, [b'A'; 65], "the object at the limit must decode correctly"); + if let Some(limit) = limit { + assert!( + out.capacity() <= limit, + "buffer growth must not allocate beyond the cap" + ); + } + } + Ok(()) +} + +#[test] +fn combined_delta_work_buffers_respect_alloc_limit() -> Result { + let mut bytes = blob_pack_with_declared_size(&[b'A'; 64], 64)?; + bytes[..data::header::SIZE].copy_from_slice(&data::header::encode(data::Version::V2, 2)); + bytes.truncate(bytes.len() - 20); + let delta_offset = bytes.len() as data::Offset; + let delta = [64, 64, 0x90, 64]; + data::entry::Header::OfsDelta { + base_distance: delta_offset - FIRST_ENTRY_OFFSET, + } + .write_to(delta.len() as u64, &mut bytes)?; + bytes.extend(deflate(&delta)?); + bytes.extend([0; 20]); + + // Two 64-byte work buffers and four delta instruction bytes share one allocation. + for limit in [Some(64), Some(131), Some(132), None] { + for initial_len in [0, 100] { + let pack = data::File::from_data(bytes.clone(), PathBuf::from("allocation.pack"), gix_hash::Kind::Sha1)? + .with_alloc_limit_bytes(limit); + let mut out = vec![0; initial_len]; + let initial_capacity = out.capacity(); + let result = pack.decode_entry( + pack.entry(delta_offset)?, + &mut out, + &mut Default::default(), + &|_, _| None, + &mut cache::Never, + ); + if limit.is_some_and(|limit| limit < 132) { + let err = result.expect_err("the combined allocation must fit within the cap"); + assert!( + err.classify().any(|classification| classification.class() + == gix_error::Class::ResourceExhaustion(gix_error::ResourceExhaustionKind::AllocationLimit)), + "oversized combined buffers must report the allocation limit: {err}" + ); + } else { + result?; + assert_eq!( + out, [b'A'; 64], + "the delta preserves its base when the combined buffer fits" + ); + } + if let Some(limit) = limit { + assert!( + out.capacity() <= initial_capacity.max(limit), + "new allocations must respect the cap, including when reusing a buffer" + ); + } + } + } + Ok(()) +} + +#[test] +fn resolved_base_and_delta_instructions_respect_alloc_limit() -> Result { + let bytes = ref_delta_pack(&[64, 64, 0x90, 64])?; + let pack = data::File::from_data(bytes, PathBuf::from("allocation.pack"), gix_hash::Kind::Sha1)? + .with_alloc_limit_bytes(Some(64)); + let mut out = Vec::new(); + let err = pack + .decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut out, + &mut Default::default(), + &|_, out| { + out.clear(); + out.extend_from_slice(&[b'A'; 64]); + Some(data::decode::entry::ResolvedBase::OutOfPack { + kind: gix_object::Kind::Blob, + end: out.len(), + }) + }, + &mut cache::Never, + ) + .expect_err("the resolved base and delta instructions must fit together within the cap"); + assert!( + err.classify().any(|classification| classification.class() + == gix_error::Class::ResourceExhaustion(gix_error::ResourceExhaustionKind::AllocationLimit)), + "the initial combined buffer must report the allocation limit: {err}" + ); + assert!( + out.capacity() <= 64, + "the combined buffer must be checked before reserving instruction space" + ); + Ok(()) +} + +#[test] +fn ref_delta_header_cycles_are_rejected() -> Result { + for (num_entries, close_with_ofs) in [(1, false), (2, false), (3, false), (2, true)] { + let (pack, offsets) = ref_delta_chain(num_entries, &[0, 0], close_with_ofs)?; + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_header(pack.entry(FIRST_ENTRY_OFFSET)?, &mut Default::default(), &|base_id| { + resolutions.set(resolutions.get() + 1); + assert!(resolutions.get() < 20, "a cyclic header lookup must terminate promptly"); + let offset = offsets[usize::from(base_id.as_bytes()[0]) % offsets.len()]; + Some(data::decode::header::ResolvedBase::InPack( + pack.entry(offset).expect("the synthetic delta entry exists"), + )) + }) + .expect_err("cyclic ref-delta bases must not be accepted"); + assert!(err.is_corrupted(), "a delta cycle is corrupt pack data: {err}"); + } + Ok(()) +} + +#[test] +fn ref_delta_entry_cycles_are_rejected() -> Result { + for delta in [&[0, 0][..], &[][..]] { + for (num_entries, close_with_ofs) in [(1, false), (2, false), (3, false), (2, true)] { + let (pack, offsets) = ref_delta_chain(num_entries, delta, close_with_ofs)?; + let pack = pack.with_alloc_limit_bytes(Some(64 * 1024)); + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut Vec::new(), + &mut Default::default(), + &|base_id, _| { + resolutions.set(resolutions.get() + 1); + assert!(resolutions.get() < 20, "a cyclic object lookup must terminate promptly"); + let offset = offsets[usize::from(base_id.as_bytes()[0]) % offsets.len()]; + Some(data::decode::entry::ResolvedBase::InPack( + pack.entry(offset).expect("the synthetic delta entry exists"), + )) + }, + &mut cache::Never, + ) + .expect_err("even zero-sized cyclic deltas must be rejected"); + assert!(err.is_corrupted(), "a delta cycle is corrupt pack data: {err}"); + } + } + Ok(()) +} + +#[test] +fn delta_chain_metadata_respects_alloc_limit() -> Result { + let (pack, offsets) = ref_delta_chain(12, &[], false)?; + let pack = pack.with_alloc_limit_bytes(Some(64)); + let err = pack + .decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut Vec::new(), + &mut Default::default(), + &|base_id, _| { + Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Some(&offset) => data::decode::entry::ResolvedBase::InPack( + pack.entry(offset).expect("the synthetic delta entry exists"), + ), + None => data::decode::entry::ResolvedBase::OutOfPack { + kind: gix_object::Kind::Blob, + end: 0, + }, + }) + }, + &mut cache::Never, + ) + .expect_err("zero-sized delta payloads still require memory for their chain"); + assert_eq!( + err.classify().find_map(|classification| match classification.class() { + gix_error::Class::ResourceExhaustion(kind) => Some(kind), + _ => None, + }), + Some(gix_error::ResourceExhaustionKind::AllocationLimit), + "delta-chain storage must respect the allocation limit before decoding payloads" + ); + Ok(()) +} + +#[test] +fn forward_ref_delta_chain_is_accepted() -> Result { + let (pack, offsets) = ref_delta_chain(12, &[0, 0], false)?; + let entry = pack.entry(FIRST_ENTRY_OFFSET)?; + let header = pack.decode_header(entry.clone(), &mut Default::default(), &|base_id| { + Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Some(&offset) => data::decode::header::ResolvedBase::InPack( + pack.entry(offset).expect("the synthetic delta entry exists"), + ), + None => data::decode::header::ResolvedBase::OutOfPack { + kind: gix_object::Kind::Blob, + num_deltas: None, + }, + }) + })?; + let mut out = Vec::new(); + let decoded = pack.decode_entry( + entry, + &mut out, + &mut Default::default(), + &|base_id, _| { + Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Some(&offset) => data::decode::entry::ResolvedBase::InPack( + pack.entry(offset).expect("the synthetic delta entry exists"), + ), + None => data::decode::entry::ResolvedBase::OutOfPack { + kind: gix_object::Kind::Blob, + end: 0, + }, + }) + }, + &mut cache::Never, + )?; + assert_eq!(header.num_deltas, 12, "forward references are valid without a cycle"); + assert_eq!(decoded.num_deltas, 12, "all forward delta bases are resolved"); + assert!(out.is_empty(), "each delta preserves the empty base blob"); + Ok(()) +} + +/// Each entry refers to the next entry's numbered object ID. Tests resolve the final ID either +/// outside the pack or back to the first entry. An optional backward ofs-delta closes a mixed cycle. +/// The deliberately inflated object count ensures cycle detection cannot trust the pack header. +fn ref_delta_chain( + num_entries: u8, + delta: &[u8], + close_with_ofs: bool, +) -> Result<(data::File<Vec<u8>>, Vec<data::Offset>)> { + let mut pack = data::header::encode(data::Version::V2, u32::MAX).to_vec(); + let mut offsets = Vec::new(); + for index in 0..num_entries { + let offset = pack.len() as u64; + offsets.push(offset); + let header = if close_with_ofs && index + 1 == num_entries { + data::entry::Header::OfsDelta { + base_distance: offset - FIRST_ENTRY_OFFSET, + } + } else { + data::entry::Header::RefDelta { + base_id: gix_hash::ObjectId::from_bytes_or_panic(&[index + 1; 20]), + } + }; + header.write_to(delta.len() as u64, &mut pack)?; + pack.extend(deflate(delta)?); + } + pack.extend([0; 20]); + Ok(( + data::File::from_data(pack, PathBuf::from("ref-delta-chain.pack"), gix_hash::Kind::Sha1)?, + offsets, + )) +} + /// Reproducer for GHSA-x494-mj8g-cj27: malformed delta copy instructions currently reach /// `gix_pack::data::File::decode_entry()` and panic while slicing the base object instead of /// returning an error for attacker-controlled pack data. From f83b9691daa38ab38665bd4e3284e9242dbb6408 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Mon, 5 Oct 2026 13:44:14 +0200 Subject: [PATCH 13/28] feat(gix-config): add `File::new_section_with_meta()` for explicit section origins Creating a section with its own source, path, include level, or trust previously required changing the file's metadata first. Add `File::new_section_with_meta()` to attach owned or shared metadata directly to one new section, leaving the file's origin and the defaults for subsequent sections unchanged. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- gix-config/src/file/access/mutate.rs | 34 ++++++++- gix-config/src/file/access/read_only.rs | 5 +- gix-config/tests/config/file/access/mutate.rs | 74 +++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/gix-config/src/file/access/mutate.rs b/gix-config/src/file/access/mutate.rs index 614a751f037..841b2c2b502 100644 --- a/gix-config/src/file/access/mutate.rs +++ b/gix-config/src/file/access/mutate.rs @@ -124,7 +124,11 @@ impl File { .section_mut_from_id(id, nl) .expect("BUG: Section did not have id from lookup")) } - None => self.new_section_inner(name, subsection_name.map(bstr::BString::from)), + None => self.new_section_inner( + name, + subsection_name.map(bstr::BString::from), + OwnShared::clone(&self.meta), + ), } } @@ -173,6 +177,9 @@ impl File { /// the generated header will use the modern subsection syntax. /// Returns a reference to the new section for immediate editing. /// + /// The section inherits this file's [metadata][Self::meta()]. Use + /// [`Self::new_section_with_meta()`] to provide a different origin for just this section. + /// /// # Examples /// /// Creating a new empty section: @@ -204,11 +211,30 @@ impl File { /// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(()) /// ``` pub fn new_section(&mut self, name: impl AsRef<str>, subsection: impl IntoBStringOpt) -> Result<SectionMut<'_>> { - self.new_section_inner(name.as_ref(), subsection.into_bstring_opt()) + self.new_section_with_meta(name, subsection, OwnShared::clone(&self.meta)) } - fn new_section_inner(&mut self, name: &str, subsection: Option<bstr::BString>) -> Result<SectionMut<'_>> { - let section = file::SectionData::new(name, subsection, OwnShared::clone(&self.meta), &mut self.backing)?; + /// Like [`Self::new_section()`], but attaches the given `meta`data to the new section instead of + /// inheriting this file's metadata. + /// + /// This leaves the file's metadata and that of existing sections unchanged. Subsequent sections + /// created with [`Self::new_section()`] still inherit the file's metadata. + pub fn new_section_with_meta( + &mut self, + name: impl AsRef<str>, + subsection: impl IntoBStringOpt, + meta: impl Into<OwnShared<Metadata>>, + ) -> Result<SectionMut<'_>> { + self.new_section_inner(name.as_ref(), subsection.into_bstring_opt(), meta.into()) + } + + fn new_section_inner( + &mut self, + name: &str, + subsection: Option<bstr::BString>, + meta: OwnShared<Metadata>, + ) -> Result<SectionMut<'_>> { + let section = file::SectionData::new(name, subsection, meta, &mut self.backing)?; let id = self.push_section_internal(section); let nl = self.detect_newline_style_smallvec(); let mut section = self.section_mut_from_id(id, nl).expect("each id yields a section"); diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index 7bdfdd7f794..9a0e1a9ccea 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -426,7 +426,7 @@ impl File { /// Return this file's metadata, typically set when it was first created to indicate its origins. /// - /// It will be used in all newly created sections to identify them. + /// Newly created sections inherit it unless created with [`File::new_section_with_meta()`]. /// Change it with [`File::set_meta()`]. pub fn meta(&self) -> &Metadata { &self.meta @@ -434,7 +434,8 @@ impl File { /// Change the origin of this instance to be the given `meta`data. /// - /// This is useful to control what origin about-to-be-added sections receive. + /// This is useful to control what origin about-to-be-added sections receive by default. + /// Use [`File::new_section_with_meta()`] to set the origin of a single new section instead. pub fn set_meta(&mut self, meta: impl Into<OwnShared<Metadata>>) -> &mut Self { self.meta = meta.into(); self diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index c9ee4fe6d20..7c5a6408222 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -24,6 +24,80 @@ mod new_section { } } +mod new_section_with_meta { + use gix_config::{File, Source, file::Metadata}; + use gix_error::TestResult; + use gix_features::threading::OwnShared; + + #[test] + fn metadata_is_specific_to_the_new_section() -> TestResult { + let default_meta = Metadata::from(Source::Local).at("repository.config"); + let mut file = File::new(default_meta.clone()); + file.new_section("core", None)?; + let meta = Metadata { + level: 2, + ..Metadata::from(Source::User) + .at("user.config") + .with(gix_sec::Trust::Reduced) + }; + { + let mut section = file.new_section_with_meta("remote", "origin", meta.clone())?; + assert_eq!(section.meta(), &meta, "the editable section receives explicit metadata"); + section.push("url", Some("example".into()))?; + } + file.new_section_with_meta("user", None, OwnShared::new(meta.clone()))?; + file.new_section("core", None)?; + + assert_eq!( + file.meta(), + &default_meta, + "explicit metadata does not change the file's origin" + ); + assert_eq!( + file.sections().map(|section| section.meta()).collect::<Vec<_>>(), + [&default_meta, &meta, &meta, &default_meta], + "owned and shared metadata persist without affecting existing or subsequent sections" + ); + assert_eq!( + file.section("remote", "origin")?.meta(), + &meta, + "lookup retains the section's explicit origin" + ); + let nl = if cfg!(windows) { "\r\n" } else { "\n" }; + assert_eq!( + file.to_string(), + format!("[core]{nl}[remote \"origin\"]{nl}\turl = example{nl}[user]{nl}[core]{nl}"), + "explicit metadata preserves normal section formatting and value insertion" + ); + Ok(()) + } + + #[test] + fn invalid_names_leave_the_file_unchanged() -> TestResult { + let mut file = File::default(); + file.new_section("core", None)?; + let before = file.to_string(); + for (name, subsection) in [("invalid.name", None), ("remote", Some("invalid\nsubsection"))] { + assert!( + file.new_section_with_meta(name, subsection.map(bstr::BString::from), Metadata::from(Source::Local)) + .is_err(), + "explicit metadata does not bypass section-header validation" + ); + assert_eq!( + file.to_string(), + before, + "invalid headers do not change the file's contents" + ); + assert_eq!( + file.meta(), + &Metadata::api(), + "errors do not change the file's metadata" + ); + } + Ok(()) + } +} + mod remove_section { use crate::Result; From c860df3659fccd800cdc0b56a0e9851227ce75b3 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:22:24 +0200 Subject: [PATCH 14/28] fix(gix): honor section trust when merging submodule overrides <!-- agent --> A report describes reduced-trust repository configuration overriding submodule settings, including command-valued updates. Apply the repository's existing configurable section filter for worktree, index, and HEAD module sources, and retain each copied section's origin metadata. Keep trusted API overrides usable and restrict module enumeration to the original module definitions. The regressions first reproduced both the trust bypass and lost provenance, then covered custom filters and every source. Git reference: `setup.c::ensure_valid_ownership`, `submodule-config.c`, and `builtin/submodule--helper.c` at `d38352cd43ab9745686d697872408bc3249a153f` distinguish shared module definitions from trusted local overrides. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gix-submodule/src/access.rs | 2 +- gix-submodule/src/lib.rs | 56 +++----- gix-submodule/tests/file/mod.rs | 40 ++++++ gix/src/repository/submodule.rs | 46 ++++--- .../fixtures/generated-archives/.gitignore | 3 + .../make_submodule_config_overrides.sh | 34 +++++ gix/tests/gix/repository/submodule.rs | 121 ++++++++++++++++-- 7 files changed, 230 insertions(+), 72 deletions(-) create mode 100755 gix/tests/fixtures/make_submodule_config_overrides.sh diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index ac05b24a0f1..e4409e10cb3 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -39,7 +39,7 @@ impl File { .filter_map(move |s| { s.header() .subsection_name() - .filter(|_| s.meta().source == crate::init::META_MARKER) + .filter(|_| std::ptr::eq(s.meta(), self.config.meta())) .filter(|name| seen.insert(*name)) }) } diff --git a/gix-submodule/src/lib.rs b/gix-submodule/src/lib.rs index 1e8ad63cfc6..f47edf8eb27 100644 --- a/gix-submodule/src/lib.rs +++ b/gix-submodule/src/lib.rs @@ -2,10 +2,8 @@ #![deny(missing_docs)] #![forbid(unsafe_code)] -use gix_error::Result; -use std::collections::BTreeMap; - use bstr::ByteSlice; +use gix_error::Result; /// All relevant information about a git module, typically from `.gitmodules` files. /// @@ -40,49 +38,28 @@ impl File { /// * `update` /// * `branch` /// - /// These values aren't validated yet, which will happen upon query. + /// These values aren't validated yet, which will happen upon query. The caller must provide + /// trusted configuration; each copied value retains its section's origin metadata. pub fn append_submodule_overrides(&mut self, config: &gix_config::File) -> Result<&mut Self> { - let mut values = BTreeMap::<_, Vec<_>>::new(); + let mut config_to_append = gix_config::File::new(config.meta_owned()); for (module_name, section) in config .sections_by_name("submodule") .into_iter() .flatten() .filter_map(|s| s.header().subsection_name().map(|n| (n, s))) { - for field in ["url", "fetchRecurseSubmodules", "ignore", "update", "branch"] { - if let Some(value) = section.value(field) { - values.entry((module_name, field)).or_default().push(value); - } + let values = ["url", "fetchRecurseSubmodules", "ignore", "update", "branch"] + .map(|field| (field, section.value(field))); + if values.iter().all(|(_, value)| value.is_none()) { + continue; } - } - - let values = { - let mut v: Vec<_> = values.into_iter().collect(); - v.sort_by_key(|a| a.0.0); - v - }; - - let mut config_to_append = gix_config::File::new(config.meta_owned()); - let mut prev_name = None; - for ((module_name, field), values) in values { - if prev_name != Some(module_name) { - config_to_append - .new_section("submodule", module_name) - .expect("all names come from valid configuration, so remain valid"); - prev_name = Some(module_name); + config_to_append.set_meta(section.meta().clone()); + let mut destination = config_to_append.new_section("submodule", module_name)?; + for (field, value) in values { + if let Some(value) = value { + destination.push(field, Some(value.as_bstr()))?; + } } - config_to_append - .section_mut("submodule", Some(module_name)) - .expect("always set at this point") - .push( - field, - Some( - values - .last() - .expect("at least one value or we wouldn't be here") - .as_bstr(), - ), - )?; } self.config.append(config_to_append)?; @@ -108,9 +85,6 @@ pub mod init { } } - /// A marker we use when listing names to not pick them up from overridden sections. - pub(crate) const META_MARKER: gix_config::Source = gix_config::Source::Api; - impl File { /// Parse `bytes` as git configuration, typically from `.gitmodules`, without doing any further validation. /// `path` can be provided to keep track of where the file was read from in the underlying [`config`](Self::config()) @@ -126,7 +100,7 @@ pub mod init { /// on the caller to assure the input data can be trusted. pub fn from_bytes(bytes: &[u8], path: impl Into<Option<PathBuf>>, config: &gix_config::File) -> Result<Self> { let metadata = { - let mut meta = gix_config::file::Metadata::from(META_MARKER); + let mut meta = gix_config::file::Metadata::api(); meta.path = path.into(); meta }; diff --git a/gix-submodule/tests/file/mod.rs b/gix-submodule/tests/file/mod.rs index 347376762b2..95c9f1c2c45 100644 --- a/gix-submodule/tests/file/mod.rs +++ b/gix-submodule/tests/file/mod.rs @@ -540,6 +540,46 @@ mod append_submodule_overrides { use crate::file::submodule; + #[test] + fn overrides_preserve_their_section_metadata() -> Result { + use gix_config::{Source, file::Metadata}; + + let mut module = submodule( + "[submodule.a] + url = from-module + update = checkout", + ); + let mut config = gix_config::File::new(Metadata::from(Source::Local).at("repository.config")); + config + .new_section("submodule", "a")? + .push("url", Some("local".into()))?; + config + .new_section_with_meta("submodule", "a", Metadata::from(Source::User).at("user.config"))? + .push("update", Some("!trusted".into()))?; + config + .new_section_with_meta("submodule", "other", Metadata::api())? + .push("url", Some("api".into()))?; + module.append_submodule_overrides(&config)?; + assert_eq!( + module.names().collect::<Vec<_>>(), + ["a"], + "API overrides do not define new modules" + ); + for (key, source, path) in [ + ("submodule.a.url", Source::Local, "repository.config"), + ("submodule.a.update", Source::User, "user.config"), + ] { + let (_, section) = module.config().raw_value_with_section_filter(key, |_| true)?; + assert_eq!(section.meta().source, source, "each winning value keeps its own origin"); + assert_eq!( + section.meta().path.as_deref(), + Some(std::path::Path::new(path)), + "source paths survive merging" + ); + } + Ok(()) + } + #[test] fn last_of_multiple_values_wins() -> Result { let mut module = submodule("[submodule.a] url = from-module"); diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index 9e0c9ab2752..ad0b30c821b 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -1,38 +1,32 @@ -use std::rc::Rc; +use std::{io::Read, rc::Rc}; +use crate::error::{ErrorExt, ResultExt, message}; use crate::{Repository, Result, submodule}; -use gix_error::{ResultExt, bail}; +use gix_fs::FileOrSymlink; impl Repository { /// Open the `.gitmodules` file as present in the worktree, or return `None` if no such file is available. /// Symlinked worktree `.gitmodules` files are silently ignored so content outside the repository /// cannot become active submodule configuration by being linked into the worktree. /// Note that git configuration is also contributing to the result based on the current snapshot. + /// Only sections accepted by the repository's configuration filter contribute overrides. /// /// Note that his method will not look in other places, like the index or the `HEAD` tree. - // TODO(submodule): make it use an updated snapshot instead once we have `config()`. pub fn open_modules_file(&self) -> Result<Option<gix_submodule::File>> { let path = match self.modules_path() { Some(path) => path, None => return Ok(None), }; - // TODO(ErrorKind): we want to use `ErrorKind::FilesystemLoop`, which otherwise happens - // when doing `gix_fs::options_no_follow()`, - // so we could catch NotFound along with it and save the extra check. - let metadata = match std::fs::symlink_metadata(&path) { - Ok(metadata) => metadata, + let mut file = match gix_fs::open_read_only_no_follow(&path) { + Ok(FileOrSymlink::File(file)) => file, + Ok(FileOrSymlink::Symlink) => return Ok(None), Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => bail!(err), + Err(err) => return Err(err.and_raise(message("Could not open '.gitmodules' file"))), }; - if metadata.file_type().is_symlink() { - return Ok(None); - } - let buf = std::fs::read(&path).or_raise(|| gix_error::message("Could not read '.gitmodules' file"))?; - Ok(Some(gix_submodule::File::from_bytes( - &buf, - path, - &self.config.resolved, - )?)) + let mut buf = Vec::new(); + file.read_to_end(&mut buf) + .or_raise(|| message("Could not read '.gitmodules' file"))?; + Ok(Some(self.modules_from_bytes(&buf, Some(path))?)) } /// Return a shared [`.gitmodules` file](submodule::File) which is updated automatically if the in-memory snapshot @@ -81,12 +75,26 @@ impl Repository { }, }; Ok(Some(gix_features::threading::OwnShared::new( - gix_submodule::File::from_bytes(&self.find_object(id)?.data, None, &self.config.resolved)?.into(), + self.modules_from_bytes(&self.find_object(id)?.data, None)?.into(), ))) } } } + fn modules_from_bytes(&self, bytes: &[u8], path: Option<std::path::PathBuf>) -> Result<gix_submodule::File> { + let mut overrides = gix_config::File::new(self.config.resolved.meta_owned()); + for section in self + .config + .resolved + .sections_by_name_and_filter("submodule", self.filter_config_section()) + .into_iter() + .flatten() + { + overrides.push_section(section.to_owned())?; + } + gix_submodule::File::from_bytes(bytes, path, &overrides) + } + /// Return the list of available submodules, or `None` if there is no submodule configuration. #[doc(alias = "git2")] pub fn submodules(&self) -> Result<Option<impl Iterator<Item = crate::Submodule<'_>>>> { diff --git a/gix/tests/fixtures/generated-archives/.gitignore b/gix/tests/fixtures/generated-archives/.gitignore index 53967aff452..2c4ed56fe90 100644 --- a/gix/tests/fixtures/generated-archives/.gitignore +++ b/gix/tests/fixtures/generated-archives/.gitignore @@ -60,6 +60,9 @@ # Generate this small uninitialized-submodule fixture instead of storing its archives. /make_uninitialized_submodule.tar /make_uninitialized_submodule_sha256.tar +# Generate this small submodule-config fixture instead of storing its archives. +/make_submodule_config_overrides.tar +/make_submodule_config_overrides_sha256.tar # Uses `mkfifo`, `ln -s`, executable-bit changes, and adds a submodule — # none of which round-trip cleanly through a committed tar across platforms. /repo_with_untracked_files.tar diff --git a/gix/tests/fixtures/make_submodule_config_overrides.sh b/gix/tests/fixtures/make_submodule_config_overrides.sh new file mode 100755 index 00000000000..b197b12b944 --- /dev/null +++ b/gix/tests/fixtures/make_submodule_config_overrides.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Repository-local overrides conflict with every supported .gitmodules setting, +# including a command update that must only be available to trusted sections. +git init worktree +(cd worktree + cat >.gitmodules <<EOF +[submodule "s"] + path = s + url = https://safe.example/s + update = checkout + ignore = none + branch = main + fetchRecurseSubmodules = true +EOF + cat >>.git/config <<EOF + +[submodule "s"] + url = https://override.example/s + update = !override + ignore = all + branch = override + fetchRecurseSubmodules = false +EOF + git add .gitmodules + git commit -m modules +) + +# Force each fallback source without mutating the shared fixture during tests. +cp -R worktree index +rm index/.gitmodules +cp -R index tree +rm tree/.git/index diff --git a/gix/tests/gix/repository/submodule.rs b/gix/tests/gix/repository/submodule.rs index e04ebaeaefb..ae809765917 100644 --- a/gix/tests/gix/repository/submodule.rs +++ b/gix/tests/gix/repository/submodule.rs @@ -2,6 +2,65 @@ mod modules_file { use crate::Result; use crate::submodule::repo; + #[test] + fn overrides_respect_section_trust_for_every_modules_source() -> Result { + use gix::submodule::config::Update; + use gix_sec::Trust; + + let dir = gix_testtools::scripted_fixture_read_only("make_submodule_config_overrides.sh")?; + for source in ["worktree", "index", "tree"] { + let repo_dir = dir.join(source); + for (options, use_overrides) in [ + (crate::restricted().with(Trust::Reduced), false), + (crate::restricted().with(Trust::Full), true), + ( + crate::restricted().with(Trust::Full).filter_config_section(|_| false), + false, + ), + ( + crate::restricted().with(Trust::Reduced).filter_config_section(|_| true), + true, + ), + ] { + let repo = gix::open_opts(&repo_dir, options)?; + let modules = repo.modules()?.expect("the module is available from each source"); + assert_eq!( + modules.update("s".into())?, + Some(if use_overrides { + Update::Command("override".into()) + } else { + Update::Checkout + }), + "{source}: update commands must respect the repository's section filter" + ); + for (field, original, overridden) in [ + ("url", "https://safe.example/s", "https://override.example/s"), + ("ignore", "none", "all"), + ("branch", "main", "override"), + ("fetchRecurseSubmodules", "true", "false"), + ] { + assert_eq!( + modules.config().string(&format!("submodule.s.{field}")), + Some(if use_overrides { overridden } else { original }.into()), + "{source}: {field} uses the same trust policy as update" + ); + } + } + let repo = gix::open_opts( + &repo_dir, + crate::restricted() + .with(Trust::Reduced) + .config_overrides(["submodule.s.update=!trusted"]), + )?; + assert_eq!( + repo.modules()?.expect("module exists").update("s".into())?, + Some(Update::Command("trusted".into())), + "{source}: trusted API overrides remain usable in reduced-trust repositories" + ); + } + Ok(()) + } + #[test] fn none_if_not_present() -> Result { let repo = repo("module1")?; @@ -103,8 +162,7 @@ mod advisory { use crate::Result; use gix_testtools::tempfile; - /// Reproducer for GHSA-pg4w-g64p-qwhj: `Repository::open_modules_file()` and - /// `Repository::submodules()` currently follow a symlinked worktree `.gitmodules`, allowing + /// Regression test for GHSA-pg4w-g64p-qwhj: a symlinked worktree `.gitmodules` must not allow /// attacker-controlled bytes outside the repository to define submodule configuration. #[test] fn symlinked_gitmodules_are_rejected() -> Result { @@ -122,15 +180,56 @@ mod advisory { unix_fs::symlink(&outside_modules, repo_dir.join(".gitmodules"))?; std::fs::create_dir(repo_dir.join("escaped"))?; - let repo = gix::open_opts(&repo_dir, crate::restricted())?; - assert!( - repo.open_modules_file()?.is_none(), - "worktree `.gitmodules` symlinks should not be followed outside the repository" - ); - assert!( - repo.submodules()?.is_none(), - "attacker-controlled `.gitmodules` content outside the repository should not become active submodule configuration" - ); + for target_exists in [true, false] { + if !target_exists { + std::fs::remove_file(&outside_modules)?; + } + let repo = gix::open_opts(&repo_dir, crate::restricted())?; + assert!( + repo.open_modules_file()?.is_none(), + "live and dangling worktree `.gitmodules` symlinks must both be ignored" + ); + assert!( + repo.submodules()?.is_none(), + "attacker-controlled `.gitmodules` content outside the repository should not become active submodule configuration" + ); + } + Ok(()) + } + + #[test] + fn symlinked_gitmodules_fall_back_to_index_and_tree() -> Result { + let temp = tempfile::tempdir()?; + let repo_dir = temp.path().join("repo"); + crate::init_repo_isolated(&repo_dir, gix::create::Kind::WithWorktree)?; + let modules_path = repo_dir.join(".gitmodules"); + std::fs::write( + &modules_path, + "[submodule \"safe\"]\npath = safe\nurl = https://example.invalid/safe\n", + )?; + gix_testtools::git(&repo_dir, "add .gitmodules")?; + gix_testtools::git(&repo_dir, "commit -m modules")?; + std::fs::remove_file(&modules_path)?; + let outside_modules = temp.path().join("outside.gitmodules"); + std::fs::write( + &outside_modules, + "[submodule \"escaped\"]\npath = escaped\nurl = https://example.invalid/escaped\n", + )?; + std::os::unix::fs::symlink(&outside_modules, &modules_path)?; + + for source in ["index", "tree"] { + if source == "tree" { + std::fs::remove_file(repo_dir.join(".git/index"))?; + } + let repo = gix::open_opts(&repo_dir, crate::restricted())?; + assert!(repo.open_modules_file()?.is_none(), "the worktree symlink is ignored"); + let modules = repo.modules()?.expect("tracked `.gitmodules` remains available"); + assert_eq!( + modules.names().collect::<Vec<_>>(), + &["safe"], + "{source}: only tracked configuration may define submodules" + ); + } Ok(()) } } From 4fc96a05d6766289056bfd9879d170d3ebd0898b Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:24:22 +0200 Subject: [PATCH 15/28] fix(gix-credentials): preserve credential identity when constructing URLs <!-- agent --> Multiple reports describe an issue about same URL-component confusion: reserved characters in a credential username can change the host selected when the credential context is serialized and parsed again. Percent-encode each URL component with its own delimiters, and preserve supplied host and credential fields when validating a synthesized URL. Keep explicit URL precedence, ports, IPv6 and scheme-specific host escaping, and retain the existing HTTP-path normalization behavior. The helper-level regression first reproduced a request for a different host; component tests cover reserved characters, percent escapes, Unicode and path handling. Git reference: `credential.c:223`, `strbuf.c:502` and `t/t0300-credentials.sh:662-715` at `d38352cd43ab9745686d697872408bc3249a153f` encode credentials as URL components. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- Cargo.lock | 1 + gix-credentials/Cargo.toml | 1 + gix-credentials/src/protocol/context/mod.rs | 40 +++-- gix-credentials/tests/helper/cascade.rs | 35 +++++ gix-credentials/tests/protocol/context.rs | 162 ++++++++++++++++++++ 5 files changed, 229 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a70f342e654..f5dff52096b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,7 @@ dependencies = [ "gix-trace", "gix-url", "insta", + "percent-encoding", "serde", ] diff --git a/gix-credentials/Cargo.toml b/gix-credentials/Cargo.toml index 19d181a9db7..af07e17837a 100644 --- a/gix-credentials/Cargo.toml +++ b/gix-credentials/Cargo.toml @@ -32,6 +32,7 @@ gix-trace = { version = "^0.2.0", path = "../gix-trace" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } bstr = { version = "1.12.0", default-features = false, features = ["std"] } +percent-encoding = "2.3.1" diff --git a/gix-credentials/src/protocol/context/mod.rs b/gix-credentials/src/protocol/context/mod.rs index 0bb519d5417..6ed00fd3a1d 100644 --- a/gix-credentials/src/protocol/context/mod.rs +++ b/gix-credentials/src/protocol/context/mod.rs @@ -15,9 +15,16 @@ impl Context { mod access { use bstr::BString; + use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_encode, utf8_percent_encode}; use crate::protocol::Context; + const COMPONENT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'-').remove(b'.').remove(b'_').remove(b'~'); + const PROTOCOL: &AsciiSet = &COMPONENT.remove(b'+'); + const HOST: &AsciiSet = &COMPONENT.remove(b':').remove(b'[').remove(b']'); + const HTTP_HOST: &AsciiSet = &HOST.remove(b'%'); + const PATH: &AsciiSet = &COMPONENT.remove(b'/'); + impl Context { /// Clear all fields that are considered secret. pub fn clear_secrets(&mut self) { @@ -59,23 +66,31 @@ mod access { self } - /// Convert all relevant fields into a URL for consumption. + /// Convert all relevant fields into a URL for consumption, escaping component delimiters. + /// Passwords are omitted, and a missing protocol yields `None`. pub fn to_url(&self) -> Option<BString> { use bstr::{ByteSlice, ByteVec}; - let mut buf: BString = self.protocol.clone()?.into(); + let protocol = self.protocol.as_deref()?; + let mut buf: BString = utf8_percent_encode(protocol, PROTOCOL).to_string().into(); buf.push_str(b"://"); if let Some(user) = &self.username { - buf.push_str(user); + buf.push_str(utf8_percent_encode(user, COMPONENT).to_string()); buf.push(b'@'); } if let Some(host) = &self.host { - buf.push_str(host); + // HTTP and file hosts retain URL escapes in gix-url, while other schemes decode them. + let encode_set = if matches!(protocol, "http" | "https" | "file") { + HTTP_HOST + } else { + HOST + }; + buf.push_str(utf8_percent_encode(host, encode_set).to_string()); } if let Some(path) = &self.path { if !path.starts_with_str("/") { buf.push(b'/'); } - buf.push_str(path); + buf.push_str(percent_encode(path, PATH).to_string()); } buf.into() } @@ -102,14 +117,23 @@ mod mutate { /// Destructure the url at our `url` field into parts like protocol, host, username and path and store /// them in our respective fields. If `use_http_path` is set, http paths are significant even though /// normally this isn't the case. + /// If no URL is supplied, construct and validate one without reinterpreting the supplied host or credentials. pub fn destructure_url_in_place(&mut self, use_http_path: bool) -> Result<&mut Self> { - if self.url.is_none() { + let from_components = self.url.is_none(); + if from_components { self.url = Some(self.to_url().ok_or_raise(|| { validation("Either 'url' field or both 'protocol' and 'host' fields must be provided") })?); } let url = gix_url::parse(self.url.as_ref().expect("URL is present after check above"))?; + if !matches!(url.scheme, gix_url::Scheme::Http | gix_url::Scheme::Https) || use_http_path { + let path = url.path.trim_with(|b| b == '/'); + self.path = (!path.is_empty()).then(|| path.into()); + } + if from_components { + return Ok(self); + } self.protocol = Some(url.scheme.as_str().into()); self.username = url.user().map(ToOwned::to_owned); self.password = url.password().map(ToOwned::to_owned); @@ -125,10 +149,6 @@ mod mutate { } host }); - if !matches!(url.scheme, gix_url::Scheme::Http | gix_url::Scheme::Https) || use_http_path { - let path = url.path.trim_with(|b| b == '/'); - self.path = (!path.is_empty()).then(|| path.into()); - } Ok(self) } } diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index 4ac5e5b7299..4ba426e9c1f 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -9,6 +9,41 @@ mod invoke { }; use gix_sec::identity::Account; + #[test] + fn reconstructed_urls_cannot_change_the_host_sent_to_helpers() -> Result { + for user in ["victim@trusted.example?", "victim@trusted.example/", "github.com/"] { + let outcome = Cascade::default() + .extend([Program::from_custom_definition( + r#"!f() { + while IFS= read -r line; do + case "$line" in host=*) printf 'password=%s\n' "${line#host=}" ;; esac + done + }; f"#, + )]) + .invoke( + Action::Get(Context { + protocol: Some("https".into()), + host: Some("evil.example".into()), + username: Some(user.into()), + ..Default::default() + }), + gix_prompt::Options { + mode: gix_prompt::Mode::Disable, + askpass: None, + }, + )? + .expect("the helper reports the host for which it was asked to obtain credentials"); + assert_eq!( + outcome.identity, + identity(user, "evil.example"), + "the helper sees the requested host and the complete username; otherwise URL delimiters in \ + the username could redirect credential lookup to trusted.example or github.com, exposing \ + that host's credentials when the caller authenticates to evil.example" + ); + } + Ok(()) + } + #[test] fn invalid_authentication_challenges_fail_without_helpers() { let mut error_snapshots = Vec::new(); diff --git a/gix-credentials/tests/protocol/context.rs b/gix-credentials/tests/protocol/context.rs index ad76fc7ecee..9b3310e4c9e 100644 --- a/gix-credentials/tests/protocol/context.rs +++ b/gix-credentials/tests/protocol/context.rs @@ -79,6 +79,68 @@ mod destructure_url_in_place { assert_eq_parts("https://example.com/a%2Fb/", "https", None, "example.com", "a/b", true); } + #[test] + fn component_paths_retain_existing_http_path_semantics() -> Result { + for protocol in ["https", "ssh"] { + for use_http_path in [false, true] { + for (path, normalized) in [("/repo/", Some("repo")), ("/", None)] { + let mut ctx = Context { + protocol: Some(protocol.into()), + host: Some("example.com".into()), + path: Some(path.into()), + ..Default::default() + }; + ctx.destructure_url_in_place(use_http_path)?; + let expected = if protocol == "https" && !use_http_path { + Some(path) + } else { + normalized + }; + assert_eq!( + ctx.path.as_deref().map(Vec::as_slice), + expected.map(str::as_bytes), + "supplied HTTP paths are retained when disabled; used paths are relative and root paths absent" + ); + } + } + } + Ok(()) + } + + #[test] + fn an_explicit_url_still_replaces_supplied_components() -> Result { + let mut ctx = Context { + url: Some("https://url-user:url-password@url.example/repo".into()), + protocol: Some("http".into()), + host: Some("components.example".into()), + username: Some("component-user".into()), + password: Some("component-password".into()), + ..Default::default() + }; + ctx.destructure_url_in_place(true)?; + assert_eq!( + ctx.protocol.as_deref(), + Some("https"), + "an explicit URL defines the protocol" + ); + assert_eq!( + ctx.host.as_deref(), + Some("url.example"), + "an explicit URL defines the host" + ); + assert_eq!( + ctx.username.as_deref(), + Some("url-user"), + "an explicit URL defines the user" + ); + assert_eq!( + ctx.password.as_deref(), + Some("url-password"), + "an explicit URL defines the password" + ); + Ok(()) + } + #[test] fn protocol_and_host_with_path_without_url_constructs_full_url() { let mut ctx = Context { @@ -142,8 +204,108 @@ mod to_prompt { } mod to_url { + use crate::Result; use gix_credentials::protocol::Context; + #[test] + fn component_delimiters_cannot_change_the_credential_identity() -> Result { + for protocol in ["http", "https", "ssh", "git"] { + for user in [ + "github.com/", + "victim@trusted.example/", + "victim@trusted.example?", + "victim@trusted.example#", + "user:password", + "user%2Fname", + "jĂśrg", + ] { + let mut ctx = Context { + protocol: Some(protocol.into()), + host: Some("evil.example:8443".into()), + username: Some(user.into()), + password: Some("already-supplied".into()), + path: Some("repo%2Fwith space/?#@".into()), + ..Default::default() + }; + let encoded = ctx.to_url().expect("a protocol is present"); + let parsed = gix_url::parse(&encoded)?; + assert_eq!(parsed.user(), Some(user), "user delimiters remain part of the username"); + assert_eq!(parsed.host(), Some("evil.example"), "the requested host cannot change"); + assert_eq!( + parsed.port, + Some(8443), + "the explicit port remains an authority component" + ); + assert_eq!( + parsed.password(), + None, + "the synthesized URL never contains the password" + ); + assert_eq!( + parsed.path, "/repo%2Fwith space/?#@", + "decoded path bytes survive serialization" + ); + + let mut expected = ctx.clone(); + expected.url = Some(encoded); + ctx.destructure_url_in_place(true)?; + assert_eq!( + ctx, expected, + "constructing a URL does not reinterpret supplied credentials" + ); + } + } + Ok(()) + } + + #[test] + fn hosts_keep_ports_ipv6_and_scheme_specific_percent_encoding() -> Result { + for (protocol, host, encoded_host) in [ + ("https", "[::1]:8443", "[::1]:8443"), + ("https", "[fe80::1%25eth0]:8443", "[fe80::1%25eth0]:8443"), + ("https", "exa%20mple.example:8443", "exa%20mple.example:8443"), + ("ssh", "[fe80::1%eth0]:8443", "[fe80::1%25eth0]:8443"), + ("ssh", "exa%mple.example:8443", "exa%25mple.example:8443"), + ( + "https", + "trusted.example/path@evil.example", + "trusted.example%2Fpath%40evil.example", + ), + ] { + let mut ctx = Context { + protocol: Some(protocol.into()), + host: Some(host.into()), + path: Some("repo".into()), + ..Default::default() + }; + assert_eq!( + ctx.to_url().expect("a protocol is present"), + format!("{protocol}://{encoded_host}/repo"), + "host encoding preserves ports and IPv6 without introducing authority delimiters" + ); + ctx.destructure_url_in_place(true)?; + assert_eq!( + ctx.host.as_deref(), + Some(host), + "the original host field reaches helpers intact" + ); + } + Ok(()) + } + + #[test] + fn protocol_delimiters_cannot_introduce_an_authority() { + let mut ctx = Context { + protocol: Some("https://trusted.example/".into()), + host: Some("evil.example".into()), + ..Default::default() + }; + assert!( + ctx.destructure_url_in_place(false).is_err(), + "a malformed protocol cannot select a different host" + ); + } + #[test] fn no_protocol_is_nothing() { assert_eq!(Context::default().to_url(), None); From 781b6061ee8bb1c25e952ea0de57697029c0ff79 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:24:45 +0200 Subject: [PATCH 16/28] fix(gix-pack): reject incomplete delta traversals and overlapping offsets <!-- agent --> A Report describes indexed delta components that cannot be reached from any base object yet pass delta-tree verification. Require every indexed item to have been inspected before returning success, using the existing object counter. Legitimate forward references remain supported. Return corruption errors for duplicate or overlapping index offsets and for an entry at or past the pack end, instead of panicking. The reported decoder cycle and allocation problem is handled by the fix. Three regressions failed before the fix: an unresolved cycle was accepted, while duplicate offsets and an invalid pack end panicked. Coverage includes self and multi-node components with and without a valid root, plus a valid forward delta base. Git reference: `builtin/index-pack.c::conclude_pack()` at `d38352cd43ab9745686d697872408bc3249a153f` rejects unresolved deltas. Also: progress initialization does not reset every progress implementation. Explicitly reset the object count before traversal so a reused logging progress instance cannot make a valid pack fail the completeness check. The new real-Log regression failed before this correction. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- Cargo.lock | 1 + gix-pack/Cargo.toml | 1 + gix-pack/src/cache/delta/from_offsets.rs | 2 +- gix-pack/src/cache/delta/traverse/mod.rs | 9 +- gix-pack/src/cache/delta/traverse/resolve.rs | 99 ++++++++++++++++++++ gix-pack/src/cache/delta/tree.rs | 41 +++++++- 6 files changed, 148 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5dff52096b..949fe5e4237 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2265,6 +2265,7 @@ dependencies = [ "maplit", "memmap2", "parking_lot", + "prodash", "serde", "smallvec", "uluru", diff --git a/gix-pack/Cargo.toml b/gix-pack/Cargo.toml index 2151c60af6e..b4f9df36276 100644 --- a/gix-pack/Cargo.toml +++ b/gix-pack/Cargo.toml @@ -85,6 +85,7 @@ gix-traverse = { path = "../gix-traverse" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } maplit = "1.0.2" criterion = "0.8.2" +prodash = { version = "31.0.0", features = ["progress-log"] } insta = "1.46.3" [package.metadata.docs.rs] diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index 8ad785d61d3..413c4abae48 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -109,7 +109,7 @@ impl<T> Tree<T> { ) -> Result { let bytes_to_skip: u64 = pack_offset .checked_sub(previous_offset) - .expect("continuously ascending pack offsets"); + .ok_or_raise(|| gix_error::corruption("Pack index offsets overlap an entry header"))?; if bytes_to_skip == 0 { return Ok(()); } diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index 67b51151df8..2e4162251a9 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -1,5 +1,5 @@ use gix_error::Result; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use gix_error::bail; use gix_features::{ @@ -115,6 +115,7 @@ where let object_counter = { let progress = &mut object_progress; progress.init(Some(num_objects), progress::count("objects")); + progress.set(0); progress.counter() }; size_progress.init(None, progress::bytes()); @@ -134,7 +135,7 @@ where &child_items, thread_limit, num_objects, - object_counter, + object_counter.clone(), size_counter, &resolver_progress, resolve, @@ -153,6 +154,10 @@ where bail!("The ref-delta base object {base_id} could not be found".not_found()); } + if object_counter.load(Ordering::Relaxed) != num_objects { + bail!(gix_error::corruption("Pack delta traversal left unresolved objects")); + } + object_progress.show_throughput(start); size_progress.show_throughput(start); diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index 270ecaf4f21..93014a9c5ac 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -689,6 +689,105 @@ mod tests { Ok(()) } + #[test] + fn traversal_rejects_unreachable_delta_cycles() -> gix_testtools::Result { + for has_root in [false, true] { + for cycle_len in [1, 2, 3] { + let mut pack = Vec::new(); + let mut tree = Tree::with_capacity(cycle_len + usize::from(has_root), None)?; + if has_root { + let root = append_entry(&mut pack, data::entry::Header::Blob, 0, b""); + tree.add_root(root, ())?; + } + let offsets: Vec<_> = (0..cycle_len) + .map(|_| { + append_entry( + &mut pack, + data::entry::Header::RefDelta { + base_id: gix_hash::Kind::Sha1.null(), + }, + 2, + &[0, 0], + ) + }) + .collect(); + for (index, &offset) in offsets.iter().enumerate() { + tree.add_child(offsets[(index + 1) % cycle_len], offset, ())?; + } + let err = traverse( + tree, + &pack, + Some(2), + None, + |slice, pack| pack.get(slice.start as usize..slice.end as usize), + |(), _, _| Ok(()), + ) + .expect_err("every delta must be inspected, including components without a root"); + assert!(err.is_corrupted(), "an unreachable delta cycle is corrupt pack data"); + } + } + Ok(()) + } + + #[test] + fn traversal_accepts_forward_delta_bases() -> gix_testtools::Result { + let mut pack = Vec::new(); + let child = append_entry( + &mut pack, + data::entry::Header::RefDelta { + base_id: gix_hash::Kind::Sha1.null(), + }, + 2, + &[0, 0], + ); + let root = append_entry(&mut pack, data::entry::Header::Blob, 0, b""); + let mut tree = Tree::with_capacity(2, None)?; + tree.add_child(root, child, ())?; + tree.add_root(root, ())?; + traverse( + tree, + &pack, + Some(1), + None, + |slice, pack| pack.get(slice.start as usize..slice.end as usize), + |(), _, _| Ok(()), + )?; + Ok(()) + } + + #[test] + fn traversal_accepts_reused_progress() -> gix_testtools::Result { + use progress::Count; + + let mut pack = Vec::new(); + let root = append_entry(&mut pack, data::entry::Header::Blob, 0, b""); + let mut tree = Tree::with_capacity(1, None)?; + tree.add_root(root, ())?; + let object_progress = prodash::progress::Log::new("reused", Some(0)); + object_progress.set(3); // A previous indexing or traversal operation already counted objects. + let counter = object_progress.counter(); + tree.traverse( + |slice, pack: &Vec<u8>| pack.get(slice.start as usize..slice.end as usize), + &pack, + pack.len() as u64, + |(), _, _| Ok(()), + traverse::Options { + object_progress: Box::new(object_progress), + size_progress: &mut progress::Discard, + thread_limit: Some(1), + should_interrupt: &AtomicBool::new(false), + object_hash: gix_hash::Kind::Sha1, + alloc_limit_bytes: None, + }, + )?; + assert_eq!( + counter.load(Ordering::Relaxed), + 1, + "only this traversal's objects are counted" + ); + Ok(()) + } + #[test] fn traversal_resolves_children_lazily() { let mut pack = Vec::new(); diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index ad586c67d4b..6917716cf5a 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -132,8 +132,6 @@ impl<T> Tree<T> { } self.assert_is_incrementing_and_update_next_offset(pack_entries_end) - .expect("BUG: pack now is smaller than all previously seen entries"); - Ok(()) } /// Add a new root node, one that only has children but is not a child itself, at the given pack `offset` and associate @@ -252,6 +250,17 @@ mod tests { Ok(()) } + #[test] + fn pack_end_must_follow_the_last_entry() -> gix_error::TestResult { + let mut tree = super::Tree::with_capacity(1, None)?; + tree.add_root(12, ())?; + let err = tree + .set_pack_entries_end_and_resolve_ref_offsets(12) + .expect_err("an entry at or past the pack end is corrupt input"); + assert!(err.is_corrupted(), "invalid end offsets must return a corruption error"); + Ok(()) + } + #[test] fn allocation_failure_is_reported() { let err = super::Tree::<()>::with_capacity(usize::MAX, None) @@ -327,6 +336,34 @@ mod tests { Ok(()) } + #[test] + fn duplicate_and_overlapping_offsets_are_rejected() -> gix_testtools::Result { + let first = pack::data::header::SIZE as pack::data::Offset; + let pack_file = gix_testtools::tempfile::NamedTempFile::new()?; + let mut data = pack::data::header::encode(pack::data::Version::V2, 2).to_vec(); + // A two-byte header lets the second indexed offset overlap its predecessor. + pack::data::entry::Header::Blob.write_to(16, &mut data)?; + std::fs::write(pack_file.path(), data)?; + for second in [first, first + 1] { + let err = crate::cache::delta::Tree::from_offsets_in_pack( + pack_file.path(), + [first, second].into_iter(), + &|offset| *offset, + &|_| None, + &mut gix_features::progress::Discard, + &AtomicBool::new(false), + gix_hash::Kind::Sha1, + ) + .err() + .expect("index offsets cannot duplicate or overlap pack entry headers"); + assert!( + err.is_corrupted(), + "invalid index offsets must return a corruption error" + ); + } + Ok(()) + } + fn tree(index_path: &str, pack_path: &str) -> gix_testtools::Result { let idx = pack::index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; crate::cache::delta::Tree::from_offsets_in_pack( From 5b6522a8c8e5cc1cfe692ba65e87b8a6fad392ce Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:24:53 +0200 Subject: [PATCH 17/28] fix(gix-pack)!: validate multi-pack-index references on access <!-- Byron --> Looked at everything in detail and added error handling in callbacks that didn't have them. Reduced the scope of this commit to not overlap with `odb-parallelism` branch which makes many changes additionally. Many of the newly added tests are very specific about error handling, one might call them overkill, but I left them in for good measure. Further, errors.rs in `gix-odb` I just skimmed the test titles off as everything else would be too time consuming - better have them than not have them I thought, particularly to support `odb-paralellism`. <!-- agent --> A report describes object lookup panics caused by unchecked pack IDs and large-offset ordinals in otherwise structurally valid MIDX files. Validate these references when reading an object entry, before indexing pack arrays or dereferencing the optional LOFF (Large Offsets) table. Bound LOFF ordinals by the chunk itself, not by the remaining file. Keep MIDX loading limited to structural validation: an eager scan of every offset entry adds linear startup work and faults in memory-mapped pages even when a command needs only a handful of objects. Git likewise checks these references on access. Make `pack_id_and_pack_offset_at_index()` return `gix_error::Result` and `iter()` yield fallible entries. Propagate corruption errors through object and header lookup, delta-base resolution, integrity verification, and CLI entry listing. ID-only iteration remains available without valid offset references. Preserve literal high-bit 32-bit offsets when LOFF is absent. Return `Result<Option<_>>` from `Find::location_by_oid()` and `Find::pack_offsets_and_oid()`: absence and lookup failure are different outcomes, and callers must be able to distinguish them. Propagate errors through forwarding implementations, object counting, pack-entry iterator construction, and thin-pack base lookup rather than treating corruption as a missing object or a reason to recompress. Report a pack that becomes unavailable during construction as an error instead of panicking. Retain failed index loads separately from missing files so repeated lookups cannot silently turn corruption into absence. Load pending indices before retrying failures, retry all failed slots, and reconcile disk state when refreshing. This keeps independent valid packs usable and permits recovery after repair or removal without endless refresh loops on persistent failures. Regressions cover boundary and extreme invalid references, lazy loading, valid LOFF ordinals and literal offsets, integrity verification, corruption propagation through object lookup and pack generation, and repeated, shared, concurrent, repaired, and removed index-load failures. Git reference: `d38352cd43ab9745686d697872408bc3249a153f`, `midx.c::midx_for_pack()` and `nth_midxed_offset()`. BREAKING CHANGE: Multi-pack-index offset access and entry iteration now return errors which callers must handle. `Find::location_by_oid()` and `Find::pack_offsets_and_oid()` return `Result<Option<_>>`. `output::entry::iter_from_counts()` returns a fallible construction result, and `output::Entry::from_pack_entry()` requires a base-lookup callback returning `Result<Option<ObjectId>>`. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gitoxide-core/src/pack/create.rs | 2 +- gitoxide-core/src/pack/multi_index.rs | 1 + gix-odb/src/cache.rs | 7 +- gix-odb/src/store_impls/dynamic/find.rs | 115 +-- gix-odb/src/store_impls/dynamic/handle.rs | 71 +- gix-odb/src/store_impls/dynamic/header.rs | 51 +- gix-odb/src/store_impls/dynamic/iter.rs | 31 +- gix-odb/src/store_impls/dynamic/load_index.rs | 14 +- gix-odb/src/store_impls/dynamic/types.rs | 3 +- gix-odb/src/store_impls/loose/find.rs | 33 +- gix-odb/tests/odb/error.rs | 730 +++++++++++++++++- gix-odb/tests/odb/store/dynamic.rs | 6 +- gix-odb/tests/odb/store/loose.rs | 71 +- gix-pack/benches/pack_create.rs | 21 +- gix-pack/fuzz/fuzz_targets/data_file.rs | 2 +- .../fuzz/fuzz_targets/multi_index_file.rs | 4 +- gix-pack/src/bundle/find.rs | 13 +- gix-pack/src/bundle/write/mod.rs | 5 +- gix-pack/src/cache/delta/from_offsets.rs | 19 +- gix-pack/src/cache/delta/traverse/mod.rs | 4 + gix-pack/src/cache/delta/tree.rs | 162 +++- gix-pack/src/cache/mod.rs | 2 +- gix-pack/src/data/file/decode/entry.rs | 11 +- gix-pack/src/data/file/decode/header.rs | 7 +- .../data/input/lookup_ref_delta_objects.rs | 17 +- gix-pack/src/data/output/count/objects/mod.rs | 12 +- .../src/data/output/entry/iter_from_counts.rs | 47 +- gix-pack/src/data/output/entry/mod.rs | 37 +- gix-pack/src/find_traits.rs | 28 +- gix-pack/src/index/traverse/mod.rs | 10 +- gix-pack/src/index/traverse/with_index.rs | 2 +- gix-pack/src/multi_index/access.rs | 32 +- gix-pack/src/multi_index/init.rs | 4 +- gix-pack/src/multi_index/mod.rs | 2 +- gix-pack/src/multi_index/verify.rs | 6 +- gix-pack/src/testing.rs | 8 +- gix-pack/tests/pack/data/file.rs | 11 +- gix-pack/tests/pack/data/fuzzed.rs | 20 +- .../pack/data/output/count_and_entries.rs | 77 +- gix-pack/tests/pack/malformed.rs | 342 +++++++- gix-pack/tests/pack/multi_index/access.rs | 11 +- gix-pack/tests/pack/multi_index/fuzzed.rs | 164 +++- 42 files changed, 1858 insertions(+), 357 deletions(-) diff --git a/gitoxide-core/src/pack/create.rs b/gitoxide-core/src/pack/create.rs index c1d415c18bc..f904444a6b9 100644 --- a/gitoxide-core/src/pack/create.rs +++ b/gitoxide-core/src/pack/create.rs @@ -242,7 +242,7 @@ where version: Default::default(), compression: pack_compression, }, - )) + )?) }; let mut entries_progress = progress.add_child("consuming"); diff --git a/gitoxide-core/src/pack/multi_index.rs b/gitoxide-core/src/pack/multi_index.rs index 4177a7a2da0..ac0c05e22d8 100644 --- a/gitoxide-core/src/pack/multi_index.rs +++ b/gitoxide-core/src/pack/multi_index.rs @@ -89,6 +89,7 @@ pub fn entries(multi_index_path: PathBuf, format: OutputFormat, mut out: impl st } let file = gix::odb::pack::multi_index::File::at(multi_index_path, None)?; for entry in file.iter() { + let entry = entry?; writeln!(out, "{} {} {}", entry.oid, entry.pack_index, entry.pack_offset).or_error()?; } Ok(()) diff --git a/gix-odb/src/cache.rs b/gix-odb/src/cache.rs index fcd0638b5d4..d6efac44a99 100644 --- a/gix-odb/src/cache.rs +++ b/gix-odb/src/cache.rs @@ -236,11 +236,14 @@ mod impls { Ok(possibly_obj) } - fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<gix_pack::data::entry::Location> { + fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Result<Option<gix_pack::data::entry::Location>> { self.inner.location_by_oid(id, buf) } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(u64, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid( + &self, + pack_id: u32, + ) -> Result<Option<Vec<(gix_pack::data::Offset, gix_hash::ObjectId)>>> { self.inner.pack_offsets_and_oid(pack_id) } diff --git a/gix-odb/src/store_impls/dynamic/find.rs b/gix-odb/src/store_impls/dynamic/find.rs index 7aa79c28046..c432d7ba027 100644 --- a/gix-odb/src/store_impls/dynamic/find.rs +++ b/gix-odb/src/store_impls/dynamic/find.rs @@ -1,7 +1,8 @@ use gix_error::Result; use std::ops::Deref; -use gix_error::{Message, OptionExt, ResultExt, bail, not_found}; +use gix_error::{Message, OptionExt, ResultExt, bail, message, not_found}; + use gix_pack::cache::DecodeEntry; use crate::store::{handle, load_index}; @@ -82,7 +83,7 @@ where object_index: handle::IndexForObjectInPack { pack_id, pack_offset }, index_file, pack: possibly_pack, - }) = index.lookup(id) + }) = index.lookup(id)? { let pack = match possibly_pack { Some(pack) => pack, @@ -116,13 +117,17 @@ where buffer, inflate, &|id, _out| { - let pack_offset = index_file.pack_offset_by_id(id)?; - pack.entry(pack_offset) - .ok() - .map(gix_pack::data::decode::entry::ResolvedBase::InPack) + index_file + .pack_offset_by_id(id)? + .map(|pack_offset| { + pack.entry(pack_offset) + .map(gix_pack::data::decode::entry::ResolvedBase::InPack) + }) + .transpose() }, pack_cache, ); + let res = match res { Ok(r) => Ok(( gix_object::Data { @@ -183,12 +188,12 @@ where }, index_file, pack: possibly_pack, - } = match snapshot.indices[idx].lookup(id) { + } = match snapshot.indices[idx].lookup(id)? { Some(res) => res, None => { let mut out = None; for index in &mut snapshot.indices { - out = index.lookup(id); + out = index.lookup(id)?; if out.is_some() { break; } @@ -208,24 +213,19 @@ where entry, buffer, inflate, - &|id, out| { - index_file - .pack_offset_by_id(id) - .and_then(|pack_offset| { - pack.entry(pack_offset) - .ok() - .map(gix_pack::data::decode::entry::ResolvedBase::InPack) - }) - .or_else(|| { - (id == base_id).then(|| { - out.resize(buf.len(), 0); - out.copy_from_slice(buf.as_slice()); - gix_pack::data::decode::entry::ResolvedBase::OutOfPack { - kind: obj_kind, - end: out.len(), - } - }) - }) + &|id, out| match index_file.pack_offset_by_id(id)? { + Some(pack_offset) => pack + .entry(pack_offset) + .map(gix_pack::data::decode::entry::ResolvedBase::InPack) + .map(Some), + None => Ok((id == base_id).then(|| { + out.resize(buf.len(), 0); + out.copy_from_slice(buf.as_slice()); + gix_pack::data::decode::entry::ResolvedBase::OutOfPack { + kind: obj_kind, + end: out.len(), + } + })), }, pack_cache, ) @@ -321,7 +321,11 @@ where self.try_find_cached_inner(id, buffer, &mut inflate, pack_cache, &mut snapshot, None) } - fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec<u8>) -> Option<gix_pack::data::entry::Location> { + fn location_by_oid( + &self, + id: &gix_hash::oid, + buf: &mut Vec<u8>, + ) -> Result<Option<gix_pack::data::entry::Location>> { assert!( matches!(self.token.as_ref(), Some(handle::Mode::KeepDeletedPacksAvailable)), "BUG: handle must be configured to `prevent_pack_unload()` before using this method" @@ -342,18 +346,18 @@ where object_index: handle::IndexForObjectInPack { pack_id, pack_offset }, index_file: _, pack: possibly_pack, - }) = index.lookup(id) + }) = index.lookup(id)? { let pack = match possibly_pack { Some(pack) => pack, - None => match self.store.load_pack(pack_id, marker).ok()? { + None => match self.store.load_pack(pack_id, marker).or_error()? { Some(pack) => { *possibly_pack = Some(pack); possibly_pack.as_deref().expect("just put it in") } None => { // The pack wasn't available anymore so we are supposed to try another round with a fresh index - match self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()? { + match self.store.load_one_index(self.index_ctx(snapshot.marker))? { Some(new_snapshot) => { *snapshot = new_snapshot; self.clear_cache(); @@ -363,48 +367,56 @@ where // nothing new in the index, kind of unexpected to not have a pack but to also // to have no new index yet. We set the new index before removing any slots, so // this should be observable. - return None; + return Ok(None); } } } }, }; - let entry = pack.entry(pack_offset).ok()?; + let entry = pack.entry(pack_offset)?; // This allocation is driven by on-disk pack metadata, so keep it aligned with // `gix_pack::data::File::with_alloc_limit_bytes()`. - let size: usize = entry.decompressed_size.try_into().ok()?; - if pack.alloc_limit_bytes.is_some_and(|limit| size > limit) { - return None; + let size = entry.decompressed_size; + if pack.alloc_limit_bytes.is_some_and(|limit| size > limit as u64) { + bail!("Cannot store pack entry in memory: allocation limit exceeded".allocation_limit()); } - buf.resize(size, 0); + let size_usize = usize::try_from(size).or_raise(|| { + message!("Cannot store pack entry of {size} bytes in memory: the entry size cannot be represented in memory") + .allocation_failure() + })?; + buf.clear(); + buf.try_reserve(size_usize).or_raise(|| { + message!("Cannot store pack entry of {size} bytes in memory").allocation_failure() + })?; + buf.resize(size_usize, 0); assert_eq!(pack.id, pack_id.to_intrinsic_pack_id(), "both ids must always match"); - let res = pack - .decompress_entry(&entry, &mut inflate, buf) - .ok() - .map(|entry_size_past_header| gix_pack::data::entry::Location { - pack_id: pack.id, - pack_offset, - entry_size: entry.header_size() + entry_size_past_header, - }); + let entry_size_past_header = pack.decompress_entry(&entry, &mut inflate, buf)?; + let location = gix_pack::data::entry::Location { + pack_id: pack.id, + pack_offset, + entry_size: entry.header_size() + entry_size_past_header, + }; if idx != 0 { snapshot.indices.swap(0, idx); } - return res; + return Ok(Some(location)); } } } { - let new_snapshot = self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()??; + let Some(new_snapshot) = self.store.load_one_index(self.index_ctx(snapshot.marker))? else { + return Ok(None); + }; *snapshot = new_snapshot; self.clear_cache(); } } } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(u64, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(gix_pack::data::Offset, gix_hash::ObjectId)>>> { assert!( matches!(self.token.as_ref(), Some(handle::Mode::KeepDeletedPacksAvailable)), "BUG: handle must be configured to `prevent_pack_unload()` before using this method" @@ -415,13 +427,18 @@ where { for index in &snapshot.indices { if let Some(iter) = index.iter(pack_id) { - return Some(iter.map(|e| (e.pack_offset, e.oid)).collect()); + return iter + .map(|e| e.map(|e| (e.pack_offset, e.oid))) + .collect::<Result<Vec<_>>>() + .map(Some); } } } { - let new_snapshot = self.store.load_one_index(self.index_ctx(snapshot.marker)).ok()??; + let Some(new_snapshot) = self.store.load_one_index(self.index_ctx(snapshot.marker))? else { + return Ok(None); + }; drop(snapshot); *self.snapshot.borrow_mut() = new_snapshot; } diff --git a/gix-odb/src/store_impls/dynamic/handle.rs b/gix-odb/src/store_impls/dynamic/handle.rs index 01dd216f0fb..a7b66aa05f0 100644 --- a/gix-odb/src/store_impls/dynamic/handle.rs +++ b/gix-odb/src/store_impls/dynamic/handle.rs @@ -5,6 +5,7 @@ use std::{ sync::{Arc, atomic::Ordering}, }; +use gix_error::Result; use gix_features::threading::OwnShared; use gix_hash::oid; @@ -33,19 +34,22 @@ pub(crate) enum IntraPackLookup<'a> { } impl IntraPackLookup<'_> { - pub(crate) fn pack_offset_by_id(&self, id: &oid) -> Option<gix_pack::data::Offset> { - match self { + pub(crate) fn pack_offset_by_id(&self, id: &oid) -> Result<Option<gix_pack::data::Offset>> { + Ok(match self { IntraPackLookup::Single(index) => index .lookup(id) .map(|entry_index| index.pack_offset_at_index(entry_index)), IntraPackLookup::Multi { index, required_pack_index, - } => index.lookup(id).and_then(|entry_index| { - let (pack_index, pack_offset) = index.pack_id_and_pack_offset_at_index(entry_index); - (pack_index == *required_pack_index).then_some(pack_offset) - }), - } + } => match index.lookup(id) { + Some(entry_index) => { + let (pack_index, pack_offset) = index.pack_id_and_pack_offset_at_index(entry_index)?; + (pack_index == *required_pack_index).then_some(pack_offset) + } + None => None, + }, + }) } } @@ -65,6 +69,7 @@ pub struct IndexForObjectInPack { pub(crate) mod index_lookup { use std::{collections::HashSet, sync::Arc}; + use gix_error::Result; use gix_hash::oid; use crate::store::{handle, handle::IntraPackLookup, types}; @@ -81,20 +86,23 @@ pub(crate) mod index_lookup { pub(crate) fn iter( &self, pack_id: types::PackId, - ) -> Option<Box<dyn Iterator<Item = gix_pack::index::Entry> + '_>> { - (self.id == pack_id.index).then(|| match &self.file { - handle::SingleOrMultiIndex::Single { index, .. } => index.iter(), - handle::SingleOrMultiIndex::Multi { index, .. } => { - let pack_index = pack_id.multipack_index.expect( - "BUG: multi-pack index must be set if this is a multi-pack, pack-indices seem unstable", - ); - Box::new(index.iter().filter_map(move |e| { - (e.pack_index == pack_index).then_some(gix_pack::index::Entry { - oid: e.oid, - pack_offset: e.pack_offset, - crc32: None, - }) - })) + ) -> Option<Box<dyn Iterator<Item = Result<gix_pack::index::Entry>> + '_>> { + (self.id == pack_id.index).then(|| -> Box<dyn Iterator<Item = Result<gix_pack::index::Entry>> + '_> { + match &self.file { + handle::SingleOrMultiIndex::Single { index, .. } => Box::new(index.iter().map(Ok)), + handle::SingleOrMultiIndex::Multi { index, .. } => { + let pack_index = pack_id.multipack_index.expect( + "BUG: multi-pack index must be set if this is a multi-pack, pack-indices seem unstable", + ); + Box::new(index.iter().filter_map(move |entry| match entry { + Ok(e) => (e.pack_index == pack_index).then_some(Ok(gix_pack::index::Entry { + oid: e.oid, + pack_offset: e.pack_offset, + crc32: None, + })), + Err(err) => Some(Err(err)), + })) + } } }) } @@ -162,9 +170,9 @@ pub(crate) mod index_lookup { /// Also return the index itself as it's needed to resolve intra-pack ref-delta objects. They are a possibility even though /// they won't be used in practice as it's more efficient to store their offsets. /// If it is not loaded, ask it to be loaded and put it into the returned mutable option for safe-keeping. - pub(crate) fn lookup(&mut self, object_id: &oid) -> Option<Outcome<'_>> { + pub(crate) fn lookup(&mut self, object_id: &oid) -> Result<Option<Outcome<'_>>> { let id = self.id; - match &mut self.file { + Ok(match &mut self.file { handle::SingleOrMultiIndex::Single { index, data } => index.lookup(object_id).map(move |idx| Outcome { object_index: handle::IndexForObjectInPack { pack_id: types::PackId { @@ -176,9 +184,12 @@ pub(crate) mod index_lookup { index_file: IntraPackLookup::Single(index), pack: data, }), - handle::SingleOrMultiIndex::Multi { index, data } => index.lookup(object_id).map(move |idx| { - let (pack_index, pack_offset) = index.pack_id_and_pack_offset_at_index(idx); - Outcome { + handle::SingleOrMultiIndex::Multi { index, data } => { + let Some(idx) = index.lookup(object_id) else { + return Ok(None); + }; + let (pack_index, pack_offset) = index.pack_id_and_pack_offset_at_index(idx)?; + Some(Outcome { object_index: handle::IndexForObjectInPack { pack_id: types::PackId { index: id, @@ -191,9 +202,9 @@ pub(crate) mod index_lookup { required_pack_index: pack_index, }, pack: &mut data[pack_index as usize], - } - }), - } + }) + } + }) } } } @@ -353,7 +364,7 @@ where impl TryFrom<&super::Store> for super::Store { type Error = std::io::Error; - fn try_from(s: &super::Store) -> Result<Self, Self::Error> { + fn try_from(s: &super::Store) -> std::io::Result<Self> { super::Store::at_opts( s.path().into(), s.object_hash, diff --git a/gix-odb/src/store_impls/dynamic/header.rs b/gix-odb/src/store_impls/dynamic/header.rs index 99e9cd87442..bfb161d7e8a 100644 --- a/gix-odb/src/store_impls/dynamic/header.rs +++ b/gix-odb/src/store_impls/dynamic/header.rs @@ -2,6 +2,7 @@ use gix_error::Result; use std::ops::Deref; use gix_error::{OptionExt, ResultExt, bail, not_found}; + use gix_hash::oid; use crate::{ @@ -49,7 +50,7 @@ where object_index: handle::IndexForObjectInPack { pack_id, pack_offset }, index_file, pack: possibly_pack, - }) = index.lookup(id) + }) = index.lookup(id)? { let pack = match possibly_pack { Some(pack) => pack, @@ -77,13 +78,16 @@ where }, }; let entry = pack.entry(pack_offset)?; - let res = match pack.decode_header(entry, inflate, &|id| { - index_file.pack_offset_by_id(id).and_then(|pack_offset| { - pack.entry(pack_offset) - .ok() - .map(gix_pack::data::decode::header::ResolvedBase::InPack) - }) - }) { + let res = pack.decode_header(entry, inflate, &|id| { + index_file + .pack_offset_by_id(id)? + .map(|pack_offset| { + pack.entry(pack_offset) + .map(gix_pack::data::decode::header::ResolvedBase::InPack) + }) + .transpose() + }); + let res = match res { Ok(header) => Ok(header.into()), Err(err) => { let Some(base_id) = err @@ -120,12 +124,12 @@ where }, index_file, pack: possibly_pack, - } = match snapshot.indices[idx].lookup(id) { + } = match snapshot.indices[idx].lookup(id)? { Some(res) => res, None => { let mut out = None; for index in &mut snapshot.indices { - out = index.lookup(id); + out = index.lookup(id)?; if out.is_some() { break; } @@ -140,22 +144,17 @@ where .as_ref() .expect("pack to still be available like just now"); let entry = pack.entry(pack_offset)?; - pack.decode_header(entry, inflate, &|id| { - index_file - .pack_offset_by_id(id) - .and_then(|pack_offset| { - pack.entry(pack_offset) - .ok() - .map(gix_pack::data::decode::header::ResolvedBase::InPack) - }) - .or_else(|| { - (id == base_id).then(|| { - gix_pack::data::decode::header::ResolvedBase::OutOfPack { - kind: hdr.kind(), - num_deltas: hdr.num_deltas(), - } - }) - }) + pack.decode_header(entry, inflate, &|id| match index_file.pack_offset_by_id(id)? { + Some(pack_offset) => pack + .entry(pack_offset) + .map(gix_pack::data::decode::header::ResolvedBase::InPack) + .map(Some), + None => Ok((id == base_id).then(|| { + gix_pack::data::decode::header::ResolvedBase::OutOfPack { + kind: hdr.kind(), + num_deltas: hdr.num_deltas(), + } + })), }) .map(Into::into) } diff --git a/gix-odb/src/store_impls/dynamic/iter.rs b/gix-odb/src/store_impls/dynamic/iter.rs index a3524bd31fe..1be0dd3e8d9 100644 --- a/gix-odb/src/store_impls/dynamic/iter.rs +++ b/gix-odb/src/store_impls/dynamic/iter.rs @@ -46,7 +46,8 @@ pub enum Ordering { /// /// This mode allocates and as to pre-sort objects by their offsets, delaying the start of the iteration once per pack while keeping /// memory allocated once per pack. This price is usually worth paying once querying object information is planned as pack caches - /// are more efficiently used that way. + /// are more efficiently used that way. If a multi-pack index contains corrupt pack references, its objects + /// fall back to lexicographical ordering, which does not require valid offsets. PackAscendingOffsetThenLooseLexicographical, } @@ -131,18 +132,24 @@ fn maybe_sort_entries(index: &handle::IndexLookup, order: Ordering) -> Option<Ve SingleOrMultiIndex::Multi { index, .. } => index .iter() .enumerate() - .map(|(idx, e)| EntryForOrdering { - pack_offset: e.pack_offset, - entry_index: idx as u32, - pack_index: { - debug_assert!( - e.pack_index < PackId::max_packs_in_multi_index(), - "this shows the relation between u16 and pack_index (u32) and why this is OK" - ); - e.pack_index as u16 - }, + .map(|(idx, e)| { + let e = e?; + Ok(EntryForOrdering { + pack_offset: e.pack_offset, + entry_index: idx as u32, + pack_index: { + debug_assert!( + e.pack_index < PackId::max_packs_in_multi_index(), + "this shows the relation between u16 and pack_index (u32) and why this is OK" + ); + e.pack_index as u16 + }, + }) }) - .collect(), + // Object IDs remain iterable even if their pack references are corrupt. Keep the + // public iterator's filesystem-only error type and fall back to lexical ordering. + .collect::<Result<Vec<_>>>() + .ok()?, }, }; order.sort_by(|a, b| { diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index 0ff36612b7e..036b9d264ca 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -88,6 +88,9 @@ impl super::Store { 'retry_with_changed_index: loop { let previous_state_id = index.state_id(); 'retry_with_next_slot_index: loop { + // Register before claiming a slot: otherwise another caller could see an + // exhausted cursor before the claimed load becomes observable. + let ongoing_operation = IncOnNewAndDecOnDrop::new(&index.num_indices_currently_being_loaded); match index .next_index_to_load .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| { @@ -95,7 +98,6 @@ impl super::Store { }) { Ok(slot_map_index) => { // This slot-map index is in bounds and was only given to us. - let _ongoing_operation = IncOnNewAndDecOnDrop::new(&index.num_indices_currently_being_loaded); let slot = &self.files[index.slot_indices[slot_map_index]]; let _lock = slot.write.lock(); if slot.generation.load(Ordering::SeqCst) > index.generation { @@ -119,6 +121,9 @@ impl super::Store { break 'retry_with_next_slot_index; } Err(_err) => { + // TODO(odb-parallelism): consider to search fresh indices, + // then retry all changed failed slots + // before returning the first error. Persistent failures must not advance state_id. gix_features::trace::error!(err=?_err, "Failed to load index file - some objects may seem to not exist"); continue 'retry_with_next_slot_index; } @@ -126,16 +131,11 @@ impl super::Store { } } Err(_nothing_more_to_load) => { + drop(ongoing_operation); // There can be contention as many threads start working at the same time and take all the // slots to load indices for. Some threads might just be left-over and have to wait for something // to change. // TODO: potentially hot loop - could this be a condition variable? - // This is a timing-based fix for the case that the `num_indices_being_loaded` isn't yet incremented, - // and we might break out here without actually waiting for the loading operation. Then we'd fail to - // observe a change and the underlying handler would not have all the indices it needs at its disposal. - // Yielding means we will definitely loose enough time to observe the ongoing operation, - // or its effects. - std::thread::yield_now(); while index.num_indices_currently_being_loaded.load(Ordering::SeqCst) != 0 { std::thread::yield_now(); } diff --git a/gix-odb/src/store_impls/dynamic/types.rs b/gix-odb/src/store_impls/dynamic/types.rs index 9f0feec528e..4aa804171cb 100644 --- a/gix-odb/src/store_impls/dynamic/types.rs +++ b/gix-odb/src/store_impls/dynamic/types.rs @@ -174,7 +174,8 @@ impl<T: Clone> OnDiskFile<T> { Ok(()) } Err(err) => { - // TODO: Should be provide more information? We don't even know what exactly failed right now, degenerating information. + // TODO(odb-parallelism): only NotFound should become Missing; retain other failures + // across shared handles and retry after file changes, using the branch's failure cache. self.state = Missing; Err(err) } diff --git a/gix-odb/src/store_impls/loose/find.rs b/gix-odb/src/store_impls/loose/find.rs index e9e049855a4..c9469e16aff 100644 --- a/gix-odb/src/store_impls/loose/find.rs +++ b/gix-odb/src/store_impls/loose/find.rs @@ -1,7 +1,7 @@ use gix_error::Result; use std::{cmp::Ordering, collections::HashSet}; -use gix_error::{Message, ResultExt, allocation_failure, allocation_limit, bail, corruption}; +use gix_error::{Message, ResultExt, bail, corruption, message}; use crate::store_impls::loose::{HEADER_MAX_SIZE, Store, hash_path}; @@ -110,9 +110,8 @@ impl Store { Ok(Some((size, kind))) } - /// Decode and allocation failures retain [metadata](gix_error::Error::metadata()) `size` (requested bytes) or - /// `actual` and `expected` - /// (inflated bytes); allocation limits also report `limit`. All counts are unsigned. + /// Decode failures retain [metadata](gix_error::Error::metadata()) `actual` and `expected` (unsigned inflated byte counts). + /// Allocation diagnostics include requested sizes and configured limits in their messages, not metadata. fn find_inner<'a>(&self, id: &gix_hash::oid, out: &'a mut Vec<u8>) -> Result<Option<gix_object::Data<'a>>> { let path = hash_path(id, self.path.clone()); let map = match self.map_loose_object(&path)? { @@ -129,10 +128,11 @@ impl Store { let (kind, size, header_size) = gix_object::decode::loose_header(&header[..consumed_out])?; self.ensure_in_alloc_limit(size)?; - let allocation = || allocation_error(size); let size_usize = usize::try_from(size).or_raise(|| { - allocation_failure("Cannot store loose object in memory: the object size cannot be represented in memory") - .with("size", size) + message!( + "Cannot store loose object of {size} bytes in memory: the object size cannot be represented in memory" + ) + .allocation_failure() })?; let decompressed_body_prefix_len = consumed_out .checked_sub(header_size) @@ -147,7 +147,8 @@ impl Store { // If the first inflate already reached the end of the stream, the fixed-size `header` buffer // contains the complete decompressed object, so we can skip a second streaming inflate pass. out.clear(); - out.try_reserve(size_usize).or_raise(allocation)?; + out.try_reserve(size_usize) + .or_raise(|| message!("Cannot store loose object of {size} bytes in memory"))?; if status == gix_zlib::Status::StreamEnd { out.extend_from_slice(&header[header_size..consumed_out]); } else { @@ -176,16 +177,12 @@ impl Store { })) } - /// Allocation-limit failures include [metadata](gix_error::Error::metadata()) `size` and `limit` (unsigned byte - /// counts). + /// Allocation-limit diagnostics include the requested size and configured limit in their messages. fn ensure_in_alloc_limit(&self, size: u64) -> Result { if let Some(limit) = self.alloc_limit_bytes.filter(|limit| size > *limit as u64) { bail!( - allocation_limit( - "Cannot store loose object in memory: the object exceeds the configured allocation limit" - ) - .with("size", size) - .with("limit", limit) + "Cannot store loose object of {size} bytes in memory: the object exceeds the configured allocation limit of {limit} bytes" + .allocation_limit() ); } Ok(()) @@ -217,12 +214,6 @@ mod mmap { } } -/// The raised error's [metadata](gix_error::Error::metadata()) `size` (unsigned bytes) identifies the requested -/// loose-object allocation. -fn allocation_error(size: u64) -> Message { - Message::new("Cannot store loose object in memory").with("size", size) -} - /// Report invalid inflation sizes in [metadata](gix_error::Error::metadata()) `actual` and `expected` (unsigned byte /// counts). fn size_mismatch(actual: u64, expected: u64) -> gix_error::Error { diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index cffe2769792..6a245f1432f 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -76,7 +76,9 @@ fn write_failures_preserve_custom_sources_and_metadata() -> Result { let diagnostic = err.downcast_any_ref::<Message>().expect("write context"); assert_eq!( *context, - maplit::btreemap! { "path".into() => MetadataValue::from(dir.path()) }, + [("path".into(), MetadataValue::from(dir.path()))] + .into_iter() + .collect::<gix_error::Metadata>(), "the schema retains only the native object directory" ); assert_eq!( @@ -200,10 +202,12 @@ fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> Result { let diagnostic = err.downcast_any_ref::<Message>().expect("delta resolution diagnostic"); assert_eq!( *context, - maplit::btreemap! { - "base_id".into() => MetadataValue::from(base_id.to_string()), - "object_id".into() => MetadataValue::from(blob_id.to_string()), - }, + [ + ("base_id".into(), MetadataValue::from(base_id.to_string())), + ("object_id".into(), MetadataValue::from(blob_id.to_string())), + ] + .into_iter() + .collect::<gix_error::Metadata>(), "delta-resolution metadata retains both object IDs as hex text" ); if max_depth == 0 { @@ -220,10 +224,12 @@ fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> Result { .expect("the nested recursion failure retains its limit"); assert_eq!( limit.values, - maplit::btreemap! { - "max_depth".into() => MetadataValue::U64(0), - "object_id".into() => MetadataValue::from(blob_id.to_string()), - }, + [ + ("max_depth".into(), MetadataValue::U64(0)), + ("object_id".into(), MetadataValue::from(blob_id.to_string())), + ] + .into_iter() + .collect::<gix_error::Metadata>(), "recursion-limit metadata retains the original object and unsigned limit" ); } else { @@ -343,6 +349,631 @@ fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> Result { Ok(()) } +/// Corrupt one OOFF reference while keeping all chunk sizes and the checksum valid. +/// LOFF has one entry, so ordinal one is out of bounds rather than a truncated chunk. +fn corrupt_multi_index_reference( + path: &std::path::Path, + object_id: &gix_hash::oid, + large_offset: bool, +) -> gix_error::TestResult { + let original = std::fs::read(path)?; + let index = gix_pack::multi_index::File::from_data(original.clone(), path.to_owned(), None)?; + let entry_index = index.lookup(object_id).expect("the object is indexed") as usize; + let num_indices = index.num_indices(); + let object_hash = index.object_hash(); + drop(index); + + let mut chunks = Vec::new(); + for chunk_index in 0..usize::from(original[6]) { + let table_offset = 12 + chunk_index * 12; + let chunk_id: [u8; 4] = original[table_offset..table_offset + 4].try_into()?; + let start = usize::try_from(u64::from_be_bytes( + original[table_offset + 4..table_offset + 12].try_into()?, + ))?; + let end = usize::try_from(u64::from_be_bytes( + original[table_offset + 16..table_offset + 24].try_into()?, + ))?; + let mut data = original[start..end].to_vec(); + if chunk_id == *b"OOFF" { + let offset = entry_index * 8 + if large_offset { 4 } else { 0 }; + let invalid = if large_offset { (1 << 31) | 1 } else { num_indices }; + data[offset..offset + 4].copy_from_slice(&invalid.to_be_bytes()); + } + assert_ne!(chunk_id, *b"LOFF", "these small packs do not need large offsets"); + chunks.push((chunk_id, data)); + } + if large_offset { + chunks.push((*b"LOFF", 12u64.to_be_bytes().to_vec())); + } + + let mut corrupted = original[..12].to_vec(); + corrupted[6] = chunks.len().try_into()?; + let mut offset = 12 + (chunks.len() + 1) * 12; + for (chunk_id, data) in &chunks { + corrupted.extend_from_slice(chunk_id); + corrupted.extend_from_slice(&(offset as u64).to_be_bytes()); + offset += data.len(); + } + corrupted.extend_from_slice(&[0; 4]); + corrupted.extend_from_slice(&(offset as u64).to_be_bytes()); + for (_, data) in chunks { + corrupted.extend(data); + } + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&corrupted); + corrupted.extend_from_slice(hasher.try_finalize()?.as_slice()); + std::fs::write(path, corrupted)?; + Ok(()) +} + +fn multi_index_corrupt_reference_is_fallible(large_offset: bool) -> gix_error::TestResult { + use gix_pack::Find as _; + + let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; + let objects = dir.path().join(".git/objects"); + let path = objects.join("pack/multi-pack-index"); + let index = gix_pack::multi_index::File::at(&path, None)?; + let object_id = index.oid_at_index(0).to_owned(); + let (pack_index, _) = index.pack_id_and_pack_offset_at_index(0)?; + let valid_id = index + .iter() + .collect::<gix_error::Result<Vec<_>>>()? + .into_iter() + .find(|entry| entry.pack_index == pack_index && entry.oid != object_id) + .expect("the same pack has another object") + .oid; + drop(index); + corrupt_multi_index_reference(&path, &object_id, large_offset)?; + + // Loading must not reject the MIDX and silently fall back to the valid plain indices. + let index = gix_pack::multi_index::File::at(&path, None)?; + assert_eq!( + index.oid_at_index(0), + object_id, + "object IDs need no pack-reference validation" + ); + drop(index); + let mut handle = crate::odb_at(&objects)?.into_inner(); + handle.prevent_pack_unload(); + let cache = gix_odb::Cache::from(handle.clone()); + let location = handle + .location_by_oid(&valid_id, &mut Vec::new())? + .expect("an unrelated valid reference remains usable"); + for err in [ + handle + .location_by_oid(&object_id, &mut Vec::new()) + .expect_err("the invalid reference must fail location lookup"), + handle + .pack_offsets_and_oid(location.pack_id) + .expect_err("pack iteration must reject corrupt references instead of returning partial results"), + cache + .location_by_oid(&object_id, &mut Vec::new()) + .expect_err("the cache forwards location lookup errors"), + cache + .pack_offsets_and_oid(location.pack_id) + .expect_err("the cache forwards pack iteration errors"), + gix_object::Find::try_find(&handle, &object_id, &mut Vec::new()) + .expect_err("the invalid reference must fail object lookup"), + gix_odb::Header::try_header(&handle, &object_id).expect_err("the invalid reference must fail header lookup"), + gix_object::FindHeader::try_header(&handle, &object_id) + .expect_err("the object header trait also reports corruption"), + ] { + assert!( + err.is_corrupted(), + "MIDX reference errors retain their corruption classification: {err}" + ); + assert!(!err.is_not_found(), "corruption is not object absence"); + } + let missing_id = gix_object::compute_hash(gix_testtools::object_hash(), Kind::Blob, b"not in the MIDX fixture")?; + assert!( + !handle.contains(&missing_id), + "the missing object is absent from both packs and loose storage" + ); + assert!( + handle.location_by_oid(&missing_id, &mut Vec::new())?.is_none(), + "true object absence is Ok(None), not corruption" + ); + assert!( + cache.location_by_oid(&missing_id, &mut Vec::new())?.is_none(), + "the cache preserves Ok(None) for true object absence" + ); + assert_eq!( + cache.location_by_oid(&valid_id, &mut Vec::new())?, + Some(location), + "the cache forwards successful location lookups" + ); + assert!( + gix_object::Find::try_find(&handle, &valid_id, &mut Vec::new())?.is_some(), + "unrelated valid object lookup still works" + ); + let lexical = handle.iter()?.collect::<std::result::Result<Vec<_>, _>>()?; + let ordered = handle + .iter()? + .with_ordering(gix_odb::store::iter::Ordering::PackAscendingOffsetThenLooseLexicographical) + .collect::<std::result::Result<Vec<_>, _>>()?; + assert_eq!( + ordered, lexical, + "invalid offset metadata falls back to object-ID ordering without dropping objects" + ); + assert!( + lexical.contains(&object_id), + "ID-only iteration includes the corrupt reference" + ); + Ok(()) +} + +#[test] +fn multi_index_invalid_pack_reference() -> gix_error::TestResult { + multi_index_corrupt_reference_is_fallible(false) +} + +#[test] +fn multi_index_invalid_large_offset_reference() -> gix_error::TestResult { + multi_index_corrupt_reference_is_fallible(true) +} + +fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::TestResult { + use gix_pack::{Find as _, data::output}; + + let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; + let objects = dir.path().join(".git/objects"); + let path = objects.join("pack/multi-pack-index"); + let index = gix_pack::multi_index::File::at(&path, None)?; + let entries = index.iter().collect::<gix_error::Result<Vec<_>>>()?; + let (delta_id, base_id, unrelated_id) = 'packs: { + for (pack_index, name) in index.index_names().iter().enumerate() { + let pack = gix_pack::data::File::at( + objects.join("pack").join(name).with_extension("pack"), + index.object_hash(), + )?; + for entry in entries.iter().filter(|entry| entry.pack_index as usize == pack_index) { + if let gix_pack::data::entry::Header::OfsDelta { base_distance } = pack.entry(entry.pack_offset)?.header + { + let base_offset = + gix_pack::data::entry::Header::verified_base_pack_offset(entry.pack_offset, base_distance) + .expect("the fixture's OFS delta has a valid base offset"); + let Some(base) = entries + .iter() + .find(|base| base.pack_index == entry.pack_index && base.pack_offset == base_offset) + else { + continue; + }; + if let Some(unrelated) = entries.iter().find(|other| { + other.pack_index == entry.pack_index && other.oid != entry.oid && other.oid != base.oid + }) { + break 'packs Some((entry.oid, base.oid, unrelated.oid)); + } + } + } + } + None + } + .expect("the fixture contains an OFS delta, its indexed base, and an unrelated object in the same pack"); + drop(index); + + // Construct the shared store directly: converting an Rc-backed handle with + // into_arc() disables MIDX support, which would bypass the corrupt metadata. + let store = std::sync::Arc::new(gix_odb::Store::at_opts( + objects.clone(), + gix_testtools::object_hash(), + &mut std::iter::empty(), + Default::default(), + )?); + let mut db = store.to_cache_arc(); + db.prevent_pack_unload(); + let options = gix_pack::data::output::entry::iter_from_counts::Options { + allow_thin_pack: true, + thread_limit: Some(1), + ..Default::default() + }; + let count = output::Count { + id: delta_id, + entry_pack_location: output::count::PackLocation::NotLookedUp, + }; + let mut valid_entries = output::entry::iter_from_counts( + vec![count.clone()], + db, + Box::new(gix_features::progress::Discard), + options, + )?; + let (_, generated) = valid_entries.next().expect("one input object produces one chunk")?; + assert_eq!( + generated.len(), + 1, + "thin-pack generation emits only the requested delta" + ); + assert_eq!( + generated[0].kind, + output::entry::Kind::DeltaOid { id: base_id }, + "the omitted OFS base is looked up through pack_offsets_and_oid and becomes a thin-pack ref delta" + ); + assert!(valid_entries.next().is_none(), "there are no additional entry chunks"); + drop(valid_entries); + drop(store); + + corrupt_multi_index_reference(&path, &unrelated_id, large_offset)?; + let store = std::sync::Arc::new(gix_odb::Store::at_opts( + objects, + gix_testtools::object_hash(), + &mut std::iter::empty(), + Default::default(), + )?); + let mut db = store.to_cache_arc(); + db.prevent_pack_unload(); + assert!( + db.location_by_oid(&delta_id, &mut Vec::new())?.is_some(), + "unrelated MIDX corruption does not prevent locating the requested delta" + ); + assert!( + db.location_by_oid(&base_id, &mut Vec::new())?.is_some(), + "the delta base reference remains valid too" + ); + + let err = output::entry::iter_from_counts( + vec![output::Count { + id: unrelated_id, + entry_pack_location: output::count::PackLocation::NotLookedUp, + }], + db.clone(), + Box::new(gix_features::progress::Discard), + options, + ) + .err() + .expect("resolving a corrupt MIDX reference fails the iterator constructor without panicking"); + assert!(err.is_corrupted(), "constructor errors retain MIDX corruption: {err}"); + assert!(!err.is_not_found(), "constructor corruption is not object absence"); + + let mut entries = output::entry::iter_from_counts( + vec![count.clone()], + db.clone(), + Box::new(gix_features::progress::Discard), + options, + )?; + let err = entries + .next() + .expect("the requested delta produces an iteration result") + .expect_err("thin-pack base lookup rejects unrelated corrupt metadata without panicking"); + assert!(err.is_corrupted(), "iteration errors retain MIDX corruption: {err}"); + assert!(!err.is_not_found(), "iteration corruption is not a missing delta base"); + drop(entries); + + let entries = output::entry::iter_from_counts(vec![count], db, Box::new(gix_features::progress::Discard), options)?; + let mut writer = output::bytes::FromEntriesIter::new( + entries.map(|chunk| chunk.map(|(_, entries)| entries)), + Vec::new(), + 1, + options.version, + gix_testtools::object_hash(), + ); + let err = writer + .next() + .expect("pack writing consumes the requested delta") + .expect_err("pack writing propagates the thin-pack iterator error without panicking"); + assert!( + err.is_corrupted(), + "pack-writing context retains MIDX corruption: {err}" + ); + assert!(!err.is_not_found(), "pack-writing corruption is not object absence"); + assert!( + writer.digest().is_none(), + "a failed pack is never finalized with a checksum" + ); + assert!(writer.next().is_none(), "pack writing stops after the corrupted input"); + Ok(()) +} + +#[test] +fn multi_index_pack_generation_invalid_pack_reference() -> gix_error::TestResult { + multi_index_pack_generation_is_fallible(false) +} + +#[test] +fn multi_index_pack_generation_invalid_large_offset_reference() -> gix_error::TestResult { + multi_index_pack_generation_is_fallible(true) +} + +#[test] +fn pack_location_allocation_failures_are_resource_exhaustion() -> gix_error::TestResult { + use gix_error::ResourceExhaustionKind; + use gix_pack::{Find as _, data}; + use std::io::Write as _; + + for (size, limit, classification) in [ + (8192, Some(4096), ResourceExhaustionKind::AllocationLimit), + (u64::MAX, None, ResourceExhaustionKind::AllocationFailure), + ] { + let dir = gix_testtools::tempfile::tempdir()?; + let pack_dir = dir.path().join("pack"); + std::fs::create_dir(&pack_dir)?; + let object_hash = gix_testtools::object_hash(); + let blob_id = gix_object::compute_hash(object_hash, Kind::Blob, b"blob")?; + // The advertised size is intentionally larger than the body. Location lookup must + // reject it before allocating or decompressing, regardless of any existing capacity. + let mut pack = data::header::encode(data::Version::V2, 1).to_vec(); + let blob_offset = pack.len() as u32; + data::entry::Header::Blob.write_to(size, &mut pack)?; + let mut compressed = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + compressed.write_all(b"blob")?; + compressed.flush()?; + pack.extend(compressed.into_inner()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&pack); + let pack_id = hasher.try_finalize()?; + pack.extend_from_slice(pack_id.as_slice()); + std::fs::write(pack_dir.join(format!("pack-{pack_id}.pack")), pack)?; + + let mut index = Vec::new(); + for first_byte in 0..=255 { + index.extend_from_slice(&u32::from(blob_id.first_byte() <= first_byte).to_be_bytes()); + } + index.extend_from_slice(&blob_offset.to_be_bytes()); + index.extend_from_slice(blob_id.as_slice()); + index.extend_from_slice(pack_id.as_slice()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&index); + index.extend_from_slice(hasher.try_finalize()?.as_slice()); + std::fs::write(pack_dir.join(format!("pack-{pack_id}.idx")), index)?; + + let mut handle = gix_odb::at_opts( + dir.path(), + object_hash, + [], + gix_odb::store::init::Options { + alloc_limit_bytes: limit, + ..Default::default() + }, + )?; + handle.prevent_pack_unload(); + let err = handle + .location_by_oid(&blob_id, &mut vec![0; 16384]) + .expect_err("oversized on-disk metadata must fail location lookup without panicking"); + assert!( + err.is_resource_exhausted() + && err + .classify() + .all(|marker| marker.class() == Class::ResourceExhaustion(classification)), + "size conversion, reservation, and configured limits are classified only as resource exhaustion" + ); + assert!( + !err.is_corrupted(), + "allocation failures are not classified as corruption" + ); + assert!(!err.is_not_found(), "allocation failures are not object absence"); + assert!( + err.metadata().next().is_none(), + "allocation diagnostics do not imply a size or limit metadata schema" + ); + let diagnostic = err + .iter_errors() + .filter_map(|error| error.downcast_ref::<Message>()) + .find(|diagnostic| diagnostic.message.starts_with("Cannot store pack entry")) + .expect("the failed allocation has a diagnostic"); + if limit.is_some() { + assert_eq!( + err.to_string(), + "Cannot store pack entry in memory: allocation limit exceeded", + "allocation limits retain their diagnostic without metadata suffixes" + ); + } else if usize::try_from(size).is_err() { + assert_eq!( + diagnostic.message, + format!( + "Cannot store pack entry of {size} bytes in memory: the entry size cannot be represented in memory" + ), + "conversion diagnostics retain the requested entry size in prose" + ); + assert!( + err.probable_cause().is::<std::num::TryFromIntError>(), + "the original size conversion failure remains accessible" + ); + } else { + assert_eq!( + diagnostic.message, + format!("Cannot store pack entry of {size} bytes in memory"), + "reservation diagnostics retain the requested entry size in prose" + ); + assert!( + err.probable_cause().is::<std::collections::TryReserveError>(), + "the original buffer reservation failure remains accessible" + ); + } + } + Ok(()) +} + +#[test] +fn pack_lookup_unavailable_is_not_an_error() -> gix_error::TestResult { + let dir = gix_testtools::tempfile::tempdir()?; + let mut handle = crate::odb_at(dir.path())?.into_inner(); + handle.prevent_pack_unload(); + let cache = gix_odb::Cache::from(handle.clone()); + let missing_id = gix_testtools::object_hash().null(); + for db in [&handle as &dyn gix_pack::Find, &cache] { + assert!( + db.location_by_oid(&missing_id, &mut Vec::new())?.is_none(), + "an empty store has no packed object location" + ); + assert!( + db.pack_offsets_and_oid(0)?.is_none(), + "an unavailable pack has no offsets, rather than an error" + ); + } + Ok(()) +} + +#[test] +fn pack_location_preserves_native_load_errors() -> gix_error::TestResult { + use gix_pack::Find as _; + + let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; + let objects = dir.path().join(".git/objects"); + let index = gix_pack::multi_index::File::at(objects.join("pack/multi-pack-index"), None)?; + let object_id = index.oid_at_index(0).to_owned(); + let (pack_index, _) = index.pack_id_and_pack_offset_at_index(0)?; + let pack_path = objects + .join("pack") + .join(&index.index_names()[pack_index as usize]) + .with_extension("pack"); + drop(index); + // A directory is present instead of a pack file, so this is an I/O failure, + // not a disappearing pack that can legitimately return Ok(None). + std::fs::remove_file(&pack_path)?; + std::fs::create_dir(&pack_path)?; + let mut handle = crate::odb_at(&objects)?; + handle.prevent_pack_unload(); + let err = handle + .location_by_oid(&object_id, &mut Vec::new()) + .expect_err("a native pack-loading failure must not be suppressed"); + assert!( + err.downcast_any_ref::<io::Error>().is_some(), + "the original pack-loading I/O error remains accessible" + ); + assert!(!err.is_not_found(), "a pack-loading I/O failure is not object absence"); + Ok(()) +} + +// TODO(odb-parallelism): restore shared/concurrent malformed-index coverage for location, +// offsets, object/header lookup and enumeration, including refresh_never and later valid/repaired +// packs. Cover pack/alternate removal clearing failures without refresh loops. +#[test] +fn pack_lookup_preserves_index_load_errors() -> gix_error::TestResult { + use gix_pack::Find as _; + + for lookup_location in [false, true] { + let dir = gix_testtools::tempfile::tempdir()?; + let objects = dir.path(); + std::fs::create_dir(objects.join("info"))?; + // A self-referencing alternate fails disk-state initialization before any + // index is available. Both packing APIs must propagate load_one_index errors. + std::fs::write(objects.join("info/alternates"), b".\n")?; + let object_id = gix_testtools::object_hash().null(); + let mut handle = gix_odb::at_opts( + objects, + gix_testtools::object_hash(), + [], + gix_odb::store::init::Options { + slots: gix_odb::store::init::Slots::Given(32), + ..Default::default() + }, + )?; + handle.prevent_pack_unload(); + let err = if lookup_location { + handle + .location_by_oid(&object_id, &mut Vec::new()) + .expect_err("location lookup must propagate index loading failures") + } else { + handle + .pack_offsets_and_oid(0) + .expect_err("pack iteration must propagate index loading failures") + }; + assert!( + err.is_corrupted(), + "disk-state initialization corruption must remain classified: {err}" + ); + assert!(!err.is_not_found(), "index-loading corruption is not object absence"); + } + Ok(()) +} + +#[test] +fn multi_index_ref_delta_preserves_corrupt_base_lookup() -> gix_error::TestResult { + use gix_pack::data; + use std::io::Write; + + let dir = gix_testtools::tempfile::tempdir()?; + let pack_dir = dir.path().join("pack"); + std::fs::create_dir(&pack_dir)?; + let object_hash = gix_testtools::object_hash(); + let base_id = gix_object::compute_hash(object_hash, Kind::Blob, b"base")?; + let blob_id = gix_object::compute_hash(object_hash, Kind::Blob, b"based")?; + // The second blob copies the four-byte base and appends `d`. Ref-deltas force + // the decoder to look up the base's MIDX reference instead of using an offset. + let mut pack = data::header::encode(data::Version::V2, 2).to_vec(); + let base_offset = pack.len() as u32; + data::entry::Header::Blob.write_to(4, &mut pack)?; + let mut compressed = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + compressed.write_all(b"base")?; + compressed.flush()?; + pack.extend(compressed.into_inner()); + let blob_offset = pack.len() as u32; + let delta = [4, 5, 0x90, 4, 1, b'd']; + data::entry::Header::RefDelta { base_id }.write_to(delta.len() as u64, &mut pack)?; + let mut compressed = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + compressed.write_all(&delta)?; + compressed.flush()?; + pack.extend(compressed.into_inner()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&pack); + let pack_id = hasher.try_finalize()?; + pack.extend_from_slice(pack_id.as_slice()); + + let mut entries = [(base_id, base_offset), (blob_id, blob_offset)]; + entries.sort_by_key(|(object_id, _)| *object_id); + let mut index = Vec::new(); + for first_byte in 0..=255 { + let count = entries + .iter() + .filter(|(object_id, _)| object_id.first_byte() <= first_byte) + .count() as u32; + index.extend_from_slice(&count.to_be_bytes()); + } + for (object_id, offset) in entries { + index.extend_from_slice(&offset.to_be_bytes()); + index.extend_from_slice(object_id.as_slice()); + } + index.extend_from_slice(pack_id.as_slice()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&index); + index.extend_from_slice(hasher.try_finalize()?.as_slice()); + let index_path = pack_dir.join(format!("pack-{pack_id}.idx")); + std::fs::write(pack_dir.join(format!("pack-{pack_id}.pack")), pack)?; + std::fs::write(&index_path, index)?; + let path = pack_dir.join("multi-pack-index"); + let mut multi_index = std::fs::File::create(&path)?; + gix_pack::multi_index::write_from_index_paths( + vec![index_path], + &mut multi_index, + &mut gix_features::progress::Discard, + &std::sync::atomic::AtomicBool::default(), + gix_pack::multi_index::write::Options { object_hash }, + )?; + drop(multi_index); + let handle = crate::odb_at(dir.path())?; + let mut buffer = Vec::new(); + assert_eq!( + gix_object::Find::try_find(&handle, &blob_id, &mut buffer)? + .expect("the valid delta resolves") + .data, + b"based", + "the synthetic ref-delta is valid before corruption" + ); + drop(handle); + + for large_offset in [false, true] { + let original = std::fs::read(&path)?; + corrupt_multi_index_reference(&path, &base_id, large_offset)?; + let mut handle = crate::odb_at(dir.path())?; + // If callback errors are suppressed, recursion would fail with an unclassified + // depth-limit error instead of the original corruption diagnostic. + handle.max_recursion_depth = 0; + for err in [ + gix_object::Find::try_find(&handle, &blob_id, &mut Vec::new()) + .expect_err("the ref-delta base has an invalid MIDX reference"), + gix_odb::Header::try_header(&handle, &blob_id) + .expect_err("header decoding must preserve the base lookup error too"), + ] { + assert!( + err.is_corrupted(), + "decode callbacks must retain MIDX corruption: {err}" + ); + assert!(!err.is_not_found(), "an invalid base reference is not a missing base"); + } + drop(handle); + std::fs::write(&path, original)?; + } + Ok(()) +} + #[test] #[cfg(unix)] fn disappearing_loose_objects_keep_retryable_diagnostics() -> Result { @@ -443,3 +1074,84 @@ fn disappearing_loose_objects_keep_retryable_diagnostics() -> Result { "#); Ok(()) } + +#[test] +fn in_pack_ref_delta_preserves_malformed_base_entry() -> gix_error::TestResult { + use gix_pack::data; + use std::io::Write; + + let dir = gix_testtools::tempfile::tempdir()?; + let pack_dir = dir.path().join("pack"); + std::fs::create_dir(&pack_dir)?; + let object_hash = gix_testtools::object_hash(); + let base_id = gix_object::compute_hash(object_hash, Kind::Blob, b"base")?; + let blob_id = gix_object::compute_hash(object_hash, Kind::Blob, b"based")?; + let mut pack = data::header::encode(data::Version::V2, 2).to_vec(); + let base_offset = pack.len() as u32; + // Type zero is invalid. The index still locates this base within the same pack, + // so resolving the ref-delta must propagate pack.entry()'s error, not absence. + pack.push(0); + let blob_offset = pack.len() as u32; + let delta = [4, 5, 0x90, 4, 1, b'd']; + data::entry::Header::RefDelta { base_id }.write_to(delta.len() as u64, &mut pack)?; + let mut compressed = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT); + compressed.write_all(&delta)?; + compressed.flush()?; + pack.extend(compressed.into_inner()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&pack); + let pack_id = hasher.try_finalize()?; + pack.extend_from_slice(pack_id.as_slice()); + + // A v1 index can describe the malformed base without decoding pack entries. + let mut entries = [(base_id, base_offset), (blob_id, blob_offset)]; + entries.sort_by_key(|(object_id, _)| *object_id); + let mut index = Vec::new(); + for first_byte in 0..=255 { + let count = entries + .iter() + .filter(|(object_id, _)| object_id.first_byte() <= first_byte) + .count() as u32; + index.extend_from_slice(&count.to_be_bytes()); + } + for (object_id, offset) in entries { + index.extend_from_slice(&offset.to_be_bytes()); + index.extend_from_slice(object_id.as_slice()); + } + index.extend_from_slice(pack_id.as_slice()); + let mut hasher = gix_hash::hasher(object_hash); + hasher.update(&index); + index.extend_from_slice(hasher.try_finalize()?.as_slice()); + std::fs::write(pack_dir.join(format!("pack-{pack_id}.pack")), pack)?; + std::fs::write(pack_dir.join(format!("pack-{pack_id}.idx")), index)?; + + let mut handle = crate::odb_at(dir.path())?; + // Suppressing the callback error would enter recursive base lookup and hit this limit. + handle.max_recursion_depth = 0; + for err in [ + gix_object::Find::try_find(&handle, &blob_id, &mut Vec::new()) + .expect_err("the in-pack ref-delta base has a malformed entry header"), + gix_odb::Header::try_header(&handle, &blob_id) + .expect_err("header decoding must preserve the malformed base entry error too"), + ] { + assert!( + err.is_corrupted(), + "base entry corruption must survive the callback: {err}" + ); + assert!(!err.is_not_found(), "a malformed in-pack base is not absent"); + assert!( + err.downcast_any_ref::<data::decode::DeltaBaseUnresolved>().is_none(), + "a failed base entry lookup must not become an unresolved delta" + ); + assert!( + !err.metadata().any(|values| values.contains_key("max_depth")), + "base entry corruption must bypass recursive fallback" + ); + assert_eq!( + err.probable_cause().to_string(), + "Object type 0 is unsupported", + "both decoders must retain the original pack.entry() diagnostic" + ); + } + Ok(()) +} diff --git a/gix-odb/tests/odb/store/dynamic.rs b/gix-odb/tests/odb/store/dynamic.rs index 347908e1894..6e5d7d4a414 100644 --- a/gix-odb/tests/odb/store/dynamic.rs +++ b/gix-odb/tests/odb/store/dynamic.rs @@ -242,7 +242,7 @@ fn multi_index_keep_open() -> Result { let mut buf = Vec::new(); use gix_pack::Find; let location = stable_handle - .location_by_oid(&oid, &mut buf) + .location_by_oid(&oid, &mut buf)? .expect("oid exists and is packed"); let non_existing_to_trigger_refresh = missing_id(&handle); @@ -1030,7 +1030,7 @@ fn auto_refresh_with_and_without_id_stability() -> Result { let mut stable_handle = handle.clone(); stable_handle.prevent_pack_unload(); let location = stable_handle - .location_by_oid(&hex_to_id("501b297447a8255d3533c6858bb692575cdefaa0"), &mut buf) + .location_by_oid(&hex_to_id("501b297447a8255d3533c6858bb692575cdefaa0"), &mut buf)? .expect("object exists"); assert!( stable_handle.entry_by_location(&location).is_some(), @@ -1042,7 +1042,7 @@ fn auto_refresh_with_and_without_id_stability() -> Result { assert!( stable_handle - .location_by_oid(&hex_to_id("4dac9989f96bc5b5b1263b582c08f0c5f0b58542"), &mut buf) + .location_by_oid(&hex_to_id("4dac9989f96bc5b5b1263b582c08f0c5f0b58542"), &mut buf)? .is_some(), "it finds the object in the newly unhidden pack, which also triggers a refresh providing it with new indices" ); diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 1a41eb80af9..6b04132eb0d 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -595,22 +595,21 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB let err = db .try_find(&id, &mut buf) .expect_err("the object exceeds the configured allocation limit"); - let allocation = err - .metadata() - .find(|context| context.contains_key("size")) - .expect("the allocation limit retains its byte counts"); let diagnostic = err .iter_errors() .filter_map(|error| error.downcast_ref::<Message>()) - .find(|diagnostic| diagnostic.values.contains_key("size")) + .find(|diagnostic| { + diagnostic.class == Some(Class::ResourceExhaustion(ResourceExhaustionKind::AllocationLimit)) + }) .expect("the allocation limit has a diagnostic"); + assert!( + diagnostic.values.is_empty(), + "allocation-limit diagnostics do not add size or limit metadata" + ); assert_eq!( - *allocation, - maplit::btreemap! { - "limit".into() => MetadataValue::U64(1), - "size".into() => MetadataValue::U64(56915), - }, - "allocation limits add the unsigned byte limit to the requested size" + diagnostic.message, + "Cannot store loose object of 56915 bytes in memory: the object exceeds the configured allocation limit of 1 bytes", + "allocation limits retain the requested size and configured limit in prose" ); assert_eq!( diagnostic.class, @@ -626,17 +625,20 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB Could not read loose object, "path"="<object-path>" Caused by: - 0: Cannot store loose object in memory: the object exceeds the configured allocation limit, "limit"=1, "size"=56915 + 0: Cannot store loose object of 56915 bytes in memory: the object exceeds the configured allocation limit of 1 bytes "#); assert!( err.probable_cause().is::<Message>(), "no synthetic resource-exhaustion cause remains" ); assert_eq!( - err.metadata().next().expect("the lookup records its path")["path"], - MetadataValue::from(db.object_path(&id)), - "the native object path remains separate from allocation details" + *err.metadata().next().expect("the lookup records its path"), + [("path".into(), MetadataValue::from(db.object_path(&id)))] + .into_iter() + .collect::<gix_error::Metadata>(), + "the lookup retains only the native object path" ); + assert_eq!(err.metadata().count(), 1, "only the lookup path carries metadata"); assert_eq!( err.classify() .map(|classification| classification.class()) @@ -690,26 +692,28 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB [Class::ResourceExhaustion(ResourceExhaustionKind::AllocationFailure)], "both integer conversion and reservation failures retain their allocation-failure class" ); - let allocation = err - .metadata() - .find(|context| context.contains_key("size")) - .expect("the failed allocation records its requested size"); let diagnostic = err .iter_errors() .filter_map(|error| error.downcast_ref::<Message>()) - .find(|diagnostic| diagnostic.values.contains_key("size")) + .find(|diagnostic| diagnostic.message.starts_with("Cannot store loose object")) .expect("the failed allocation has a diagnostic"); - assert_eq!( - allocation["size"], - MetadataValue::U64(size), - "the full unrepresentable size is retained" + assert!( + diagnostic.values.is_empty(), + "allocation diagnostics do not add size or limit metadata" ); if usize::try_from(size).is_err() { + assert_eq!( + diagnostic.message, + format!( + "Cannot store loose object of {size} bytes in memory: the object size cannot be represented in memory" + ), + "conversion diagnostics retain the full requested object size in prose" + ); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&path.to_string_lossy(), "<object-path>")]), "unrepresentable sizes retain the failed integer conversion", @r#" Could not read loose object, "path"="<object-path>" Caused by: - 0: Cannot store loose object in memory: the object size cannot be represented in memory, "size"=18446744073709551615 + 0: Cannot store loose object of 18446744073709551615 bytes in memory: the object size cannot be represented in memory 1: out of range integral type conversion attempted "#); assert!( @@ -722,11 +726,16 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB "the generic context classifies the unrepresentable object size" ); } else { + assert_eq!( + diagnostic.message, + format!("Cannot store loose object of {size} bytes in memory"), + "reservation diagnostics retain the full requested object size in prose" + ); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&path.to_string_lossy(), "<object-path>")]), "impossible allocations retain the failed capacity reservation", @r#" Could not read loose object, "path"="<object-path>" Caused by: - 0: Cannot store loose object in memory, "size"=18446744073709551615 + 0: Cannot store loose object of 18446744073709551615 bytes in memory 1: memory allocation failed because the computed capacity exceeded the collection's maximum "#); assert!( @@ -739,11 +748,13 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB ); } assert_eq!( - err.metadata().next().expect("the lookup records its path")["path"], - MetadataValue::from(path.as_path()), - "the object path is retained separately from allocation details" + *err.metadata().next().expect("the lookup records its path"), + [("path".into(), MetadataValue::from(path.as_path()))] + .into_iter() + .collect::<gix_error::Metadata>(), + "the lookup retains only the native object path" ); - assert_eq!(err.metadata().count(), 2, "only the two caller contexts carry metadata"); + assert_eq!(err.metadata().count(), 1, "only the lookup path carries metadata"); assert!( !err.is_corrupted() && !err.can_retry(), "an unrepresentable allocation cannot be repaired by retrying" diff --git a/gix-pack/benches/pack_create.rs b/gix-pack/benches/pack_create.rs index b42d552343e..35c85f95942 100644 --- a/gix-pack/benches/pack_create.rs +++ b/gix-pack/benches/pack_create.rs @@ -71,15 +71,18 @@ impl io::Write for CountingSink { /// Phase 2: resolve, sort and encode `counts` into a pack stream, returning the pack size in bytes. fn write_pack(odb: &Memory, counts: Vec<output::Count>) -> u64 { let num_objects = counts.len() as u32; - let entries = InOrderIter::from(entry::iter_from_counts( - counts, - odb.clone(), - Box::new(progress::Discard), - entry::iter_from_counts::Options { - mode: entry::iter_from_counts::Mode::PackCopyAndBaseObjects, - ..Default::default() - }, - )); + let entries = InOrderIter::from( + entry::iter_from_counts( + counts, + odb.clone(), + Box::new(progress::Discard), + entry::iter_from_counts::Options { + mode: entry::iter_from_counts::Mode::PackCopyAndBaseObjects, + ..Default::default() + }, + ) + .expect("resolving in-memory object locations succeeds"), + ); let mut sink = CountingSink::default(); let mut iter = FromEntriesIter::new( entries, diff --git a/gix-pack/fuzz/fuzz_targets/data_file.rs b/gix-pack/fuzz/fuzz_targets/data_file.rs index e452200bd7f..b416107a84d 100644 --- a/gix-pack/fuzz/fuzz_targets/data_file.rs +++ b/gix-pack/fuzz/fuzz_targets/data_file.rs @@ -38,7 +38,7 @@ fn fuzz(input: &[u8]) -> Result<()> { let mut out = Vec::new(); let mut inflate = gix_zlib::Inflate::default(); let mut cache = cache::Never; - _ = black_box(pack.decode_entry(entry, &mut out, &mut inflate, &|_, _| None, &mut cache)); + _ = black_box(pack.decode_entry(entry, &mut out, &mut inflate, &|_, _| Ok(None), &mut cache)); } Ok(()) diff --git a/gix-pack/fuzz/fuzz_targets/multi_index_file.rs b/gix-pack/fuzz/fuzz_targets/multi_index_file.rs index 36cc1d579e1..33ed7c6899e 100644 --- a/gix-pack/fuzz/fuzz_targets/multi_index_file.rs +++ b/gix-pack/fuzz/fuzz_targets/multi_index_file.rs @@ -27,7 +27,9 @@ fn fuzz(input: &[u8]) -> Result<()> { _ = black_box(index.index_names()); _ = black_box(index.checksum()); _ = black_box(index.verify_checksum(&mut progress::Discard, &interrupt_flag())); - _ = black_box(index.iter().take(8).count()); + index.iter().take(8).for_each(|entry| { + _ = black_box(entry); + }); if index.num_objects() > 0 { let first = index.oid_at_index(0).to_owned(); diff --git a/gix-pack/src/bundle/find.rs b/gix-pack/src/bundle/find.rs index 9915e9a39d9..b75e416de1d 100644 --- a/gix-pack/src/bundle/find.rs +++ b/gix-pack/src/bundle/find.rs @@ -45,11 +45,14 @@ impl crate::Bundle { out, inflate, &|id, _out| { - let idx = self.index.lookup(id)?; - self.pack - .entry(self.index.pack_offset_at_index(idx)) - .ok() - .map(crate::data::decode::entry::ResolvedBase::InPack) + self.index + .lookup(id) + .map(|idx| { + self.pack + .entry(self.index.pack_offset_at_index(idx)) + .map(crate::data::decode::entry::ResolvedBase::InPack) + }) + .transpose() }, cache, ) diff --git a/gix-pack/src/bundle/write/mod.rs b/gix-pack/src/bundle/write/mod.rs index 8d73728adea..e70207b3baf 100644 --- a/gix-pack/src/bundle/write/mod.rs +++ b/gix-pack/src/bundle/write/mod.rs @@ -50,8 +50,9 @@ impl crate::Bundle { /// /// * `progress` provides detailed progress information which can be discarded with [`gix_features::progress::Discard`]. /// * `should_interrupt` is checked regularly and when true, the whole operation will stop. - /// * `thin_pack_base_object_lookup` If set, we expect to see a thin-pack with objects that reference their base object by object id which is - /// expected to exist in the object database the bundle is contained within. + /// * `thin_pack_base_object_lookup` retrieves external bases to insert into the output pack, completing a thin pack. + /// Ref-deltas whose bases are not found through this lookup are resolved in-pack during indexing, including forward + /// references. A base available neither through the lookup nor in the pack causes the operation to fail. /// * `object_hash` specifies the hash to use for writing the bundle. /// * `options` further configure how the task is performed. /// diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index 413c4abae48..d779fbdfe8b 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -19,16 +19,23 @@ impl<T> Tree<T> { /// * `pack_path` is the path to the pack file itself and from which to read the entry data, which is a pack file matching the offsets /// returned by `get_pack_offset(…)`. /// * `progress` is used to track progress when creating the tree. - /// * `resolve_in_pack_id(gix_hash::oid) -> Option<data::Offset>` takes an object ID and tries to resolve it to an object within this pack if - /// possible. Failing to do so aborts the operation, and this function is not expected to be called in usual packs. It's a theoretical - /// possibility though as old packs might have referred to their objects using the 20 bytes hash, instead of their encoded offset from the base. + /// * `resolve_in_pack_id(&gix_hash::oid) -> Result<Option<data::Offset>>` takes an object ID and tries to resolve it to an offset + /// within this pack. `Err` aborts the operation with the resolver's error unchanged; `Ok(None)` aborts with a missing-base error. + /// The callback is only called for ref-deltas, which refer to their base by object ID instead of an encoded offset. + /// + /// # Ref-delta bases + /// + /// This constructor requires every base to have an offset in this pack and an item in `data_sorted_by_offsets`. + /// Forward references are supported: children whose bases occur later are attached before traversal. + /// Unlike streaming indexing, this constructor does not defer base-ID lookup until objects are decoded or insert + /// external bases. To complete thin packs, use the streaming indexing path with `data::input::LookupRefDeltaObjectsIter`. /// /// Note that the sort order is ascending. The given pack file path must match the provided offsets. pub fn from_offsets_in_pack( pack_path: &std::path::Path, data_sorted_by_offsets: impl Iterator<Item = T>, get_pack_offset: &dyn Fn(&T) -> data::Offset, - resolve_in_pack_id: &dyn Fn(&gix_hash::oid) -> Option<data::Offset>, + resolve_in_pack_id: &dyn Fn(&gix_hash::oid) -> Result<Option<data::Offset>>, progress: &mut dyn Progress, should_interrupt: &AtomicBool, object_hash: gix_hash::Kind, @@ -75,8 +82,8 @@ impl<T> Tree<T> { tree.add_root(pack_offset, data)?; } RefDelta { base_id } => { - let base_pack_offset = resolve_in_pack_id(base_id.as_ref()).ok_or_raise(|| { - message!("Could find object with id {base_id} in this pack. Thin packs are not supported") + let base_pack_offset = resolve_in_pack_id(base_id.as_ref())?.ok_or_raise(|| { + message!("Base object {base_id} was not found in this pack; offset-based tree construction requires in-pack bases") .not_found() })?; tree.add_child(base_pack_offset, pack_offset, data)?; diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index 2e4162251a9..d071ef2066f 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -74,6 +74,10 @@ where { /// Traverse this tree of delta objects with a function `inspect_object` to process each object at will. /// + /// Forward references with known base offsets are attached before traversal. Children recorded by base object ID + /// during streaming indexing are attached when their bases are fully decoded and hashed, including bases that are + /// themselves deltas. All bases must be represented in this tree; external thin-pack bases must be inserted beforehand. + /// /// * `should_run_in_parallel() -> bool` returns true if the underlying pack is big enough to warrant parallel traversal at all. /// * `resolve(EntrySlice, &mut Vec<u8>) -> Option<()>` resolves the bytes in the pack for the given `EntrySlice` and stores them in the /// output vector. It returns `Some(())` if the object existed in the pack, or `None` to indicate a resolution error, which would abort the diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index 6917716cf5a..18b419b2705 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -150,6 +150,7 @@ impl<T> Tree<T> { } /// Add a child of the item at `base_offset` which itself resides at pack `offset` and associate custom `data` with it. + /// If the base has not been added yet, defer attachment until the tree is finalized before traversal. pub(crate) fn add_child( &mut self, base_offset: crate::data::Offset, @@ -192,6 +193,7 @@ impl<T> Tree<T> { } /// Add a child whose base is identified by object id and may occur anywhere in the pack. + /// During traversal, each fully resolved object is hashed and any children waiting for its ID are attached to it. #[cfg(feature = "streaming-input")] pub(crate) fn add_child_by_id( &mut self, @@ -291,6 +293,8 @@ mod tests { mod from_offsets_in_pack { use std::sync::atomic::AtomicBool; + use gix_error::{ErrorExt, TestResult, message}; + use crate as pack; const SMALL_PACK_INDEX: &str = "objects/pack/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx"; @@ -326,7 +330,7 @@ mod tests { pack_file.path(), std::iter::once(()), &|_| first_entry_offset, - &|_| None, + &|_| Ok(None), &mut gix_features::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, @@ -349,7 +353,7 @@ mod tests { pack_file.path(), [first, second].into_iter(), &|offset| *offset, - &|_| None, + &|_| Ok(None), &mut gix_features::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, @@ -364,13 +368,165 @@ mod tests { Ok(()) } + #[test] + fn ref_delta_resolver_error_is_propagated_unchanged() -> TestResult { + let (pack_file, offsets) = ref_delta_pack()?; + let callback_error = std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + message("base index is inaccessible").with("path", "base-index"), + ) + .and_raise(message("could not resolve in-pack base").with("operation", "base lookup")); + let original_sources: Vec<_> = callback_error + .iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect(); + let original_classes: Vec<_> = callback_error + .classify() + .map(|classification| classification.class()) + .collect(); + let callback_error = std::cell::RefCell::new(Some(callback_error)); + let err = crate::cache::delta::Tree::from_offsets_in_pack( + pack_file.path(), + offsets.into_iter(), + &|offset| *offset, + &|base_id| { + assert_eq!( + base_id, + gix_hash::Kind::Sha1.null().as_ref(), + "the resolver receives the ref-delta's base ID" + ); + Err(callback_error + .borrow_mut() + .take() + .expect("the failing resolver is called only once")) + }, + &mut gix_features::progress::Discard, + &AtomicBool::new(false), + gix_hash::Kind::Sha1, + ) + .err() + .expect("a resolver error must abort tree construction"); + + assert_eq!( + err.iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect::<Vec<_>>(), + original_sources, + "the resolver's sources and locations survive without added tree context" + ); + assert_eq!( + err.classify() + .map(|classification| classification.class()) + .collect::<Vec<_>>(), + original_classes, + "tree construction preserves all resolver error classifications" + ); + assert_eq!( + err.downcast_any_ref::<std::io::Error>() + .expect("the resolver's concrete I/O error remains available") + .kind(), + std::io::ErrorKind::PermissionDenied, + "the native failure remains available for recovery" + ); + assert!(!err.is_not_found(), "a resolver failure is not a missing base"); + Ok(()) + } + + #[test] + fn ref_delta_resolver_none_is_a_missing_base() -> TestResult { + let (pack_file, offsets) = ref_delta_pack()?; + let resolutions = std::cell::Cell::new(0); + let err = crate::cache::delta::Tree::from_offsets_in_pack( + pack_file.path(), + offsets.into_iter(), + &|offset| *offset, + &|_| { + resolutions.set(resolutions.get() + 1); + Ok(None) + }, + &mut gix_features::progress::Discard, + &AtomicBool::new(false), + gix_hash::Kind::Sha1, + ) + .err() + .expect("Ok(None) means the ref-delta base is absent from the pack"); + + assert_eq!(resolutions.get(), 1, "a missing base is looked up only once"); + assert!( + err.is_not_found(), + "an absent base retains its not-found classification" + ); + assert_eq!( + err.to_string(), + format!( + "Base object {} was not found in this pack; offset-based tree construction requires in-pack bases", + gix_hash::Kind::Sha1.null() + ), + "a missing base identifies the ID and the offset-based constructor's in-pack requirement" + ); + Ok(()) + } + + #[test] + fn ref_delta_resolver_some_attaches_child_to_base() -> TestResult { + let (pack_file, offsets) = ref_delta_pack()?; + let resolutions = std::cell::Cell::new(0); + let tree = crate::cache::delta::Tree::from_offsets_in_pack( + pack_file.path(), + offsets.into_iter(), + &|offset| *offset, + &|base_id| { + resolutions.set(resolutions.get() + 1); + assert_eq!( + base_id, + gix_hash::Kind::Sha1.null().as_ref(), + "the resolver receives the ref-delta's base ID" + ); + Ok(Some(offsets[0])) + }, + &mut gix_features::progress::Discard, + &AtomicBool::new(false), + gix_hash::Kind::Sha1, + )?; + + assert_eq!(resolutions.get(), 1, "only the ref-delta requires a base lookup"); + assert_eq!(tree.root_items.len(), 1, "the blob remains the sole root"); + assert_eq!(tree.child_items.len(), 1, "the ref-delta becomes the sole child"); + assert_eq!(tree.root_items[0].offset, offsets[0], "the base keeps its pack offset"); + assert_eq!( + tree.root_items[0].children(), + &[0], + "the resolved base owns the ref-delta child" + ); + assert_eq!( + tree.child_items[0].data, offsets[1], + "the child retains its caller-provided data" + ); + Ok(()) + } + + fn ref_delta_pack() -> TestResult<(gix_testtools::tempfile::NamedTempFile, [pack::data::Offset; 2])> { + let pack_file = gix_testtools::tempfile::NamedTempFile::new()?; + let mut pack_data = pack::data::header::encode(pack::data::Version::V2, 2).to_vec(); + // Tree construction only reads entry headers, so compressed payloads and a trailer are unnecessary. + let base_offset = pack_data.len() as pack::data::Offset; + pack::data::entry::Header::Blob.write_to(0, &mut pack_data)?; + let delta_offset = pack_data.len() as pack::data::Offset; + pack::data::entry::Header::RefDelta { + base_id: gix_hash::Kind::Sha1.null(), + } + .write_to(0, &mut pack_data)?; + std::fs::write(pack_file.path(), pack_data)?; + Ok((pack_file, [base_offset, delta_offset])) + } + fn tree(index_path: &str, pack_path: &str) -> gix_testtools::Result { let idx = pack::index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; crate::cache::delta::Tree::from_offsets_in_pack( &fixture_path(pack_path), idx.sorted_offsets().into_iter(), &|ofs| *ofs, - &|id| idx.lookup(id).map(|index| idx.pack_offset_at_index(index)), + &|id| Ok(idx.lookup(id).map(|index| idx.pack_offset_at_index(index))), &mut gix_features::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, diff --git a/gix-pack/src/cache/mod.rs b/gix-pack/src/cache/mod.rs index 83a124c2cb7..0e71613ad7b 100644 --- a/gix-pack/src/cache/mod.rs +++ b/gix-pack/src/cache/mod.rs @@ -73,7 +73,7 @@ pub mod object; /// pack_path, /// offsets.into_iter(), /// &|offset| *offset, -/// &|_id| None, // self-contained pack: no ref-delta lookups +/// &|_id| Ok(None), // self-contained pack: no ref-delta lookups /// &mut gix_features::progress::Discard, /// &AtomicBool::default(), /// gix_hash::Kind::default(), diff --git a/gix-pack/src/data/file/decode/entry.rs b/gix-pack/src/data/file/decode/entry.rs index f610e7ca80b..42fae8cebc6 100644 --- a/gix-pack/src/data/file/decode/entry.rs +++ b/gix-pack/src/data/file/decode/entry.rs @@ -188,16 +188,18 @@ where /// `inflate` will be used for decompressing entries, and will not be reset after usage, but before first using it. /// /// `resolve` is a function to lookup objects with the given [`ObjectId`][gix_hash::ObjectId], in case the full object id is used to refer to - /// a base object, instead of an in-pack offset. + /// a base object, instead of an in-pack offset. Return `Ok(None)` if the base could not be found; + /// lookup errors are propagated unchanged instead of being treated as missing bases. /// /// `delta_cache` is a mechanism to avoid looking up base objects multiple times when decompressing multiple objects in a row. /// Use a [Noop-Cache][cache::Never] to disable caching all together at the cost of repeating work. + #[expect(clippy::type_complexity)] // Keep the fallible resolver's arguments and result explicit. pub fn decode_entry( &self, entry: data::Entry, out: &mut Vec<u8>, inflate: &mut gix_zlib::Inflate, - resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Option<ResolvedBase>, + resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Result<Option<ResolvedBase>>, delta_cache: &mut dyn cache::DecodeEntry, ) -> Result<Outcome> { use crate::data::entry::Header::*; @@ -221,10 +223,11 @@ where /// resolve: technically, this shouldn't ever be required as stored local packs don't refer to objects by id /// that are outside of the pack. Unless, of course, the ref refers to an object within this pack, which means /// it's very, very large as 20bytes are smaller than the corresponding MSB encoded number + #[expect(clippy::type_complexity)] // Match the public decoder's resolver signature. fn resolve_deltas( &self, last: data::Entry, - resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Option<ResolvedBase>, + resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Result<Option<ResolvedBase>>, inflate: &mut gix_zlib::Inflate, out: &mut Vec<u8>, cache: &mut dyn cache::DecodeEntry, @@ -305,7 +308,7 @@ where .or_error()?; self.entry(offset)? } - Header::RefDelta { base_id } => match resolve(base_id.as_ref(), out) { + Header::RefDelta { base_id } => match resolve(base_id.as_ref(), out)? { Some(ResolvedBase::InPack(entry)) => entry, Some(ResolvedBase::OutOfPack { end, kind }) => { base_buffer_size = Some(end); diff --git a/gix-pack/src/data/file/decode/header.rs b/gix-pack/src/data/file/decode/header.rs index db4ce843373..d4780052793 100644 --- a/gix-pack/src/data/file/decode/header.rs +++ b/gix-pack/src/data/file/decode/header.rs @@ -46,7 +46,8 @@ where /// `inflate` will be used for (partially) decompressing entries, and will be reset before first use, but not after the last use. /// /// `resolve` is a function to lookup objects with the given [`ObjectId`][gix_hash::ObjectId], in case the full object id - /// is used to refer to a base object, instead of an in-pack offset. + /// is used to refer to a base object, instead of an in-pack offset. Return `Ok(None)` if the base could not be found; + /// lookup errors are propagated unchanged instead of being treated as missing bases. /// /// For delta entries, this only probes the initial delta header bytes to determine the result /// object size. It can reject streams that end or overflow within that probe, but it does not @@ -57,7 +58,7 @@ where &self, mut entry: data::Entry, inflate: &mut gix_zlib::Inflate, - resolve: &dyn Fn(&gix_hash::oid) -> Option<ResolvedBase>, + resolve: &dyn Fn(&gix_hash::oid) -> Result<Option<ResolvedBase>>, ) -> Result<Outcome> { use crate::data::entry::Header::*; let mut num_deltas = 0; @@ -90,7 +91,7 @@ where if first_delta_decompressed_size.is_none() { first_delta_decompressed_size = Some(self.decode_delta_object_size(inflate, &entry)?); } - match resolve(base_id.as_ref()) { + match resolve(base_id.as_ref())? { Some(ResolvedBase::InPack(base_entry)) => entry = base_entry, Some(ResolvedBase::OutOfPack { kind, diff --git a/gix-pack/src/data/input/lookup_ref_delta_objects.rs b/gix-pack/src/data/input/lookup_ref_delta_objects.rs index eec5bdc9570..0f0c5e963de 100644 --- a/gix-pack/src/data/input/lookup_ref_delta_objects.rs +++ b/gix-pack/src/data/input/lookup_ref_delta_objects.rs @@ -3,7 +3,15 @@ use gix_hash::ObjectId; use crate::data::{entry::Header, input}; -/// An iterator to resolve thin packs on the fly. +/// An iterator to complete thin packs by inserting externally available base objects on the fly. +/// +/// Each base found through `lookup` is inserted before its first dependent delta. References to inserted bases +/// become offset deltas, and subsequent pack offsets and base distances are adjusted accordingly. +/// Bases not found externally remain ref-deltas for in-pack resolution by +/// [`crate::index::write_data_iter_to_stream`], which supports forward references and attaches children as bases resolve. +/// A base found neither externally nor in the pack causes indexing to fail. +/// +/// Base lookup failures are yielded as errors without ending iteration. pub struct LookupRefDeltaObjectsIter<I, Find> { /// The inner iterator whose entries we will resolve. pub inner: I, @@ -90,8 +98,9 @@ where Header::RefDelta { base_id } => { match self.inserted_entry_length_at_offset.iter().rfind(|e| e.oid == base_id) { None => { - let base_entry = match self.lookup.try_find(&base_id, &mut self.buf).ok()? { - Some(obj) => { + let base_entry = match self.lookup.try_find(&base_id, &mut self.buf) { + Err(err) => return Some(Err(err)), + Ok(Some(obj)) => { let current_pack_offset = entry.pack_offset; let mut entry = match input::Entry::from_data_obj(&obj, 0, self.compression) { Ok(e) => e, @@ -106,7 +115,7 @@ where ); entry } - None => { + Ok(None) => { entry.pack_offset = self.shifted_pack_offset(entry.pack_offset); return Some(Ok(entry)); } diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index eab1931d50d..daaf0ec201f 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -268,7 +268,7 @@ mod expand { &changes_delegate.objects }; for id in objects_ref.iter() { - out.push(id_to_count(db, buf2, id, objects, stats, allow_pack_lookups)); + out.push(id_to_count(db, buf2, id, objects, stats, allow_pack_lookups)?); } break; } @@ -295,7 +295,7 @@ mod expand { out = objects.dissolve(stats); } for id in &traverse_delegate.non_trees { - out.push(id_to_count(db, buf1, id, objects, stats, allow_pack_lookups)); + out.push(id_to_count(db, buf1, id, objects, stats, allow_pack_lookups)?); } break; } @@ -355,17 +355,17 @@ mod expand { objects: &gix_features::progress::AtomicStep, statistics: &mut Outcome, allow_pack_lookups: bool, - ) -> output::Count { + ) -> Result<output::Count> { objects.fetch_add(1, Ordering::Relaxed); statistics.expanded_objects += 1; - output::Count { + Ok(output::Count { id: id.to_owned(), entry_pack_location: if allow_pack_lookups { - PackLocation::LookedUp(db.location_by_oid(id, buf)) + PackLocation::LookedUp(db.location_by_oid(id, buf)?) } else { PackLocation::NotLookedUp }, - } + }) } struct CountingObjects<'a> { diff --git a/gix-pack/src/data/output/entry/iter_from_counts.rs b/gix-pack/src/data/output/entry/iter_from_counts.rs index a48669b908d..43ce14e7898 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -2,7 +2,7 @@ pub(crate) mod function { use gix_error::Result; use std::{cmp::Ordering, sync::Arc}; - use gix_error::{ResultExt, message}; + use gix_error::{OptionExt, ResultExt, message, not_found}; use gix_features::{ parallel, parallel::SequenceId, @@ -29,7 +29,7 @@ pub(crate) mod function { /// * `options` /// * more configuration /// - /// _Returns_ the checksum of the pack + /// Returns an iterator over entry chunks, or an error if resolving pack locations fails before iteration. /// /// ## Discussion /// @@ -56,8 +56,10 @@ pub(crate) mod function { chunk_size, compression, }: Options, - ) -> impl Iterator<Item = Result<(SequenceId, Vec<output::Entry>)>> - + parallel::reduce::Finalize<Reduce = reduce::Statistics<gix_error::Error>> + ) -> Result< + impl Iterator<Item = Result<(SequenceId, Vec<output::Entry>)>> + + parallel::reduce::Finalize<Reduce = reduce::Statistics<gix_error::Error>>, + > where Find: crate::Find + Send + Clone + 'static, { @@ -84,16 +86,17 @@ pub(crate) mod function { use crate::data::output::count::PackLocation::*; match count.entry_pack_location { LookedUp(_) => continue, - NotLookedUp => count.entry_pack_location = LookedUp(db.location_by_oid(&count.id, buf)), + NotLookedUp => { + count.entry_pack_location = LookedUp(db.location_by_oid(&count.id, buf)?); + } } } progress.lock().inc_by(chunk_size); - Ok::<_, ()>(()) + Ok::<_, gix_error::Error>(()) } }, - parallel::reduce::IdentityWithResult::<(), ()>::default(), - ) - .expect("infallible - we ignore none-existing objects"); + parallel::reduce::IdentityWithResult::<(), gix_error::Error>::default(), + )?; progress.lock().show_throughput(start); } let counts_range_by_pack_id = match mode { @@ -138,7 +141,7 @@ pub(crate) mod function { let progress = Arc::new(parking_lot::Mutex::new(progress)); let chunks = util::ChunkRanges::new(chunk_size, counts.len()); - parallel::reduce::Stepwise::new( + Ok(parallel::reduce::Stepwise::new( chunks.enumerate(), thread_limit, { @@ -187,20 +190,22 @@ pub(crate) mod function { base_index_offset, allow_thin_pack.then_some({ |pack_id, base_offset| { - let (cached_pack_id, cache) = pack_offsets_to_id.get_or_insert_with(|| { - db.pack_offsets_and_oid(pack_id) - .map(|mut v| { - v.sort_by_key(|e| e.0); - (pack_id, v) - }) - .expect("pack used for counts is still available") - }); + if pack_offsets_to_id.is_none() { + let mut offsets = + db.pack_offsets_and_oid(pack_id)?.ok_or_raise(|| { + not_found("Pack used for counts is no longer available") + })?; + offsets.sort_by_key(|e| e.0); + pack_offsets_to_id = Some((pack_id, offsets)); + } + let (cached_pack_id, cache) = + pack_offsets_to_id.as_ref().expect("just set"); debug_assert_eq!(*cached_pack_id, pack_id); stats.ref_delta_objects += 1; - cache + Ok(cache .binary_search_by_key(&base_offset, |e| e.0) .ok() - .map(|idx| cache[idx].1) + .map(|idx| cache[idx].1)) } }), version, @@ -247,7 +252,7 @@ pub(crate) mod function { } }, reduce::Statistics::default(), - ) + )) } } diff --git a/gix-pack/src/data/output/entry/mod.rs b/gix-pack/src/data/output/entry/mod.rs index 29ec47d086a..c471f31c30a 100644 --- a/gix-pack/src/data/output/entry/mod.rs +++ b/gix-pack/src/data/output/entry/mod.rs @@ -55,12 +55,14 @@ impl output::Entry { /// Create an Entry from a previously counted object which is located in a pack. It's `entry` is provided here. /// The `target_version` specifies what kind of target `Entry` version the caller desires. Both supported versions use /// the same entry encoding. + /// + /// Errors from `pack_offset_to_oid` are propagated rather than triggering recompression as if the base were absent. pub fn from_pack_entry( mut entry: find::Entry, count: &output::Count, potential_bases: &[output::Count], bases_index_offset: usize, - pack_offset_to_oid: Option<impl FnMut(u32, u64) -> Option<ObjectId>>, + pack_offset_to_oid: Option<impl FnMut(u32, u64) -> Result<Option<ObjectId>>>, target_version: data::Version, ) -> Option<Result<Self>> { if entry.version != target_version { @@ -88,23 +90,24 @@ impl output::Entry { message("an ofs-delta base distance pointing before pack start").corrupted_error() )); }; - potential_bases - .binary_search_by(|e| { - e.entry_pack_location - .as_ref() - .expect("packed") - .pack_offset - .cmp(&base_offset) - }) - .ok() - .map(|idx| output::entry::Kind::DeltaRef { + match potential_bases.binary_search_by(|e| { + e.entry_pack_location + .as_ref() + .expect("packed") + .pack_offset + .cmp(&base_offset) + }) { + Ok(idx) => Some(output::entry::Kind::DeltaRef { object_index: idx + bases_index_offset, - }) - .or_else(|| { - pack_offset_to_oid - .and_then(|mut f| f(pack_location.pack_id, base_offset)) - .map(|id| output::entry::Kind::DeltaOid { id }) - }) + }), + Err(_) => match pack_offset_to_oid { + Some(mut lookup) => match lookup(pack_location.pack_id, base_offset) { + Ok(base_id) => base_id.map(|id| output::entry::Kind::DeltaOid { id }), + Err(err) => return Some(Err(err)), + }, + None => None, + }, + } } RefDelta { base_id: _ } => None, // ref deltas are for thin packs or legacy, repack them as base objects } diff --git a/gix-pack/src/find_traits.rs b/gix-pack/src/find_traits.rs index 8036fd324ee..2e6e0084dcd 100644 --- a/gix-pack/src/find_traits.rs +++ b/gix-pack/src/find_traits.rs @@ -43,14 +43,16 @@ pub trait Find { pack_cache: &mut dyn crate::cache::DecodeEntry, ) -> Result<Option<(gix_object::Data<'a>, Option<data::entry::Location>)>>; - /// Find the packs location where an object with `id` can be found in the database, or `None` if there is no pack - /// holding the object. + /// Find the pack location where an object with `id` can be found in the database. /// - /// _Note_ that this is always None if the object isn't packed even though it exists as loose object. - fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec<u8>) -> Option<data::entry::Location>; + /// Returns `Ok(None)` if no pack holds the object, including when it exists only as a loose object. + /// Lookup, corruption, and object retrieval failures are returned as errors. + fn location_by_oid(&self, id: &gix_hash::oid, buf: &mut Vec<u8>) -> Result<Option<data::entry::Location>>; /// Obtain a vector of all offsets, in index order, along with their object id. - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(data::Offset, gix_hash::ObjectId)>>; + /// + /// Returns `Ok(None)` if the pack is unavailable, or an error if loading or reading its index fails. + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(data::Offset, gix_hash::ObjectId)>>>; /// Return the [`find::Entry`] for `location` if it is backed by a pack. /// @@ -172,11 +174,11 @@ mod find_impls { (*self).try_find_cached(id, buffer, pack_cache) } - fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<data::entry::Location> { + fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Result<Option<data::entry::Location>> { (*self).location_by_oid(id, buf) } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(data::Offset, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(data::Offset, gix_hash::ObjectId)>>> { (*self).pack_offsets_and_oid(pack_id) } @@ -202,11 +204,11 @@ mod find_impls { self.deref().try_find_cached(id, buffer, pack_cache) } - fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<data::entry::Location> { + fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Result<Option<data::entry::Location>> { self.deref().location_by_oid(id, buf) } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(data::Offset, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(data::Offset, gix_hash::ObjectId)>>> { self.deref().pack_offsets_and_oid(pack_id) } @@ -232,11 +234,11 @@ mod find_impls { self.deref().try_find_cached(id, buffer, pack_cache) } - fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<data::entry::Location> { + fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Result<Option<data::entry::Location>> { self.deref().location_by_oid(id, buf) } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(data::Offset, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(data::Offset, gix_hash::ObjectId)>>> { self.deref().pack_offsets_and_oid(pack_id) } @@ -262,11 +264,11 @@ mod find_impls { self.deref().try_find_cached(id, buffer, pack_cache) } - fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Option<data::entry::Location> { + fn location_by_oid(&self, id: &oid, buf: &mut Vec<u8>) -> Result<Option<data::entry::Location>> { self.deref().location_by_oid(id, buf) } - fn pack_offsets_and_oid(&self, pack_id: u32) -> Option<Vec<(data::Offset, gix_hash::ObjectId)>> { + fn pack_offsets_and_oid(&self, pack_id: u32) -> Result<Option<Vec<(data::Offset, gix_hash::ObjectId)>>> { self.deref().pack_offsets_and_oid(pack_id) } diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index 3dbb8a5ed56..fbdaec31c0c 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -175,10 +175,12 @@ where buf, inflate, &|id, _| { - let index = self.lookup(id)?; - pack.entry(self.pack_offset_at_index(index)) - .ok() - .map(crate::data::decode::entry::ResolvedBase::InPack) + self.lookup(id) + .map(|index| { + pack.entry(self.pack_offset_at_index(index)) + .map(crate::data::decode::entry::ResolvedBase::InPack) + }) + .transpose() }, cache, ) { diff --git a/gix-pack/src/index/traverse/with_index.rs b/gix-pack/src/index/traverse/with_index.rs index 0a75ffe4c98..6112f44e1f6 100644 --- a/gix-pack/src/index/traverse/with_index.rs +++ b/gix-pack/src/index/traverse/with_index.rs @@ -111,7 +111,7 @@ where pack.path(), sorted_entries.into_iter().map(Entry::from), &|e| e.index_entry.pack_offset, - &|id| self.lookup(id).map(|idx| self.pack_offset_at_index(idx)), + &|id| Ok(self.lookup(id).map(|idx| self.pack_offset_at_index(idx))), &mut progress.add_child_with_id("indexing".into(), ProgressId::TreeFromOffsetsObjects.into()), should_interrupt, self.object_hash, diff --git a/gix-pack/src/multi_index/access.rs b/gix-pack/src/multi_index/access.rs index 2f797535e9f..7c2631d6037 100644 --- a/gix-pack/src/multi_index/access.rs +++ b/gix-pack/src/multi_index/access.rs @@ -3,6 +3,8 @@ use std::{ path::{Path, PathBuf}, }; +use gix_error::{Result, bail, corruption, validation}; + use crate::{ data, index::PrefixLookupResult, @@ -110,7 +112,12 @@ where /// Given the `index` ranging from 0 to [File::num_objects()], return the pack index and its absolute offset into the pack. /// /// The pack-index refers to an entry in the [`index_names`][File::index_names()] list, from which the pack can be derived. - pub fn pack_id_and_pack_offset_at_index(&self, index: EntryIndex) -> (PackIndex, data::Offset) { + /// + /// Returns an error if `index` is out of bounds or its pack or large-offset reference is corrupt. + pub fn pack_id_and_pack_offset_at_index(&self, index: EntryIndex) -> Result<(PackIndex, data::Offset)> { + if index >= self.num_objects { + bail!(validation("Multi-index entry index is out of bounds")); + } const OFFSET_ENTRY_SIZE: usize = 4 + 4; let index = index as usize; let start = self.offsets_ofs + index * OFFSET_ENTRY_SIZE; @@ -118,13 +125,20 @@ where const HIGH_BIT: u32 = 1 << 31; let pack_index = crate::read_u32(&self.data[start..][..4]); + if pack_index >= self.num_indices { + bail!(corruption("Multi-index pack index is out of bounds")); + } let offset = &self.data[start + 4..][..4]; let ofs32 = crate::read_u32(offset); let pack_offset = if (ofs32 & HIGH_BIT) == HIGH_BIT { // We determine if large offsets are actually larger than 4GB and if not, we don't use the high-bit to signal anything // but allow the presence of the large-offset chunk to signal what's happening. - if let Some(offsets_64) = self.large_offsets_ofs { - let from = offsets_64 + (ofs32 ^ HIGH_BIT) as usize * 8; + if let Some(offsets_64) = &self.large_offsets { + let ordinal = (ofs32 ^ HIGH_BIT) as usize; + if ordinal >= offsets_64.len() / 8 { + bail!(corruption("Multi-index large-offset index is out of bounds")); + } + let from = offsets_64.start + ordinal * 8; crate::read_u64(&self.data[from..][..8]) } else { u64::from(ofs32) @@ -132,18 +146,18 @@ where } else { u64::from(ofs32) }; - (pack_index, pack_offset) + Ok((pack_index, pack_offset)) } - /// Return an iterator over all entries within this file. - pub fn iter(&self) -> impl Iterator<Item = Entry> + '_ { + /// Return an iterator over all entries within this file, reporting corrupt references as errors. + pub fn iter(&self) -> impl Iterator<Item = Result<Entry>> + '_ { (0..self.num_objects).map(move |idx| { - let (pack_index, pack_offset) = self.pack_id_and_pack_offset_at_index(idx); - Entry { + let (pack_index, pack_offset) = self.pack_id_and_pack_offset_at_index(idx)?; + Ok(Entry { oid: self.oid_at_index(idx).to_owned(), pack_offset, pack_index, - } + }) }) } } diff --git a/gix-pack/src/multi_index/init.rs b/gix-pack/src/multi_index/init.rs index 113a8809dd1..30e6df88231 100644 --- a/gix-pack/src/multi_index/init.rs +++ b/gix-pack/src/multi_index/init.rs @@ -35,6 +35,8 @@ where { /// Instantiate a multi-index file from `data` as assumed to be read or memory-mapped from `path`. /// + /// Only the structure is validated here; per-object pack and large-offset references are checked on access. + /// /// `alloc_limit_bytes` bounds each allocation caused by untrusted on-disk multi-index data. /// Use `None` to disable the limit. /// @@ -138,7 +140,7 @@ where alloc_limit_bytes, lookup_ofs: lookup.start, offsets_ofs: offsets.start, - large_offsets_ofs: large_offsets.map(|r| r.start), + large_offsets, num_objects, num_indices, }) diff --git a/gix-pack/src/multi_index/mod.rs b/gix-pack/src/multi_index/mod.rs index 2e81cf924cc..f923e4adda2 100644 --- a/gix-pack/src/multi_index/mod.rs +++ b/gix-pack/src/multi_index/mod.rs @@ -35,7 +35,7 @@ pub struct File<T = MMap> { index_names: Vec<PathBuf>, lookup_ofs: usize, offsets_ofs: usize, - large_offsets_ofs: Option<usize>, + large_offsets: Option<std::ops::Range<usize>>, } /// diff --git a/gix-pack/src/multi_index/verify.rs b/gix-pack/src/multi_index/verify.rs index 32440b20cf4..7e6685aba39 100644 --- a/gix-pack/src/multi_index/verify.rs +++ b/gix-pack/src/multi_index/verify.rs @@ -150,13 +150,13 @@ where "The object id at multi-index entry {entry_index} wasn't in order" ))); } - let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index); + let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index)?; pack_ids_and_offsets.push((pack_id, entry_index)); progress.inc(); } { let entry_index = self.num_objects - 1; - let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index); + let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index)?; pack_ids_and_offsets.push((pack_id, entry_index)); } // sort by pack-id to allow handling all indices matching a pack while its open. @@ -204,7 +204,7 @@ where for entry_id in multi_index_entries_to_check.iter().map(|e| e.1) { let oid = self.oid_at_index(entry_id); - let (_, expected_pack_offset) = self.pack_id_and_pack_offset_at_index(entry_id); + let (_, expected_pack_offset) = self.pack_id_and_pack_offset_at_index(entry_id)?; let entry_in_bundle_index = index.lookup(oid).ok_or_raise(|| { message!("{oid} wasn't found in the index referenced in the multi-pack index").corrupted() })?; diff --git a/gix-pack/src/testing.rs b/gix-pack/src/testing.rs index a91861471c5..34c65790f89 100644 --- a/gix-pack/src/testing.rs +++ b/gix-pack/src/testing.rs @@ -38,12 +38,12 @@ impl crate::Find for Memory { })) } - fn location_by_oid(&self, _id: &gix_hash::oid, _buf: &mut Vec<u8>) -> Option<crate::data::entry::Location> { - None + fn location_by_oid(&self, _id: &gix_hash::oid, _buf: &mut Vec<u8>) -> Result<Option<crate::data::entry::Location>> { + Ok(None) } - fn pack_offsets_and_oid(&self, _pack_id: u32) -> Option<Vec<(crate::data::Offset, gix_hash::ObjectId)>> { - None + fn pack_offsets_and_oid(&self, _pack_id: u32) -> Result<Option<Vec<(crate::data::Offset, gix_hash::ObjectId)>>> { + Ok(None) } fn entry_by_location(&self, _location: &crate::data::entry::Location) -> Option<Entry> { diff --git a/gix-pack/tests/pack/data/file.rs b/gix-pack/tests/pack/data/file.rs index 18128c96e47..5fada16a264 100644 --- a/gix-pack/tests/pack/data/file.rs +++ b/gix-pack/tests/pack/data/file.rs @@ -97,7 +97,7 @@ mod method { entry.clone(), &mut buf, &mut inflate, - &|_, _| None, + &|_, _| Ok(None), &mut gix_odb::pack::cache::Never ) .is_ok(), @@ -112,7 +112,7 @@ mod method { entry, &mut buf, &mut inflate, - &|_, _| None, + &|_, _| Ok(None), &mut gix_odb::pack::cache::Never, ) .expect_err("pack-controlled allocations larger than the configured limit are rejected"); @@ -138,6 +138,7 @@ mod method { /// All hardcoded offsets are obtained via `git pack-verify --verbose tests/fixtures/packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx` mod decode_entry { use bstr::ByteSlice; + use gix_error::Result; use gix_pack::{cache, data::decode::entry::ResolvedBase}; use crate::{SMALL_PACK, data::file::pack_at, fixture_path, fixup}; @@ -188,7 +189,7 @@ mod decode_entry { } fn decode_entry_at_offset(offset: u64) -> Vec<u8> { - fn resolve_with_panic(_oid: &gix_hash::oid, _out: &mut Vec<u8>) -> Option<ResolvedBase> { + fn resolve_with_panic(_oid: &gix_hash::oid, _out: &mut Vec<u8>) -> Result<Option<ResolvedBase>> { panic!("should not want to resolve an id here") } @@ -209,6 +210,8 @@ mod decode_entry { /// All hardcoded offsets are obtained via `git pack-verify --verbose tests/fixtures/packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx` mod resolve_header { + use gix_error::Result; + use crate::{SMALL_PACK, data::file::pack_at}; #[test] @@ -244,7 +247,7 @@ mod resolve_header { } fn resolve_header_at_offset(offset: u64) -> gix_pack::data::decode::header::Outcome { - fn resolve_with_panic(_oid: &gix_hash::oid) -> Option<gix_pack::data::decode::header::ResolvedBase> { + fn resolve_with_panic(_oid: &gix_hash::oid) -> Result<Option<gix_pack::data::decode::header::ResolvedBase>> { panic!("should not want to resolve an id here") } diff --git a/gix-pack/tests/pack/data/fuzzed.rs b/gix-pack/tests/pack/data/fuzzed.rs index c8618a2be2c..61b7c600cdf 100644 --- a/gix-pack/tests/pack/data/fuzzed.rs +++ b/gix-pack/tests/pack/data/fuzzed.rs @@ -93,7 +93,7 @@ fn non_canonical_pack_entry_header_is_accepted() { entry, &mut out, &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) .expect("non-canonical entry decodes like a canonical git object"); @@ -124,7 +124,7 @@ fn oversized_declared_object_size_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -170,7 +170,7 @@ fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -222,7 +222,7 @@ fn runaway_delta_allocation_is_rejected_with_fuzz_alloc_limit() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -265,7 +265,7 @@ fn invalid_ofs_delta_base_distance_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -325,7 +325,7 @@ fn out_of_bounds_entry_data_offset_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -383,7 +383,7 @@ fn malformed_delta_instruction_relocation_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -439,7 +439,7 @@ fn runaway_delta_chain_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) .map(|_| ()) @@ -495,7 +495,7 @@ fn overlong_delta_header_size_is_reported_without_panicking() { entry, &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); @@ -556,7 +556,7 @@ fn short_delta_application_is_reported_without_panicking() { file.entry(delta_pack_offset).expect("delta entry is parseable"), &mut Vec::new(), &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut gix_pack::cache::Never, ) })); diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs index 0750ccec31a..dcf4a4f44b3 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -37,7 +37,7 @@ fn invalid_ofs_delta_base_distance_is_an_error() -> Result { &count, &[], 0, - None::<fn(u32, u64) -> Option<gix_hash::ObjectId>>, + None::<fn(u32, u64) -> gix_error::Result<Option<gix_hash::ObjectId>>>, gix_pack::data::Version::V2, ); @@ -56,6 +56,79 @@ fn invalid_ofs_delta_base_distance_is_an_error() -> Result { Ok(()) } +#[test] +fn thin_pack_base_lookup_distinguishes_errors_from_absence() -> gix_error::TestResult { + use gix_error::ErrorExt; + + let mut data = Vec::new(); + gix_pack::data::entry::Header::OfsDelta { base_distance: 4 }.write_to(0, &mut data)?; + let base_offset = gix_pack::data::header::SIZE as u64; + let count = output::Count::from_data( + object_hash().null(), + Some(gix_pack::data::entry::Location { + pack_id: 0, + pack_offset: base_offset + 4, + entry_size: data.len(), + }), + ); + let entry = gix_pack::find::Entry { + data, + version: gix_pack::data::Version::V2, + }; + assert!( + output::Entry::from_pack_entry( + entry.clone(), + &count, + &[], + 0, + Some(|_, _| Ok(None)), + gix_pack::data::Version::V2, + ) + .is_none(), + "an absent base can trigger recompression instead of reusing the delta" + ); + let err = output::Entry::from_pack_entry( + entry.clone(), + &count, + &[], + 0, + Some(|_, _| Err(gix_error::corruption("invalid base reference").raise())), + gix_pack::data::Version::V2, + ) + .expect("a failed lookup must not request recompression") + .expect_err("corrupt base references must remain errors"); + assert!( + err.is_corrupted(), + "the callback's corruption classification is preserved" + ); + assert_eq!( + err.to_string(), + "invalid base reference", + "the original diagnostic is preserved" + ); + + let base_id = object_hash().empty_blob(); + let reused = output::Entry::from_pack_entry( + entry, + &count, + &[], + 0, + Some(|pack_id, offset| { + assert_eq!(pack_id, 0, "the callback receives the delta's pack"); + assert_eq!(offset, base_offset, "the callback receives the validated base offset"); + Ok(Some(base_id)) + }), + gix_pack::data::Version::V2, + ) + .expect("a found base permits delta reuse")?; + assert_eq!( + reused.kind, + output::entry::Kind::DeltaOid { id: base_id }, + "a found base becomes a ref-delta" + ); + Ok(()) +} + #[test] fn traversals() -> Result { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -374,7 +447,7 @@ fn traversals() -> Result { allow_thin_pack, ..Default::default() }, - ); + )?; let entries: Vec<_> = InOrderIter::from(entries_iter.by_ref()) .collect::<std::result::Result<Vec<_>, _>>()? .into_iter() diff --git a/gix-pack/tests/pack/malformed.rs b/gix-pack/tests/pack/malformed.rs index 22741f8cfe7..53994c40358 100644 --- a/gix-pack/tests/pack/malformed.rs +++ b/gix-pack/tests/pack/malformed.rs @@ -5,6 +5,7 @@ use std::{ path::PathBuf, }; +use gix_error::{ErrorExt, TestResult, message}; use gix_pack::{cache, data}; const FIRST_ENTRY_OFFSET: data::Offset = data::header::SIZE as data::Offset; @@ -20,7 +21,7 @@ fn plain_object_buffer_growth_respects_alloc_limit() -> Result { pack.entry(FIRST_ENTRY_OFFSET)?, &mut out, &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut cache::Never, )?; assert_eq!(out, [b'A'; 65], "the object at the limit must decode correctly"); @@ -59,7 +60,7 @@ fn combined_delta_work_buffers_respect_alloc_limit() -> Result { pack.entry(delta_offset)?, &mut out, &mut Default::default(), - &|_, _| None, + &|_, _| Ok(None), &mut cache::Never, ); if limit.is_some_and(|limit| limit < 132) { @@ -101,10 +102,10 @@ fn resolved_base_and_delta_instructions_respect_alloc_limit() -> Result { &|_, out| { out.clear(); out.extend_from_slice(&[b'A'; 64]); - Some(data::decode::entry::ResolvedBase::OutOfPack { + Ok(Some(data::decode::entry::ResolvedBase::OutOfPack { kind: gix_object::Kind::Blob, end: out.len(), - }) + })) }, &mut cache::Never, ) @@ -121,6 +122,304 @@ fn resolved_base_and_delta_instructions_respect_alloc_limit() -> Result { Ok(()) } +#[test] +fn ref_delta_resolver_error_is_propagated_unchanged() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("resolver-error.pack"), gix_hash::Kind::Sha1)?; + let callback_error = std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + message("base storage is inaccessible").with("path", "external-base"), + ) + .and_raise(message("could not resolve delta base").with("operation", "base lookup")); + let original_sources: Vec<_> = callback_error + .iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect(); + let original_classes: Vec<_> = callback_error + .classify() + .map(|classification| classification.class()) + .collect(); + let callback_error = std::cell::RefCell::new(Some(callback_error)); + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut Vec::new(), + &mut Default::default(), + &|base_id, _| { + resolutions.set(resolutions.get() + 1); + assert_eq!( + base_id, + gix_hash::Kind::Sha1.null().as_ref(), + "the resolver receives the synthetic external base ID" + ); + Err(callback_error + .borrow_mut() + .take() + .expect("the failing resolver is called only once")) + }, + &mut cache::Never, + ) + .expect_err("a resolver failure must abort decoding instead of becoming an unresolved base"); + + assert_eq!(resolutions.get(), 1, "a failed base lookup must not be retried"); + assert_eq!( + err.iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect::<Vec<_>>(), + original_sources, + "the callback's context, native sources, and locations must survive without added decode context" + ); + assert_eq!( + err.classify() + .map(|classification| classification.class()) + .collect::<Vec<_>>(), + original_classes, + "decoding must preserve every callback error classification" + ); + let native = err + .downcast_any_ref::<std::io::Error>() + .expect("the resolver's concrete I/O error remains available for recovery"); + assert_eq!( + native.kind(), + std::io::ErrorKind::PermissionDenied, + "the native failure must not be replaced with a missing-base error" + ); + assert!( + native + .get_ref() + .expect("the native I/O error retains its payload") + .is::<gix_error::Message>(), + "the native source payload retains its concrete type" + ); + assert_eq!( + err.dominant_class(), + Some(gix_error::Class::PermissionDenied), + "base-resolution permissions remain actionable at the decode boundary" + ); + assert!(!err.is_not_found(), "a resolver failure is not a missing delta base"); + assert!( + err.downcast_any_ref::<data::decode::DeltaBaseUnresolved>().is_none(), + "DeltaBaseUnresolved must only be constructed for a successful lookup returning None" + ); + Ok(()) +} + +#[test] +fn ref_delta_resolver_none_is_an_unresolved_base() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("missing-base.pack"), gix_hash::Kind::Sha1)?; + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut Vec::new(), + &mut Default::default(), + &|_, _| { + resolutions.set(resolutions.get() + 1); + Ok(None) + }, + &mut cache::Never, + ) + .expect_err("Ok(None) means the ref-delta base could not be found"); + + assert_eq!(resolutions.get(), 1, "a missing base must not trigger repeated lookups"); + assert!(err.is_not_found(), "an unresolved delta base remains a missing object"); + assert!( + err.probable_cause().is::<data::decode::DeltaBaseUnresolved>(), + "the unresolved base remains the typed probable cause" + ); + assert_eq!( + err.downcast_any_ref::<data::decode::DeltaBaseUnresolved>() + .expect("the missing-base error retains its concrete type") + .0, + gix_hash::Kind::Sha1.null(), + "the unresolved error identifies the requested synthetic base" + ); + Ok(()) +} + +#[test] +fn ref_delta_resolver_some_decodes_external_base() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("external-base.pack"), gix_hash::Kind::Sha1)?; + let mut out = Vec::new(); + let resolutions = std::cell::Cell::new(0); + let decoded = pack.decode_entry( + pack.entry(FIRST_ENTRY_OFFSET)?, + &mut out, + &mut Default::default(), + &|base_id, out| { + resolutions.set(resolutions.get() + 1); + resolve_external_blob(base_id, out) + }, + &mut cache::Never, + )?; + + assert_eq!(resolutions.get(), 1, "the successful callback resolves the base once"); + assert_eq!( + decoded.kind, + gix_object::Kind::Blob, + "the base determines the object kind" + ); + assert_eq!( + decoded.num_deltas, 1, + "the external base completes the one-entry delta chain" + ); + assert_eq!( + decoded.object_size, 1, + "the decoded size matches the delta result header" + ); + assert_eq!(out, b"A", "Ok(Some(base)) preserves successful delta application"); + Ok(()) +} + +#[test] +fn ref_delta_header_resolver_error_is_propagated_unchanged() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("header-resolver-error.pack"), gix_hash::Kind::Sha1)?; + let callback_error = std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + message("base storage is inaccessible").with("path", "external-base"), + ) + .and_raise(message("could not resolve delta base header").with("operation", "base header lookup")); + let original_sources: Vec<_> = callback_error + .iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect(); + let original_classes: Vec<_> = callback_error + .classify() + .map(|classification| classification.class()) + .collect(); + let callback_error = std::cell::RefCell::new(Some(callback_error)); + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_header(pack.entry(FIRST_ENTRY_OFFSET)?, &mut Default::default(), &|base_id| { + resolutions.set(resolutions.get() + 1); + assert_eq!( + base_id, + gix_hash::Kind::Sha1.null().as_ref(), + "the header resolver receives the synthetic external base ID" + ); + Err(callback_error + .borrow_mut() + .take() + .expect("the failing header resolver is called only once")) + }) + .expect_err("a resolver failure must abort header decoding instead of becoming an unresolved base"); + + assert_eq!(resolutions.get(), 1, "a failed base header lookup must not be retried"); + assert_eq!( + err.iter_errors_with_locations() + .map(|source| (source.error().to_string(), source.location())) + .collect::<Vec<_>>(), + original_sources, + "the callback's context, native sources, and locations must survive without added header decode context" + ); + assert_eq!( + err.classify() + .map(|classification| classification.class()) + .collect::<Vec<_>>(), + original_classes, + "header decoding must preserve every callback error classification" + ); + let native = err + .downcast_any_ref::<std::io::Error>() + .expect("the header resolver's concrete I/O error remains available for recovery"); + assert_eq!( + native.kind(), + std::io::ErrorKind::PermissionDenied, + "the native failure must not be replaced with a missing-base error" + ); + assert!( + native + .get_ref() + .expect("the native I/O error retains its payload") + .is::<gix_error::Message>(), + "the native source payload retains its concrete type" + ); + assert_eq!( + err.dominant_class(), + Some(gix_error::Class::PermissionDenied), + "base-resolution permissions remain actionable at the header decode boundary" + ); + assert!( + !err.is_not_found(), + "a header resolver failure is not a missing delta base" + ); + assert!( + err.downcast_any_ref::<data::decode::DeltaBaseUnresolved>().is_none(), + "DeltaBaseUnresolved must only be constructed for a successful header lookup returning None" + ); + Ok(()) +} + +#[test] +fn ref_delta_header_resolver_none_is_an_unresolved_base() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("header-missing-base.pack"), gix_hash::Kind::Sha1)?; + let resolutions = std::cell::Cell::new(0); + let err = pack + .decode_header(pack.entry(FIRST_ENTRY_OFFSET)?, &mut Default::default(), &|_| { + resolutions.set(resolutions.get() + 1); + Ok(None) + }) + .expect_err("Ok(None) means the ref-delta base header could not be found"); + + assert_eq!( + resolutions.get(), + 1, + "a missing base header must not trigger repeated lookups" + ); + assert!( + err.is_not_found(), + "an unresolved delta base header remains a missing object" + ); + assert!( + err.probable_cause().is::<data::decode::DeltaBaseUnresolved>(), + "the unresolved base remains the typed probable cause" + ); + assert_eq!( + err.downcast_any_ref::<data::decode::DeltaBaseUnresolved>() + .expect("the missing-base error retains its concrete type") + .0, + gix_hash::Kind::Sha1.null(), + "the unresolved header error identifies the requested synthetic base" + ); + Ok(()) +} + +#[test] +fn ref_delta_header_resolver_some_decodes_external_base() -> TestResult { + let bytes = ref_delta_pack(&[1, 1, 0x90, 1])?; + let pack = data::File::from_data(bytes, PathBuf::from("header-external-base.pack"), gix_hash::Kind::Sha1)?; + let resolutions = std::cell::Cell::new(0); + let decoded = pack.decode_header(pack.entry(FIRST_ENTRY_OFFSET)?, &mut Default::default(), &|base_id| { + resolutions.set(resolutions.get() + 1); + resolve_external_header_blob(base_id) + })?; + + assert_eq!( + resolutions.get(), + 1, + "the successful header callback resolves the base once" + ); + assert_eq!( + decoded.kind, + gix_object::Kind::Blob, + "the base header determines the object kind" + ); + assert_eq!( + decoded.num_deltas, 1, + "the external base header completes the one-entry delta chain" + ); + assert_eq!( + decoded.object_size, 1, + "the decoded size matches the delta result header" + ); + Ok(()) +} + #[test] fn ref_delta_header_cycles_are_rejected() -> Result { for (num_entries, close_with_ofs) in [(1, false), (2, false), (3, false), (2, true)] { @@ -131,9 +430,9 @@ fn ref_delta_header_cycles_are_rejected() -> Result { resolutions.set(resolutions.get() + 1); assert!(resolutions.get() < 20, "a cyclic header lookup must terminate promptly"); let offset = offsets[usize::from(base_id.as_bytes()[0]) % offsets.len()]; - Some(data::decode::header::ResolvedBase::InPack( + Ok(Some(data::decode::header::ResolvedBase::InPack( pack.entry(offset).expect("the synthetic delta entry exists"), - )) + ))) }) .expect_err("cyclic ref-delta bases must not be accepted"); assert!(err.is_corrupted(), "a delta cycle is corrupt pack data: {err}"); @@ -157,9 +456,9 @@ fn ref_delta_entry_cycles_are_rejected() -> Result { resolutions.set(resolutions.get() + 1); assert!(resolutions.get() < 20, "a cyclic object lookup must terminate promptly"); let offset = offsets[usize::from(base_id.as_bytes()[0]) % offsets.len()]; - Some(data::decode::entry::ResolvedBase::InPack( + Ok(Some(data::decode::entry::ResolvedBase::InPack( pack.entry(offset).expect("the synthetic delta entry exists"), - )) + ))) }, &mut cache::Never, ) @@ -180,7 +479,7 @@ fn delta_chain_metadata_respects_alloc_limit() -> Result { &mut Vec::new(), &mut Default::default(), &|base_id, _| { - Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Ok(Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { Some(&offset) => data::decode::entry::ResolvedBase::InPack( pack.entry(offset).expect("the synthetic delta entry exists"), ), @@ -188,7 +487,7 @@ fn delta_chain_metadata_respects_alloc_limit() -> Result { kind: gix_object::Kind::Blob, end: 0, }, - }) + })) }, &mut cache::Never, ) @@ -209,7 +508,7 @@ fn forward_ref_delta_chain_is_accepted() -> Result { let (pack, offsets) = ref_delta_chain(12, &[0, 0], false)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; let header = pack.decode_header(entry.clone(), &mut Default::default(), &|base_id| { - Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Ok(Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { Some(&offset) => data::decode::header::ResolvedBase::InPack( pack.entry(offset).expect("the synthetic delta entry exists"), ), @@ -217,7 +516,7 @@ fn forward_ref_delta_chain_is_accepted() -> Result { kind: gix_object::Kind::Blob, num_deltas: None, }, - }) + })) })?; let mut out = Vec::new(); let decoded = pack.decode_entry( @@ -225,7 +524,7 @@ fn forward_ref_delta_chain_is_accepted() -> Result { &mut out, &mut Default::default(), &|base_id, _| { - Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { + Ok(Some(match offsets.get(usize::from(base_id.as_bytes()[0])) { Some(&offset) => data::decode::entry::ResolvedBase::InPack( pack.entry(offset).expect("the synthetic delta entry exists"), ), @@ -233,7 +532,7 @@ fn forward_ref_delta_chain_is_accepted() -> Result { kind: gix_object::Kind::Blob, end: 0, }, - }) + })) }, &mut cache::Never, )?; @@ -600,13 +899,16 @@ fn ref_delta_pack_with_declared_size(delta: &[u8], decompressed_size: u64) -> Re Ok(pack) } -fn resolve_external_blob(_id: &gix_hash::oid, out: &mut Vec<u8>) -> Option<data::decode::entry::ResolvedBase> { +fn resolve_external_blob( + _id: &gix_hash::oid, + out: &mut Vec<u8>, +) -> gix_error::Result<Option<data::decode::entry::ResolvedBase>> { out.clear(); out.extend_from_slice(b"A"); - Some(data::decode::entry::ResolvedBase::OutOfPack { + Ok(Some(data::decode::entry::ResolvedBase::OutOfPack { kind: gix_object::Kind::Blob, end: 1, - }) + })) } /// Resolve the synthetic ref-delta base for `decode_header()` tests. @@ -614,9 +916,9 @@ fn resolve_external_blob(_id: &gix_hash::oid, out: &mut Vec<u8>) -> Option<data: /// Header decoding uses a resolver that only reports base metadata, unlike `decode_entry()`, /// which also needs the base bytes in `out`. Providing this resolver lets malformed ref-delta /// fixtures reach delta-header parsing without failing earlier on the unresolved `_id`. -fn resolve_external_header_blob(_id: &gix_hash::oid) -> Option<data::decode::header::ResolvedBase> { - Some(data::decode::header::ResolvedBase::OutOfPack { +fn resolve_external_header_blob(_id: &gix_hash::oid) -> gix_error::Result<Option<data::decode::header::ResolvedBase>> { + Ok(Some(data::decode::header::ResolvedBase::OutOfPack { kind: gix_object::Kind::Blob, num_deltas: None, - }) + })) } diff --git a/gix-pack/tests/pack/multi_index/access.rs b/gix-pack/tests/pack/multi_index/access.rs index aea12552016..eb7492e215c 100644 --- a/gix-pack/tests/pack/multi_index/access.rs +++ b/gix-pack/tests/pack/multi_index/access.rs @@ -42,6 +42,7 @@ fn lookup_with_ambiguity() { /// The fixture is expected to contain at least one ambiguous prefix for each supported object hash. fn ambiguous_prefix(file: &gix_pack::multi_index::File) -> gix_hash::Prefix { for entry in file.iter() { + let entry = entry.expect("fixture contains valid pack and offset references"); for hex_len in 4..=file.object_hash().len_in_hex() { let prefix = gix_hash::Prefix::new(&entry.oid, hex_len).unwrap(); if matches!(file.lookup_prefix(prefix, None), Some(Err(()))) { @@ -57,6 +58,7 @@ fn lookup_prefix() { let (file, _path) = multi_index(object_hash()); for (idx, entry) in file.iter().enumerate() { + let entry = entry.expect("fixture contains valid pack and offset references"); for mut candidates in [None, Some(0..0)] { let hex_len = (idx % file.object_hash().len_in_hex()).max(5); let hex_oid = entry.oid.to_hex_with_len(hex_len).to_string(); @@ -159,15 +161,20 @@ fn general() { let actual_oid = file.oid_at_index(*idx); assert_eq!(actual_oid, *expected_oid); assert_eq!(file.lookup(actual_oid), Some(*idx)); - let (pack_id, pack_offset) = file.pack_id_and_pack_offset_at_index(*idx); + let (pack_id, pack_offset) = file + .pack_id_and_pack_offset_at_index(*idx) + .expect("fixture contains valid pack and offset references"); assert_eq!(pack_id, 0, "we only have one pack here"); assert_eq!(pack_offset, *expected_pack_offset); } let mut count = 0; for (idx, entry) in file.iter().enumerate() { + let entry = entry.expect("fixture contains valid pack and offset references"); assert_eq!(entry.oid, file.oid_at_index(idx as u32)); - let (pack_index, pack_offset) = file.pack_id_and_pack_offset_at_index(idx as u32); + let (pack_index, pack_offset) = file + .pack_id_and_pack_offset_at_index(idx as u32) + .expect("fixture contains valid pack and offset references"); assert_eq!(pack_index, entry.pack_index); assert_eq!(pack_offset, entry.pack_offset); count += 1; diff --git a/gix-pack/tests/pack/multi_index/fuzzed.rs b/gix-pack/tests/pack/multi_index/fuzzed.rs index 26962cb2a36..3a3c5a1d504 100644 --- a/gix-pack/tests/pack/multi_index/fuzzed.rs +++ b/gix-pack/tests/pack/multi_index/fuzzed.rs @@ -102,6 +102,140 @@ fn absurd_pack_count_is_rejected_with_fuzz_alloc_limit() { insta::assert_debug_snapshot!(err, "the error explains the structural failure", @"Pack count exceeds the available pack-name data"); } +#[test] +fn out_of_bounds_pack_indices_are_rejected_on_access() -> gix_error::TestResult { + for pack_index in [1, u32::MAX] { + let index = gix_pack::multi_index::File::from_data( + multi_index_with_offset(b"a.idx", pack_index, 0, None), + PathBuf::from("invalid-pack-index.midx"), + None, + )?; + assert_eq!( + index.lookup(gix_hash::Kind::Sha1.null()), + Some(0), + "loading and OID lookup stay lazy" + ); + let err = index + .pack_id_and_pack_offset_at_index(0) + .expect_err("pack indices must refer to an existing pack name"); + assert!(err.is_corrupted(), "out-of-bounds pack indices are corrupt data"); + assert_eq!( + err.to_string(), + "Multi-index pack index is out of bounds", + "the invalid pack index is rejected before accessing a pack" + ); + let err = index + .iter() + .next() + .expect("there is one object") + .expect_err("iteration validates pack references"); + assert!(err.is_corrupted(), "iteration preserves errors"); + } + Ok(()) +} + +#[test] +fn out_of_bounds_large_offsets_are_rejected_on_access() -> gix_error::TestResult { + for ordinal in [1, 0x7fff_ffff] { + let index = gix_pack::multi_index::File::from_data( + multi_index_with_offset(b"a.idx", 0, (1 << 31) | ordinal, Some(&[1 << 32])), + PathBuf::from("invalid-large-offset.midx"), + None, + )?; + assert_eq!( + index.lookup(gix_hash::Kind::Sha1.null()), + Some(0), + "loading and OID lookup stay lazy" + ); + let err = index + .pack_id_and_pack_offset_at_index(0) + .expect_err("large-offset ordinals must stay inside the LOFF chunk, including its checksum boundary"); + assert!(err.is_corrupted(), "out-of-bounds large offsets are corrupt data"); + assert_eq!( + err.to_string(), + "Multi-index large-offset index is out of bounds", + "the invalid large-offset ordinal is rejected before reading the LOFF table" + ); + let err = index + .iter() + .next() + .expect("there is one object") + .expect_err("iteration validates LOFF references"); + assert!(err.is_corrupted(), "iteration preserves corruption errors"); + } + Ok(()) +} + +#[test] +fn out_of_bounds_entry_indices_are_validation_errors() -> gix_error::TestResult { + let index = gix_pack::multi_index::File::from_data( + valid_multi_index_with_index_name(b"a.idx"), + PathBuf::from("valid.midx"), + None, + )?; + for entry_index in [index.num_objects(), u32::MAX] { + let err = index + .pack_id_and_pack_offset_at_index(entry_index) + .expect_err("caller-provided entry indices must stay in bounds"); + assert!( + err.is_validation(), + "an invalid caller index is not corrupt on-disk data" + ); + assert!(!err.is_corrupted(), "the file itself remains valid"); + } + Ok(()) +} + +#[test] +fn integrity_verification_rejects_corrupt_offset_references() -> gix_error::TestResult { + for mut data in [ + multi_index_with_offset(b"a.idx", 1, 0, None), + multi_index_with_offset(b"a.idx", 0, (1 << 31) | 1, Some(&[1 << 32])), + ] { + let checksum_start = data.len() - gix_hash::Kind::Sha1.len_in_bytes(); + let mut hasher = gix_hash::hasher(gix_hash::Kind::Sha1); + hasher.update(&data[..checksum_start]); + data[checksum_start..].copy_from_slice(hasher.try_finalize()?.as_slice()); + let index = gix_pack::multi_index::File::from_data(data, "invalid-reference.midx".into(), None)?; + let err = index + .verify_integrity_fast( + &mut gix_features::progress::Discard, + &std::sync::atomic::AtomicBool::new(false), + ) + .expect_err("verification must inspect references even though loading does not"); + assert!(err.is_corrupted(), "verification retains corruption classification"); + assert!( + err.to_string().contains("index is out of bounds"), + "a valid checksum lets verification reach the corrupt reference before opening packs: {err}" + ); + } + Ok(()) +} + +#[test] +fn high_bit_offsets_follow_the_presence_of_the_large_offset_chunk() -> gix_error::TestResult { + for (offset, large_offsets, expected) in [ + (1 << 31, Some(&[1 << 32][..]), 1 << 32), + ((1 << 31) | 1, Some(&[1 << 32, 1 << 33][..]), 1 << 33), + (42, Some(&[1 << 32][..]), 42), + (1 << 31, None, 1 << 31), + (u32::MAX, None, u64::from(u32::MAX)), + ] { + let index = gix_pack::multi_index::File::from_data( + multi_index_with_offset(b"a.idx", 0, offset, large_offsets), + PathBuf::from("valid-large-offset.midx"), + None, + ) + .expect("a high bit indicates a large-offset ordinal only when LOFF is present"); + assert_eq!( + index.pack_id_and_pack_offset_at_index(0)?, + (0, expected), + "both large-offset ordinals and literal 32-bit offsets remain supported" + ); + } + Ok(()) +} + fn malformed_multi_index_with_inconsistent_fanout() -> Vec<u8> { const HEADER_LEN: usize = 12; const TOC_LEN: usize = 5 * 12; @@ -193,25 +327,32 @@ fn multi_index_with_absurd_pack_count() -> Vec<u8> { } fn valid_multi_index_with_index_name(index_name: &[u8]) -> Vec<u8> { + multi_index_with_offset(index_name, 0, 0, None) +} + +/// Build a one-object index with an optional LOFF chunk, so offset validation can distinguish +/// literal 32-bit offsets from ordinals without relying on large packs or filesystem fixtures. +fn multi_index_with_offset(index_name: &[u8], pack_index: u32, offset: u32, large_offsets: Option<&[u64]>) -> Vec<u8> { const HEADER_LEN: usize = 12; - const TOC_LEN: usize = 5 * 12; const FAN_LEN: usize = 256 * 4; const LOOKUP_LEN: usize = 20; const OFFSETS_LEN: usize = 8; const TRAILER_LEN: usize = 20; let pnam_len = index_name.len() + 1; - let pnam_start = HEADER_LEN + TOC_LEN; + let num_chunks = 4 + u8::from(large_offsets.is_some()); + let pnam_start = HEADER_LEN + (usize::from(num_chunks) + 1) * 12; let fan_start = pnam_start + pnam_len; let lookup_start = fan_start + FAN_LEN; let offsets_start = lookup_start + LOOKUP_LEN; - let trailer_start = offsets_start + OFFSETS_LEN; + let large_offsets_start = offsets_start + OFFSETS_LEN; + let trailer_start = large_offsets_start + large_offsets.map_or(0, std::mem::size_of_val); let mut data = Vec::with_capacity(trailer_start + TRAILER_LEN); data.extend_from_slice(b"MIDX"); data.push(1); data.push(gix_hash::Kind::Sha1 as u8); - data.push(4); + data.push(num_chunks); data.push(0); data.extend_from_slice(&1u32.to_be_bytes()); @@ -219,19 +360,24 @@ fn valid_multi_index_with_index_name(index_name: &[u8]) -> Vec<u8> { push_chunk(&mut data, b"OIDF", fan_start as u64); push_chunk(&mut data, b"OIDL", lookup_start as u64); push_chunk(&mut data, b"OOFF", offsets_start as u64); + if large_offsets.is_some() { + push_chunk(&mut data, b"LOFF", large_offsets_start as u64); + } push_chunk(&mut data, b"\0\0\0\0", trailer_start as u64); data.extend_from_slice(index_name); data.push(0); - for fan_idx in 0..256 { - let count = if fan_idx == 255 { 1u32 } else { 0u32 }; - data.extend_from_slice(&count.to_be_bytes()); + for _ in 0..256 { + data.extend_from_slice(&1u32.to_be_bytes()); } data.extend_from_slice(gix_hash::Kind::Sha1.null().as_slice()); - data.extend_from_slice(&0u32.to_be_bytes()); - data.extend_from_slice(&0u32.to_be_bytes()); + data.extend_from_slice(&pack_index.to_be_bytes()); + data.extend_from_slice(&offset.to_be_bytes()); + for offset in large_offsets.into_iter().flatten() { + data.extend_from_slice(&offset.to_be_bytes()); + } data.extend_from_slice(&[0; TRAILER_LEN]); debug_assert_eq!(data.len(), trailer_start + TRAILER_LEN); data From e8cd238974416d1244fd70018a6ad3296a6cf30f Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Mon, 5 Oct 2026 19:50:58 +0200 Subject: [PATCH 18/28] change(gix-error)!: wrap `Metadata` and simplify diagnostic keys <!-- agent --> Quoting every metadata key adds noise to diagnostic output even when the name is already unambiguous. Keep nonempty keys containing only ASCII letters, digits, `_`, `-`, or `.` unquoted in `Message` display and metadata debug output. Empty and unusual keys remain quoted and escaped, and value formatting is unchanged. Replace the public `BTreeMap` alias with a private-storage `Metadata` wrapper so dictionary formatting and its public API no longer expose the implementation. Preserve lexicographic ordering and provide construction, lookup, mutation, iteration, indexing, collection, and extension APIs. **Breaking change:** `Metadata` is no longer interchangeable with `BTreeMap`. Callers must use the wrapper API; iterator return types hide the underlying collection. Add regression coverage for key quoting, compact values, and dictionary access, and update dependent assertions and output snapshots. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- gitoxide-core/tests/organize.rs | 4 +- gix-actor/tests/actor/identity.rs | 4 +- gix-actor/tests/actor/signature.rs | 6 +- gix-attributes/tests/attributes/parse.rs | 44 ++-- gix-config-value/tests/value/path.rs | 2 +- gix-config/tests/config/file/access/mutate.rs | 4 +- .../tests/config/file/access/raw/raw_value.rs | 4 +- .../config/file/access/raw/set_raw_value.rs | 2 +- .../tests/config/file/access/read_only.rs | 12 +- gix-config/tests/config/file/init/from_env.rs | 2 +- .../tests/config/file/mutable/section.rs | 6 +- gix-config/tests/config/parse/section.rs | 12 +- gix-credentials/tests/helper/cascade.rs | 6 +- gix-credentials/tests/helper/context.rs | 8 +- gix-credentials/tests/helper/invoke.rs | 2 +- gix-date/tests/time/parse/mod.rs | 2 +- gix-discover/tests/discover/parse.rs | 6 +- gix-error/src/concrete/metadata.rs | 131 ++++++++++- gix-error/tests/error/classification.rs | 8 +- gix-error/tests/error/convert.rs | 2 +- gix-error/tests/error/error.rs | 6 +- gix-error/tests/error/exn.rs | 6 +- gix-error/tests/error/metadata.rs | 217 ++++++++++++++---- gix-error/tests/error/test.rs | 8 +- gix-filter/tests/filter/driver.rs | 6 +- gix-mailmap/tests/mailmap/parse.rs | 16 +- gix-object/tests/object/encode.rs | 2 +- gix-object/tests/object/tree/editor.rs | 2 +- gix-odb/tests/odb/error.rs | 38 +-- gix-odb/tests/odb/store/loose.rs | 16 +- gix-path/tests/path/relative_path.rs | 24 +- gix-pathspec/tests/defaults.rs | 2 +- gix-pathspec/tests/normalize/mod.rs | 10 +- gix-pathspec/tests/parse/invalid.rs | 86 +++---- gix-protocol/tests/protocol/fetch/ref_map.rs | 2 +- .../loose/reflog/create_or_update/tests.rs | 2 +- gix-ref/src/store/file/transaction/prepare.rs | 15 +- gix-ref/tests/refs/error.rs | 50 ++-- gix-ref/tests/refs/file/log.rs | 4 +- gix-ref/tests/refs/file/reference.rs | 4 +- gix-ref/tests/refs/file/store/find.rs | 2 +- gix-ref/tests/refs/file/store/iter.rs | 2 +- gix-ref/tests/refs/file/store/reflog.rs | 4 +- .../create_or_update/collisions.rs | 4 +- .../create_or_update/mod.rs | 89 ++++++- .../transaction/prepare_and_commit/delete.rs | 8 +- gix-ref/tests/refs/file/worktree.rs | 12 +- gix-ref/tests/refs/packed/find.rs | 4 +- gix-ref/tests/refs/packed/iter.rs | 10 +- gix-ref/tests/refs/packed/open.rs | 2 +- gix-refspec/tests/refspec/parse/fetch.rs | 6 +- gix-refspec/tests/refspec/parse/invalid.rs | 22 +- .../revision/spec/parse/anchor/at_symbol.rs | 24 +- .../spec/parse/anchor/colon_symbol.rs | 2 +- .../revision/spec/parse/anchor/refnames.rs | 2 +- .../tests/revision/spec/parse/kind.rs | 12 +- .../spec/parse/navigate/caret_symbol.rs | 20 +- gix-submodule/tests/file/mod.rs | 16 +- gix-url/tests/url/parse/file.rs | 2 +- gix-url/tests/url/parse/http.rs | 2 +- gix-url/tests/url/parse/invalid.rs | 48 ++-- .../tests/worktree-state/checkout.rs | 8 +- gix/src/config/mod.rs | 2 +- gix/src/config/tree/keys.rs | 2 +- .../connection/fetch/update_refs/tests.rs | 8 +- gix/tests/gix-init.rs | 12 +- gix/tests/gix/clone.rs | 6 +- gix/tests/gix/commit.rs | 2 +- gix/tests/gix/config/file_mut.rs | 2 +- gix/tests/gix/config/tree.rs | 26 +-- gix/tests/gix/init.rs | 8 +- gix/tests/gix/reference/mod.rs | 2 +- gix/tests/gix/remote/connect.rs | 4 +- gix/tests/gix/repository/config/mod.rs | 14 +- gix/tests/gix/repository/object.rs | 4 +- gix/tests/gix/repository/open.rs | 10 +- gix/tests/gix/repository/remote.rs | 8 +- .../gix/revision/spec/from_bytes/ambiguous.rs | 8 +- gix/tests/gix/revision/spec/from_bytes/mod.rs | 16 +- .../gix/revision/spec/from_bytes/traverse.rs | 2 +- gix/tests/gix/submodule.rs | 8 +- .../invalid-branch-name-failure | 2 +- tests/snapshots/porcelain/init/fail | 2 +- .../init/fail-with-multi-element-directory | 2 +- 84 files changed, 773 insertions(+), 461 deletions(-) diff --git a/gitoxide-core/tests/organize.rs b/gitoxide-core/tests/organize.rs index 517ff5db8a0..a98119b50c0 100644 --- a/gitoxide-core/tests/organize.rs +++ b/gitoxide-core/tests/organize.rs @@ -35,7 +35,7 @@ fn url_components_cannot_move_repositories_outside_the_destination() -> TestResu Failed to handle 1 repository Caused by: - 0: the origin URL resolves outside the repository destination, "destination"="<fixture>/escaped", "destination_root"="<fixture>/destination" + 0: the origin URL resolves outside the repository destination, destination="<fixture>/escaped", destination_root="<fixture>/destination" "# ); } @@ -107,7 +107,7 @@ fn an_existing_symlink_cannot_redirect_the_destination() -> TestResult { Failed to handle 1 repository Caused by: - 0: the origin URL resolves outside the repository destination, "destination"="<fixture>/outside/repository", "destination_root"="<fixture>/destination" + 0: the origin URL resolves outside the repository destination, destination="<fixture>/outside/repository", destination_root="<fixture>/destination" "# ); } diff --git a/gix-actor/tests/actor/identity.rs b/gix-actor/tests/actor/identity.rs index 6a3a96bfe96..c258bf2815c 100644 --- a/gix-actor/tests/actor/identity.rs +++ b/gix-actor/tests/actor/identity.rs @@ -48,11 +48,11 @@ fn lenient_parsing() -> gix_testtools::Result { [ Custom { kind: Other, - error: Signature name or email must not contain '<', '>' or \n, "input"="fl <First Last<fl@openoffice.org >> ", + error: Signature name or email must not contain '<', '>' or \n, input="fl <First Last<fl@openoffice.org >> ", }, Custom { kind: Other, - error: Signature name or email must not contain '<', '>' or \n, "input"="fl <First Last<fl@openoffice.org", + error: Signature name or email must not contain '<', '>' or \n, input="fl <First Last<fl@openoffice.org", }, ] "#); diff --git a/gix-actor/tests/actor/signature.rs b/gix-actor/tests/actor/signature.rs index 72d3537671a..f697f40a81f 100644 --- a/gix-actor/tests/actor/signature.rs +++ b/gix-actor/tests/actor/signature.rs @@ -13,7 +13,7 @@ mod write_to { insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature names reject angle brackets", @r#" Custom { kind: Other, - error: Signature name or email must not contain '<', '>' or \n, "input"="invalid < middlename", + error: Signature name or email must not contain '<', '>' or \n, input="invalid < middlename", } "#); } @@ -28,7 +28,7 @@ mod write_to { insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature email addresses reject angle brackets", @r#" Custom { kind: Other, - error: Signature name or email must not contain '<', '>' or \n, "input"="server>.example.com", + error: Signature name or email must not contain '<', '>' or \n, input="server>.example.com", } "#); } @@ -43,7 +43,7 @@ mod write_to { insta::assert_debug_snapshot!(signature.write_to(&mut Vec::new()).expect_err("the signature is invalid"), "signature names reject newlines", @r#" Custom { kind: Other, - error: Signature name or email must not contain '<', '>' or \n, "input"="hello\nnewline", + error: Signature name or email must not contain '<', '>' or \n, input="hello\nnewline", } "#); } diff --git a/gix-attributes/tests/attributes/parse.rs b/gix-attributes/tests/attributes/parse.rs index 44beeec9e51..087cab162cb 100644 --- a/gix-attributes/tests/attributes/parse.rs +++ b/gix-attributes/tests/attributes/parse.rs @@ -96,9 +96,9 @@ fn exclamation_marks_must_be_escaped_or_error_unlike_gitignore() { line(r"\!hello"), (pattern(r"!hello", Mode::NO_SUB_DIR, None), vec![], 1) ); - insta::assert_debug_snapshot!(assert_validation(try_line(r"!hello")), "exclamation marks must be escaped or error unlike gitignore", @r#"Line 1 has a negative pattern, for literal characters use \!, "input"="!hello""#); + insta::assert_debug_snapshot!(assert_validation(try_line(r"!hello")), "exclamation marks must be escaped or error unlike gitignore", @r#"Line 1 has a negative pattern, for literal characters use \!, input="!hello""#); assert!(lenient_lines(r#"!hello"#).is_empty()); - insta::assert_debug_snapshot!(assert_validation(try_line(r#""!hello""#)), "even in quotes they trigger…", @r#"Line 1 has a negative pattern, for literal characters use \!, "input"="!hello""#); + insta::assert_debug_snapshot!(assert_validation(try_line(r#""!hello""#)), "even in quotes they trigger…", @r#"Line 1 has a negative pattern, for literal characters use \!, input="!hello""#); assert!(lenient_lines(r#""!hello""#).is_empty()); assert_eq!( line(r#""\\!hello""#), @@ -192,26 +192,26 @@ fn custom_macros_must_be_valid_attribute_names() { Macro in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="-prefixdash" + 0: Attribute has non-ascii characters or starts with '-', input="-prefixdash" "#); assert!(lenient_lines(r"[attr]-prefixdash").is_empty()); insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]!exclamation")), "custom macros must be valid attribute names", @r#" Macro in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="!exclamation" + 0: Attribute has non-ascii characters or starts with '-', input="!exclamation" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]assignment=value")), "custom macros must be valid attribute names", @r#" Macro in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="assignment=value" + 0: Attribute has non-ascii characters or starts with '-', input="assignment=value" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]你弽")), "custom macros must be valid attribute names", @r#" Macro in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="你弽" + 0: Attribute has non-ascii characters or starts with '-', input="你弽" "#); assert!(lenient_lines(r"[attr]你弽").is_empty()); } @@ -235,11 +235,11 @@ fn invalid_names_retain_line_context_and_the_validation_cause() { Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="你弽", + 0: Attribute has non-ascii characters or starts with '-', input="你弽", Macro in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="你弽", + 0: Attribute has non-ascii characters or starts with '-', input="你弽", ] "#); } @@ -250,21 +250,21 @@ fn attribute_names_must_not_begin_with_dash_and_must_be_ascii_only() { Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="-a" + 0: Attribute has non-ascii characters or starts with '-', input="-a" "#); assert!(lenient_lines(r"p !-a").is_empty()); insta::assert_debug_snapshot!(assert_validation(try_line(r#"p !!a"#)), "exclamation marks aren't allowed either", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="!a" + 0: Attribute has non-ascii characters or starts with '-', input="!a" "#); assert!(lenient_lines(r#"p !!a"#).is_empty()); insta::assert_debug_snapshot!(assert_validation(try_line(r#"p 你弽"#)), "nor is utf-8 encoded characters - gitoxide could consider to relax this when established", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="你弽" + 0: Attribute has non-ascii characters or starts with '-', input="你弽" "#); assert!(lenient_lines(r#"p 你弽"#).is_empty()); } @@ -275,25 +275,25 @@ fn attribute_names_must_not_be_empty() { Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="" + 0: Attribute has non-ascii characters or starts with '-', input="" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"p =")), "an assignment that is nothing but an equals sign has no name either", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="" + 0: Attribute has non-ascii characters or starts with '-', input="" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"p -")), "prefixes need a name to apply to", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="" + 0: Attribute has non-ascii characters or starts with '-', input="" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"p !")), "the unspecified prefix needs one as well", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="" + 0: Attribute has non-ascii characters or starts with '-', input="" "#); assert!( gix_attributes::NameRef::try_from(bstr::BStr::new(b"")).is_err(), @@ -307,20 +307,20 @@ fn attribute_names_must_not_use_the_reserved_builtin_prefix() { Attribute in line 1 has an invalid name Caused by: - 0: Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_objectmode" + 0: Attribute name uses the reserved 'builtin_' prefix, input="builtin_objectmode" "#); assert!(lenient_lines(r"p builtin_objectmode").is_empty()); insta::assert_debug_snapshot!(assert_validation(try_line(r"p -builtin_objectmode")), "the prefix is checked after '-' and '!' are stripped, just like in `parse_attr()`", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_objectmode" + 0: Attribute name uses the reserved 'builtin_' prefix, input="builtin_objectmode" "#); insta::assert_debug_snapshot!(assert_validation(try_line(r"[attr]builtin_macro -text")), "macro names are checked against the reserved namespace as well", @r#" Macro in line 1 has an invalid name Caused by: - 0: Attribute name uses the reserved 'builtin_' prefix, "input"="builtin_macro" + 0: Attribute name uses the reserved 'builtin_' prefix, input="builtin_macro" "#); assert_eq!( line(r"p builtin"), @@ -383,25 +383,25 @@ fn only_ascii_blanks_separate_attributes() { Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="text\u{a0}eol" + 0: Attribute has non-ascii characters or starts with '-', input="text\u{a0}eol" "#); insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{b}b")), "a vertical tab is part of the name, not a separator", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="a\x0bb" + 0: Attribute has non-ascii characters or starts with '-', input="a\x0bb" "#); insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{c}b")), "a form feed is part of the name, not a separator", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="a\x0cb" + 0: Attribute has non-ascii characters or starts with '-', input="a\x0cb" "#); insta::assert_debug_snapshot!(assert_validation(try_line("p a\u{2028}b")), "vertical tabs, form feeds and unicode line separators aren't blanks either", @r#" Attribute in line 1 has an invalid name Caused by: - 0: Attribute has non-ascii characters or starts with '-', "input"="a\u{2028}b" + 0: Attribute has non-ascii characters or starts with '-', input="a\u{2028}b" "#); } diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index 311b7d04e58..c5b585c43b9 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -143,7 +143,7 @@ mod interpolate { }) .expect_err("the username is not UTF-8"); insta::assert_debug_snapshot!(err, "malformed usernames are validation errors with the utf8 cause", @r#" - Ill-formed UTF-8 in username, "input"="\xff" + Ill-formed UTF-8 in username, input="\xff" Caused by: 0: invalid utf-8 sequence of 1 bytes from index 0 diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index 7c5a6408222..5ca094e089e 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -225,11 +225,11 @@ mod rename_section { let mut file = gix_config::File::try_from("[core] a = b").unwrap(); let err = file.rename_section("core", None, "new_core", None).unwrap_err(); assert!(err.is_validation()); - insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"section names can only be ascii, '-', "input"="new_core""#); + insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"section names can only be ascii, '-', input="new_core""#); let err = file.rename_section("core", None, "new-core", "a\nb").unwrap_err(); assert!(err.is_validation()); - insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"sub-section names must not contain newlines or null bytes, "input"="a\nb""#); + insta::assert_debug_snapshot!(err, "section renaming validates new name", @r#"sub-section names must not contain newlines or null bytes, input="a\nb""#); } #[test] diff --git a/gix-config/tests/config/file/access/raw/raw_value.rs b/gix-config/tests/config/file/access/raw/raw_value.rs index d3520902b3d..6274c0ef594 100644 --- a/gix-config/tests/config/file/access/raw/raw_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_value.rs @@ -141,10 +141,10 @@ fn invalid_value_names_are_reported_by_mutable_lookups() -> Result { let mut config = File::try_from("[core]\na=b")?; let err = config.raw_value_mut_by("core", None, "1invalid").unwrap_err(); assert!(err.is_validation()); - insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="1invalid""#); + insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., input="1invalid""#); let err = config.raw_values_mut_by("core", None, "contains.dot").unwrap_err(); assert!(err.is_validation()); - insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="contains.dot""#); + insta::assert_debug_snapshot!(err, "invalid value names are reported by mutable lookups", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., input="contains.dot""#); Ok(()) } diff --git a/gix-config/tests/config/file/access/raw/set_raw_value.rs b/gix-config/tests/config/file/access/raw/set_raw_value.rs index 9f7a3a45f1d..d911cc617a2 100644 --- a/gix-config/tests/config/file/access/raw/set_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_raw_value.rs @@ -81,6 +81,6 @@ fn invalid_value_names_fail_without_creating_a_section() { let mut file = gix_config::File::default(); let err = file.set_raw_value_by("new", None, "not.valid", "value").unwrap_err(); assert!(err.is_validation()); - insta::assert_debug_snapshot!(err, "invalid value names fail without creating a section", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., "input"="not.valid""#); + insta::assert_debug_snapshot!(err, "invalid value names fail without creating a section", @r#"Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character., input="not.valid""#); assert_eq!(file.sections().count(), 0, "validation precedes section creation"); } diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index 0f5be78f0b3..58cb750d371 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -44,12 +44,12 @@ fn typed_lookup_errors_can_be_erased() -> Result { assert!(err.is_not_found(), "erasure retains missing-value classification"); insta::assert_debug_snapshot!(error_snapshots, "typed lookup errors can be erased", @r#" [ - Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", - Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid", - Colors are specific color values and their attributes, like 'brightred', or 'blue', "input"="invalid", - Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", - Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid", - Colors are specific color values and their attributes, like 'brightred', or 'blue', "input"="invalid", + Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid", + Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, input="invalid", + Colors are specific color values and their attributes, like 'brightred', or 'blue', input="invalid", + Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid", + Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, input="invalid", + Colors are specific color values and their attributes, like 'brightred', or 'blue', input="invalid", ] "#); Ok(()) diff --git a/gix-config/tests/config/file/init/from_env.rs b/gix-config/tests/config/file/init/from_env.rs index e2596a3fdd1..cc718b46b8f 100644 --- a/gix-config/tests/config/file/init/from_env.rs +++ b/gix-config/tests/config/file/init/from_env.rs @@ -35,7 +35,7 @@ fn parse_error_with_invalid_count() -> Result { let err = File::from_env(Default::default()).expect_err("the configuration count is not an integer"); assert!(err.is_validation(), "invalid counts are validation errors"); insta::assert_debug_snapshot!(err, "parse error with invalid count", @r#" - GIT_CONFIG_COUNT was not a positive integer, "input"="invalid" + GIT_CONFIG_COUNT was not a positive integer, input="invalid" Caused by: 0: invalid digit found in string diff --git a/gix-config/tests/config/file/mutable/section.rs b/gix-config/tests/config/file/mutable/section.rs index b53f1ccd03d..a929f6e569c 100644 --- a/gix-config/tests/config/file/mutable/section.rs +++ b/gix-config/tests/config/file/mutable/section.rs @@ -210,7 +210,7 @@ mod value_name_validation { Message { message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", class: Validation, - values: {"input": Bytes("not.valid")}, + values: {input: Bytes("not.valid")}, } "#); let err = section @@ -223,7 +223,7 @@ mod value_name_validation { Message { message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", class: Validation, - values: {"input": Bytes("1invalid")}, + values: {input: Bytes("1invalid")}, } "#); let err = section.set("also invalid", "value").unwrap_err(); @@ -234,7 +234,7 @@ mod value_name_validation { Message { message: "Valid value names consist of alphanumeric characters or dashes, starting with an alphabetic character.", class: Validation, - values: {"input": Bytes("also invalid")}, + values: {input: Bytes("also invalid")}, } "#); assert_eq!(section.num_values(), 0, "validation happens before mutation"); diff --git a/gix-config/tests/config/parse/section.rs b/gix-config/tests/config/parse/section.rs index c2ddd20a4da..805281fa8a4 100644 --- a/gix-config/tests/config/parse/section.rs +++ b/gix-config/tests/config/parse/section.rs @@ -49,10 +49,10 @@ mod header { } insta::assert_debug_snapshot!(message_diagnostics, "names must be mostly ascii", @r#" [ - section names can only be ascii, '-', "input"="🤗", - section names can only be ascii, '-', "input"="x.y", - section names can only be ascii, '-', "input"="x y", - section names can only be ascii, '-', "input"="x\ny", + section names can only be ascii, '-', input="🤗", + section names can only be ascii, '-', input="x.y", + section names can only be ascii, '-', input="x y", + section names can only be ascii, '-', input="x\ny", ] "#); } @@ -68,8 +68,8 @@ mod header { } insta::assert_debug_snapshot!(message_diagnostics, "subsections with newlines and null bytes are rejected", @r#" [ - sub-section names must not contain newlines or null bytes, "input"="a\nb", - sub-section names must not contain newlines or null bytes, "input"="a\0b", + sub-section names must not contain newlines or null bytes, input="a\nb", + sub-section names must not contain newlines or null bytes, input="a\0b", ] "#); } diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index 4ba426e9c1f..46a653d8f5f 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -72,15 +72,15 @@ mod invoke { I/O error (Other) Caused by: - 0: "wwwauth[]"="Basic realm=\"a\rb\"" must not contain null bytes or newlines neither in key nor in value., "input"="Basic realm=\"a\rb\"", + 0: "wwwauth[]"="Basic realm=\"a\rb\"" must not contain null bytes or newlines neither in key nor in value., input="Basic realm=\"a\rb\"", I/O error (Other) Caused by: - 0: "wwwauth[]"="Basic\nusername=other" must not contain null bytes or newlines neither in key nor in value., "input"="Basic\nusername=other", + 0: "wwwauth[]"="Basic\nusername=other" must not contain null bytes or newlines neither in key nor in value., input="Basic\nusername=other", I/O error (Other) Caused by: - 0: "wwwauth[]"="Basic\0realm=example" must not contain null bytes or newlines neither in key nor in value., "input"="Basic\0realm=example", + 0: "wwwauth[]"="Basic\0realm=example" must not contain null bytes or newlines neither in key nor in value., input="Basic\0realm=example", ] "#); } diff --git a/gix-credentials/tests/helper/context.rs b/gix-credentials/tests/helper/context.rs index 98ac8a8593d..82d02c4173f 100644 --- a/gix-credentials/tests/helper/context.rs +++ b/gix-credentials/tests/helper/context.rs @@ -102,15 +102,15 @@ mod write_to { [ Custom { kind: Other, - error: "url"="foo\0" must not contain null bytes or newlines neither in key nor in value., "input"="foo\0", + error: "url"="foo\0" must not contain null bytes or newlines neither in key nor in value., input="foo\0", }, Custom { kind: Other, - error: "url"="foo\n" must not contain null bytes or newlines neither in key nor in value., "input"="foo\n", + error: "url"="foo\n" must not contain null bytes or newlines neither in key nor in value., input="foo\n", }, Custom { kind: Other, - error: "url"="foo\r" must not contain null bytes or newlines neither in key nor in value., "input"="foo\r", + error: "url"="foo\r" must not contain null bytes or newlines neither in key nor in value., input="foo\r", }, ] "#); @@ -192,7 +192,7 @@ username=bob"; #[test] fn null_bytes_when_decoding() { let err = Context::from_bytes(b"url=https://foo\0", ContextOptions::default()).unwrap_err(); - insta::assert_debug_snapshot!(err, "null bytes when decoding", @r#""url"="https://foo\0" must not contain null bytes or newlines neither in key nor in value., "input"="https://foo\0""#); + insta::assert_debug_snapshot!(err, "null bytes when decoding", @r#""url"="https://foo\0" must not contain null bytes or newlines neither in key nor in value., input="https://foo\0""#); } #[test] diff --git a/gix-credentials/tests/helper/invoke.rs b/gix-credentials/tests/helper/invoke.rs index c85a466caf2..d75f271c4d4 100644 --- a/gix-credentials/tests/helper/invoke.rs +++ b/gix-credentials/tests/helper/invoke.rs @@ -90,7 +90,7 @@ mod program { Caused by: 0: I/O error (Other) - 1: Credentials helper program failed, "exit_code"=128, "exit_status"="exit status: 128" + 1: Credentials helper program failed, exit_code=128, exit_status="exit status: 128" "#); assert!( err.classify().next().is_none(), diff --git a/gix-date/tests/time/parse/mod.rs b/gix-date/tests/time/parse/mod.rs index c5d77408478..72a1427f9e6 100644 --- a/gix-date/tests/time/parse/mod.rs +++ b/gix-date/tests/time/parse/mod.rs @@ -199,7 +199,7 @@ fn git_default() { #[test] fn invalid_dates_can_be_produced_without_current_time() { - insta::assert_debug_snapshot!(gix_date::parse("foobar", None).expect_err("invalid dates can be produced without current time"), "invalid dates can be produced without current time", @r#"Unknown date format, "input"="foobar""#); + insta::assert_debug_snapshot!(gix_date::parse("foobar", None).expect_err("invalid dates can be produced without current time"), "invalid dates can be produced without current time", @r#"Unknown date format, input="foobar""#); } /// Tests for compact ISO8601 formats (YYYYMMDDTHHMMSS variants) diff --git a/gix-discover/tests/discover/parse.rs b/gix-discover/tests/discover/parse.rs index 6ce8f3ea42b..65480951b73 100644 --- a/gix-discover/tests/discover/parse.rs +++ b/gix-discover/tests/discover/parse.rs @@ -35,9 +35,9 @@ fn invalid() { } insta::assert_debug_snapshot!(error_snapshots, "invalid", @r#" [ - Format should be 'gitdir: <path>', but got, "input"="gitdir:", - Format should be 'gitdir: <path>', but got, "input"="bogus: foo", - Format should be 'gitdir: <path>', but got, "input"="gitdir: ", + Format should be 'gitdir: <path>', but got, input="gitdir:", + Format should be 'gitdir: <path>', but got, input="bogus: foo", + Format should be 'gitdir: <path>', but got, input="gitdir: ", ] "#); } diff --git a/gix-error/src/concrete/metadata.rs b/gix-error/src/concrete/metadata.rs index fddc74fcc3a..7d1acdcc159 100644 --- a/gix-error/src/concrete/metadata.rs +++ b/gix-error/src/concrete/metadata.rs @@ -6,7 +6,9 @@ use crate::{Class, Error, ErrorExt, ResourceExhaustionKind}; /// /// Functions returning metadata document the keys and their meaning. [`crate::Exn::metadata()`] and /// [`crate::Error::metadata()`] yield non-empty dictionaries separately; dictionaries from independent causes -/// are never merged. +/// are never merged. Keys are iterated and formatted in lexicographic order. +/// Debug formatting and [`Message`]'s display omit key quotes for non-empty names containing only ASCII +/// letters, digits, `_`, `-`, or `.`. Other keys use quoted, escaped string formatting. /// /// # Common schemas /// @@ -41,7 +43,126 @@ use crate::{Class, Error, ErrorExt, ResourceExhaustionKind}; /// /// A failed exit status does not by itself establish a recovery class. Preserve native errors from spawning or /// communicating with a program as causes; their contexts can also use `program`, without an exit status or output. -pub type Metadata = BTreeMap<Cow<'static, str>, MetadataValue>; +#[derive(Clone, Default, PartialEq)] +pub struct Metadata(BTreeMap<Cow<'static, str>, MetadataValue>); + +impl Metadata { + /// Create an empty dictionary. + pub fn new() -> Self { + Self::default() + } + + /// Return the number of values in this dictionary. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Return whether this dictionary has no values. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return whether a value is recorded under `key`. + pub fn contains_key(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Return the value recorded under `key`, if present. + pub fn get(&self, key: &str) -> Option<&MetadataValue> { + self.0.get(key) + } + + /// Return a mutable reference to the value recorded under `key`, if present. + pub fn get_mut(&mut self, key: &str) -> Option<&mut MetadataValue> { + self.0.get_mut(key) + } + + /// Record `value` under `key`, returning any previous value. + pub fn insert( + &mut self, + key: impl Into<Cow<'static, str>>, + value: impl Into<MetadataValue>, + ) -> Option<MetadataValue> { + self.0.insert(key.into(), value.into()) + } + + /// Remove and return the value recorded under `key`, if present. + pub fn remove(&mut self, key: &str) -> Option<MetadataValue> { + self.0.remove(key) + } + + /// Remove all recorded values. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Iterate over keys and values in lexicographic key order. + pub fn iter( + &self, + ) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &MetadataValue)> + ExactSizeIterator + '_ { + self.0.iter() + } + + /// Iterate over keys and mutable values in lexicographic key order. + pub fn iter_mut( + &mut self, + ) -> impl DoubleEndedIterator<Item = (&Cow<'static, str>, &mut MetadataValue)> + ExactSizeIterator + '_ { + self.0.iter_mut() + } +} + +impl std::ops::Index<&str> for Metadata { + type Output = MetadataValue; + + fn index(&self, key: &str) -> &Self::Output { + &self.0[key] + } +} + +impl FromIterator<(Cow<'static, str>, MetadataValue)> for Metadata { + fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<const N: usize> From<[(Cow<'static, str>, MetadataValue); N]> for Metadata { + fn from(values: [(Cow<'static, str>, MetadataValue); N]) -> Self { + values.into_iter().collect() + } +} + +impl Extend<(Cow<'static, str>, MetadataValue)> for Metadata { + fn extend<T: IntoIterator<Item = (Cow<'static, str>, MetadataValue)>>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl fmt::Debug for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_map(); + for (key, value) in self.iter() { + debug.entry(&DebugKey(key), value); + } + debug.finish() + } +} + +struct DebugKey<'a>(&'a str); + +impl fmt::Debug for DebugKey<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.0.is_empty() + && self + .0 + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) + { + f.write_str(self.0) + } else { + fmt::Debug::fmt(self.0, f) + } + } +} /// A diagnostic message with an optional semantic class and named diagnostic values. /// @@ -334,7 +455,7 @@ impl Message { /// Add `value` under `key`, replacing any previous value in this context. /// Inspect values through [`crate::Error::metadata()`], or [`crate::Exn::metadata()`] on typed exceptions. pub fn with(mut self, key: impl Into<Cow<'static, str>>, value: impl Into<MetadataValue>) -> Self { - self.values.insert(key.into(), value.into()); + self.values.insert(key, value); self } } @@ -356,8 +477,8 @@ impl fmt::Debug for Message { impl fmt::Display for Message { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message)?; - for (key, value) in &self.values { - write!(f, ", {key:?}={value}")?; + for (key, value) in self.values.iter() { + write!(f, ", {:?}={value}", DebugKey(key))?; } Ok(()) } diff --git a/gix-error/tests/error/classification.rs b/gix-error/tests/error/classification.rs index f5c37a62564..1307e5fa799 100644 --- a/gix-error/tests/error/classification.rs +++ b/gix-error/tests/error/classification.rs @@ -1432,7 +1432,7 @@ fn markers_remain_transparent_alongside_classified_errors() { "classifications remain ordered and are not deduplicated" ); insta::assert_debug_snapshot!(err, "markers supply classifications without becoming diagnostic errors", @r#" - context with input, "input"="bad" + context with input, input="bad" Caused by: 0: specific diagnostic @@ -1462,20 +1462,20 @@ fn markers_remain_transparent_alongside_classified_errors() { ); insta::assert_debug_snapshot!(err.iter_errors().map(ToString::to_string).collect::<Vec<_>>(), "only real diagnostics remain visible in diagnostic iteration", @r#" [ - "context with input, \"input\"=\"bad\"", + "context with input, input=\"bad\"", "specific diagnostic", ] "#); #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] insta::assert_debug_snapshot!(err, @r#" - context with input, "input"="bad" + context with input, input="bad" Caused by: 0: specific diagnostic "#); #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] insta::assert_debug_snapshot!(err, "chain Debug retains the classified message's concrete type", @r#" - context with input, "input"="bad" + context with input, input="bad" Caused by: 0: specific diagnostic diff --git a/gix-error/tests/error/convert.rs b/gix-error/tests/error/convert.rs index 33dbcfe150c..fa8afd7aff1 100644 --- a/gix-error/tests/error/convert.rs +++ b/gix-error/tests/error/convert.rs @@ -81,7 +81,7 @@ fn bail_with_public_context_preserves_diagnostics() { assert!( error .to_string() - .starts_with(r#"could not read configuration, "path"="config""#), + .starts_with(r#"could not read configuration, path="config""#), "the new context remains the top-level diagnostic in either reporting mode" ); assert!(error.is_not_found(), "the I/O classification is retained"); diff --git a/gix-error/tests/error/error.rs b/gix-error/tests/error/error.rs index 4bead6cc8c8..f51ada66b03 100644 --- a/gix-error/tests/error/error.rs +++ b/gix-error/tests/error/error.rs @@ -394,16 +394,16 @@ fn raising_a_converted_error_preserves_stored_types() { #[test] fn validation_error_displays_input_with_debug_formatting() { let err = validation("invalid input").with_input(b"hello\n ".as_slice()); - insta::assert_debug_snapshot!(format_args!("{}", err), "it won't hide whitespace and other special characters", @r#"invalid input, "input"="hello\n ""#); + insta::assert_debug_snapshot!(format_args!("{}", err), "it won't hide whitespace and other special characters", @r#"invalid input, input="hello\n ""#); let err = Error::from_error(err); #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "validation error displays input with debug formatting", @r#"invalid input, "input"="hello\n ""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "validation error displays input with debug formatting", @r#"invalid input, input="hello\n ""#); #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "validation error displays input with debug formatting", @r#" Message { message: "invalid input", class: Validation, - values: {"input": Bytes("hello\n ")}, + values: {input: Bytes("hello\n ")}, } "#); assert!(err.is_validation()); diff --git a/gix-error/tests/error/exn.rs b/gix-error/tests/error/exn.rs index 83c1e911b55..bdcac5911d5 100644 --- a/gix-error/tests/error/exn.rs +++ b/gix-error/tests/error/exn.rs @@ -1135,11 +1135,11 @@ fn io_payload_direct_validation_retains_metadata_and_types() { let human = r#"I/O error (InvalidData) Caused by: - 0: invalid input, "input"="ref\xff""#; + 0: invalid input, input="ref\xff""#; let typed = r#"I/O error (InvalidData) Caused by: - 0: Message { message: "invalid input", class: Validation, values: {"input": Bytes("ref\xff")} }"#; + 0: Message { message: "invalid input", class: Validation, values: {input: Bytes("ref\xff")} }"#; let compact = format!("{err:?}"); let location = err.frame().location(); let at = format!(", at {}:{}", location.file(), location.line()); @@ -1149,7 +1149,7 @@ Caused by: r#"I/O error (InvalidData){at} Caused by: - 0: invalid input, "input"="ref\xff"{at}"# + 0: invalid input, input="ref\xff"{at}"# ), "a native payload inherits its owning frame's rendering location" ); diff --git a/gix-error/tests/error/metadata.rs b/gix-error/tests/error/metadata.rs index 11299c0b617..26f62c66bde 100644 --- a/gix-error/tests/error/metadata.rs +++ b/gix-error/tests/error/metadata.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap, path::Path}; +use std::{borrow::Cow, path::Path}; use gix_error::{ Class, Error, ErrorExt, Message, MetadataValue, ResourceExhaustionKind, ResultExt, classify, corruption, not_found, @@ -142,7 +142,7 @@ fn message_debug_keeps_class_and_values_compact() { assert_eq!( format!("{message:?}"), format!( - r#"Message {{ message: "details", class: {expected_class}, values: {{"input": Bytes("bad\xff"), "offset": U64(42)}} }}"# + r#"Message {{ message: "details", class: {expected_class}, values: {{input: Bytes("bad\xff"), offset: U64(42)}} }}"# ), "compact debug omits Some and preserves ordered, typed values" ); @@ -152,7 +152,7 @@ fn message_debug_keeps_class_and_values_compact() { r#"Message {{ message: "details", class: {expected_class}, - values: {{"input": Bytes("bad\xff"), "offset": U64(42)}}, + values: {{input: Bytes("bad\xff"), offset: U64(42)}}, }}"# ), "pretty debug keeps both the class and the entire values map on single lines" @@ -180,10 +180,10 @@ fn message_debug_omits_absent_class_and_empty_values() { ), ( Message::new("details").with("offset", 42_u64), - r#"Message { message: "details", values: {"offset": U64(42)} }"#, + r#"Message { message: "details", values: {offset: U64(42)} }"#, r#"Message { message: "details", - values: {"offset": U64(42)}, + values: {offset: U64(42)}, }"#, ), ] { @@ -257,7 +257,7 @@ fn scalar_values_are_lossless_and_keys_are_local_to_a_context() { insta::assert_debug_snapshot!(format_args!("{}", Message::new("details") .with("z", "line\nbreak") .with("a", 2) - ), "keys are ordered and text is escaped", @r#"details, "a"=2, "z"="line\nbreak""#); + ), "keys are ordered and text is escaped", @r#"details, a=2, z="line\nbreak""#); #[cfg(unix)] { use std::os::unix::ffi::OsStringExt; @@ -279,11 +279,11 @@ fn scalar_values_are_lossless_and_keys_are_local_to_a_context() { ); } - insta::assert_snapshot!(metadata, "validate Display", @r#"details, "bytes"="ref\xff", "flag"=true, "float"=1.5, "path"="objects", "signed"=-9223372036854775808, "text"="line\nbreak", "unsigned"=18446744073709551615"#); + insta::assert_snapshot!(metadata, "validate Display", @r#"details, bytes="ref\xff", flag=true, float=1.5, path="objects", signed=-9223372036854775808, text="line\nbreak", unsigned=18446744073709551615"#); insta::assert_debug_snapshot!(metadata, "validate Display", @r#" Message { message: "details", - values: {"bytes": Bytes("ref\xff"), "flag": Bool(true), "float": F64(1.5), "path": Path("objects"), "signed": I64(-9223372036854775808), "text": String("line\nbreak"), "unsigned": U64(18446744073709551615)}, + values: {bytes: Bytes("ref\xff"), flag: Bool(true), float: F64(1.5), path: Path("objects"), signed: I64(-9223372036854775808), text: String("line\nbreak"), unsigned: U64(18446744073709551615)}, } "#); } @@ -373,7 +373,7 @@ fn merged_metadata_visits_native_sources_nested_errors_and_sibling_causes() { let error = std::io::Error::other(error) .and_raise_typed(Message::new("read").with_input("less specific").with("key", "example")); let mut expected = expected; - expected.insert("key".into(), MetadataValue::from("example")); + expected.insert("key", "example"); assert_eq!( error.metadata_merged(), expected, @@ -416,7 +416,7 @@ fn metadata_contexts_preserve_causes_and_remain_separate_through_conversion() { assert!(err.is_not_found() && err.can_retry()); #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] - insta::assert_snapshot!(format!("{:#}", err.error()), "the stored error's alternate display omits locations", @r#"custom: lookup, "path"="first": missing"#); + insta::assert_snapshot!(format!("{:#}", err.error()), "the stored error's alternate display omits locations", @r#"custom: lookup, path="first": missing"#); #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] insta::assert_snapshot!(format!("{:#}", err.error()), "the stored error's alternate display omits locations", @r#" custom @@ -425,9 +425,9 @@ fn metadata_contexts_preserve_causes_and_remain_separate_through_conversion() { custom Caused by: - 0: lookup, "path"="first" + 0: lookup, path="first" 1: missing - 2: read, "path"="second" + 2: read, path="second" └─0: timed out "#); @@ -437,9 +437,9 @@ fn metadata_contexts_preserve_causes_and_remain_separate_through_conversion() { custom Caused by: - 0: read, "path"="second" + 0: read, path="second" 1: timed out - 2: lookup, "path"="first" + 2: lookup, path="first" 3: missing "#); } else { @@ -447,9 +447,9 @@ fn metadata_contexts_preserve_causes_and_remain_separate_through_conversion() { custom Caused by: - 0: lookup, "path"="first" + 0: lookup, path="first" 1: missing - 2: read, "path"="second" + 2: read, path="second" └─0: timed out "#); } @@ -482,7 +482,7 @@ fn classified_context_preserves_the_real_callee() { let error = std::io::Error::from(std::io::ErrorKind::PermissionDenied) .and_raise_typed(gix_error::retryable("try reading again").with("path", Path::new("HEAD"))); insta::assert_debug_snapshot!(error, "the message context supplies explicit retryability", @r#" - try reading again, "path"="HEAD" + try reading again, path="HEAD" Caused by: 0: permission denied @@ -529,14 +529,14 @@ fn classified_context_preserves_the_real_callee() { ); if cfg!(all(feature = "auto-chain-error", not(feature = "tree-error"))) { insta::assert_debug_snapshot!(error, "conversion retains the real cause", @r#" - try reading again, "path"="HEAD" + try reading again, path="HEAD" Caused by: 0: permission denied "#); } else { insta::assert_debug_snapshot!(error, "conversion retains the real cause", @r#" - try reading again, "path"="HEAD" + try reading again, path="HEAD" Caused by: 0: permission denied @@ -575,8 +575,8 @@ fn message_classifications_survive_markers_native_sources_and_nested_branches() 0: native 1: I/O error (Other) 2: lookup - 3: first, "path"="a" - 4: second, "path"="b" + 3: first, path="a" + 4: second, path="b" "#); assert!( error.is_not_found() && error.is_retryable(), @@ -602,8 +602,8 @@ fn message_classifications_survive_markers_native_sources_and_nested_branches() 0: native 1: I/O error (Other) 2: lookup - 3: first, "path"="a" - 4: second, "path"="b" + 3: first, path="a" + 4: second, path="b" "#); } else { insta::assert_debug_snapshot!(error, "classification markers remain transparent", @r#" @@ -613,8 +613,8 @@ fn message_classifications_survive_markers_native_sources_and_nested_branches() 0: native 1: I/O error (Other) 2: lookup - 3: first, "path"="a" - 4: second, "path"="b" + 3: first, path="a" + 4: second, path="b" "#); } assert!( @@ -651,7 +651,7 @@ fn messages_are_visible_in_reports_unlike_markers() { lookup failed Caused by: - 0: missing reference, "path"="HEAD" + 0: missing reference, path="HEAD" "#); assert!( error.probable_cause().is::<Message>(), @@ -728,7 +728,7 @@ fn message_builders_preserve_messages_and_replace_the_class() { Message { message: "details", class: Corruption, - values: {"input": Bytes("bad\xff")}, + values: {input: Bytes("bad\xff")}, } "#); assert_eq!( @@ -766,7 +766,7 @@ fn message_builders_preserve_messages_and_replace_the_class() { Message { message: "object 42 is missing", class: NotFound, - values: {"object": I64(42)}, + values: {object: I64(42)}, } "#); assert_eq!( @@ -1020,20 +1020,20 @@ fn class_constructors_create_visible_diagnostics_without_synthetic_sources() { } insta::assert_debug_snapshot!(diagnostics, "class constructors create visible diagnostics without synthetic sources", @r#" [ - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", - details, "path"="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", + details, path="HEAD", ] "#); } @@ -1054,7 +1054,7 @@ fn offending_input_does_not_require_a_validation_class_or_an_extra_cause() { MetadataValue::Bytes(input.into()), "type erasure preserves the original bytes" ); - insta::assert_debug_snapshot!(error, "the diagnostic remains the cause", @r#"Malformed reference, "input"="ref: invalid\xff\n""#); + insta::assert_debug_snapshot!(error, "the diagnostic remains the cause", @r#"Malformed reference, input="ref: invalid\xff\n""#); assert!( error.probable_cause().is::<Message>(), "the diagnostic remains the cause" @@ -1062,25 +1062,23 @@ fn offending_input_does_not_require_a_validation_class_or_an_extra_cause() { } #[test] -fn metadata_alias_matches_fields_and_borrowed_iterators() { +fn metadata_wrapper_matches_fields_and_borrowed_iterators() { use gix_error::Metadata; - let values: BTreeMap<Cow<'static, str>, MetadataValue> = - BTreeMap::from([(Cow::Borrowed("path"), MetadataValue::from("HEAD"))]); - let values: Metadata = values; + let values = Metadata::from([(Cow::Borrowed("path"), MetadataValue::from("HEAD"))]); let context = Message { values, ..Message::new("context") }; - let values: &BTreeMap<Cow<'static, str>, MetadataValue> = &context.values; + let values: &Metadata = &context.values; assert_eq!( values["path"], MetadataValue::from("HEAD"), - "the public alias and values field are compatible with the underlying map" + "the public wrapper provides access to the values field" ); let exception = context.raise_typed(); - insta::assert_debug_snapshot!(exception, "metadata inspection borrows the values displayed with the context", @r#"context, "path"="HEAD""#); + insta::assert_debug_snapshot!(exception, "metadata inspection borrows the values displayed with the context", @r#"context, path="HEAD""#); let dictionary: &Metadata = exception .metadata() .next() @@ -1103,3 +1101,122 @@ fn metadata_alias_matches_fields_and_borrowed_iterators() { "Error::metadata() borrows the converted context's actual values field" ); } + +#[test] +fn metadata_keys_are_quoted_only_when_needed() { + for (key, formatted) in [ + ("input", "input"), + ("exit_status", "exit_status"), + ("core.bare", "core.bare"), + ("object-id", "object-id"), + ("42", "42"), + ("", r#""""#), + ("two words", r#""two words""#), + ("line\nbreak", r#""line\nbreak""#), + ("tab\tkey", r#""tab\tkey""#), + ("null\0key", r#""null\0key""#), + ("key\"", r#""key\"""#), + ("key\\", r#""key\\""#), + ("key=value", r#""key=value""#), + ("key:value", r#""key:value""#), + ("key,other", r#""key,other""#), + ("{key}", r#""{key}""#), + ("[key]", r#""[key]""#), + ("clĂŠ", r#""clĂŠ""#), + ("key\u{200d}", r#""key\u{200d}""#), + ] { + let message = Message::new("details").with(key, "line\nbreak"); + assert_eq!( + message.to_string(), + format!(r#"details, {formatted}="line\nbreak""#), + "display avoids unnecessary quotes without making unusual keys ambiguous" + ); + assert_eq!( + format!("{message:?}"), + format!(r#"Message {{ message: "details", values: {{{formatted}: String("line\nbreak")}} }}"#), + "message debug uses the same key quoting while retaining value types and escapes" + ); + assert_eq!( + format!("{:?}", message.values), + format!(r#"{{{formatted}: String("line\nbreak")}}"#), + "direct dictionary debug omits the implementation wrapper" + ); + assert_eq!( + format!("{:#?}", message.values), + format!("{{\n {formatted}: String(\"line\\nbreak\"),\n}}"), + "pretty dictionary debug uses the same quoting and keeps values compact" + ); + } +} + +#[test] +fn metadata_provides_basic_dictionary_access_without_exposing_storage() { + use gix_error::Metadata; + + let mut values = Metadata::new(); + assert_eq!( + values, + Metadata::default(), + "new and default create equal empty dictionaries" + ); + assert!(values.is_empty(), "new dictionaries have no values"); + assert_eq!(values.len(), 0, "new dictionaries have zero entries"); + assert_eq!(format!("{values:?}"), "{}", "empty debug output is an empty dictionary"); + assert_eq!(values.get("missing"), None, "absent keys have no value"); + assert_eq!(values.get_mut("missing"), None, "absent keys have no mutable value"); + assert!(!values.contains_key("missing"), "absent keys are not contained"); + assert_eq!(values.remove("missing"), None, "removing an absent key has no effect"); + assert_eq!( + values.insert(String::from("z"), 1_u64), + None, + "insertion accepts owned keys and scalar values without explicit conversion" + ); + assert_eq!( + values.insert("z", 2_u64), + Some(1_u64.into()), + "insertion accepts borrowed static keys and returns the replaced value" + ); + assert!(values.contains_key("z"), "borrowed lookup finds owned keys"); + *values.get_mut("z").expect("z was inserted") = 3_u64.into(); + assert_eq!( + values.get("z"), + Some(&3_u64.into()), + "mutable access changes the stored value" + ); + values.extend([("a".into(), false.into()), ("m".into(), "text".into())]); + assert_eq!(values.len(), 3, "extension records additional values"); + assert_eq!( + values.iter().map(|(key, _)| key.as_ref()).collect::<Vec<_>>(), + ["a", "m", "z"], + "iteration follows lexicographic key order" + ); + assert_eq!( + values.iter().rev().map(|(key, _)| key.as_ref()).collect::<Vec<_>>(), + ["z", "m", "a"], + "iteration can traverse keys in reverse order" + ); + assert_eq!( + values.iter().len(), + values.len(), + "iteration exposes its exact remaining length" + ); + for (_, value) in values.iter_mut() { + *value = true.into(); + } + assert_eq!(values["a"], true.into(), "mutable iteration updates recorded values"); + let copied: Metadata = values.iter().map(|(key, value)| (key.clone(), value.clone())).collect(); + assert_eq!( + copied, + values.clone(), + "collection and cloning preserve keys and values" + ); + assert_eq!( + values.remove("m"), + Some(true.into()), + "removal returns the recorded value" + ); + assert_eq!(values.len(), 2, "removal decreases the dictionary size"); + assert_eq!(copied.len(), 3, "copied dictionaries own independent storage"); + values.clear(); + assert!(values.is_empty(), "clear removes all recorded values"); +} diff --git a/gix-error/tests/error/test.rs b/gix-error/tests/error/test.rs index 1185efb2a85..15563b2f995 100644 --- a/gix-error/tests/error/test.rs +++ b/gix-error/tests/error/test.rs @@ -103,21 +103,21 @@ fn io_payload_reports_expand_each_payload_once_in_both_backends() { r#"I/O error (InvalidData) Caused by: - 0: invalid input, "input"="ref\xff""#, + 0: invalid input, input="ref\xff""#, 2, ), (false, true) => ( r#"I/O error (InvalidData) Caused by: - 0: invalid input, "input"="ref\xff""#, + 0: invalid input, input="ref\xff""#, 1, ), (true, false) => ( r#"I/O error (InvalidData) Caused by: - 0: invalid input, "input"="ref\xff" + 0: invalid input, input="ref\xff" └─0: payload child 1: explicit sibling"#, 4, @@ -127,7 +127,7 @@ Caused by: Caused by: 0: explicit sibling - 1: invalid input, "input"="ref\xff" + 1: invalid input, input="ref\xff" 2: payload child"#, 4, ), diff --git a/gix-filter/tests/filter/driver.rs b/gix-filter/tests/filter/driver.rs index 997b3b79ab2..0a8ef903a6f 100644 --- a/gix-filter/tests/filter/driver.rs +++ b/gix-filter/tests/filter/driver.rs @@ -257,7 +257,7 @@ mod shutdown { err.classify().next().is_none(), "a subprocess exit status does not identify corruption or invalid input" ); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(env!("CARGO_BIN_EXE_gix-filter-test-arrow"), "<filter-driver>"), ("exit code:", "exit status:")]), "the failed command and status are retained", @r#"Filter process "\'<filter-driver>\' process fail-on-shutdown" failed, "exit_code"=1, "exit_status"="exit status: 1""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(env!("CARGO_BIN_EXE_gix-filter-test-arrow"), "<filter-driver>"), ("exit code:", "exit status:")]), "the failed command and status are retained", @r#"Filter process "\'<filter-driver>\' process fail-on-shutdown" failed, exit_code=1, exit_status="exit status: 1""#); Ok(()) } @@ -482,7 +482,7 @@ pub(crate) mod apply { kind: Other, error: Message { message: "Driver process \"/bin/sh\" \"-c\" \"'<filter-driver>' smudge 'do/fail'\" \"sh\" failed", - values: {"exit_code": I64(101), "exit_status": String("exit status: 101"), "program": Path("/bin/sh")}, + values: {exit_code: I64(101), exit_status: String("exit status: 101"), program: Path("/bin/sh")}, }, } "#); @@ -492,7 +492,7 @@ pub(crate) mod apply { kind: Other, error: Message { message: "Driver process \"<filter-driver>\" \"smudge\" \"do/fail\" failed", - values: {"exit_code": I64(101), "exit_status": String("exit code: 101"), "program": Path("<filter-driver>")}, + values: {exit_code: I64(101), exit_status: String("exit code: 101"), program: Path("<filter-driver>")}, }, } "#); diff --git a/gix-mailmap/tests/mailmap/parse.rs b/gix-mailmap/tests/mailmap/parse.rs index 8970f0d7ebf..a2eaba17807 100644 --- a/gix-mailmap/tests/mailmap/parse.rs +++ b/gix-mailmap/tests/mailmap/parse.rs @@ -8,10 +8,10 @@ fn line_numbers_are_counted_correctly_in_errors() { assert_eq!(actual.len(), 2); let err = actual.next().expect("two items left").unwrap_err(); - insta::assert_debug_snapshot!(err, "malformed mailmap lines retain their original input and one-based line number", @r#"3: Missing closing bracket '>' in email, "input"="<missing closing brace""#); + insta::assert_debug_snapshot!(err, "malformed mailmap lines retain their original input and one-based line number", @r#"3: Missing closing bracket '>' in email, input="<missing closing brace""#); let err = actual.next().expect("one item left").unwrap_err(); - insta::assert_debug_snapshot!(err, "malformed mailmap lines retain their original input and one-based line number", @r#"Line 5 does not contain an email, "input"="just a name""#); + insta::assert_debug_snapshot!(err, "malformed mailmap lines retain their original input and one-based line number", @r#"Line 5 does not contain an email, input="just a name""#); } #[test] @@ -121,28 +121,28 @@ fn the_second_email_may_be_empty() { #[test] fn error_if_there_is_just_a_name() { let err = try_line("just a name").unwrap_err(); - insta::assert_debug_snapshot!(err, "a name without an email reports the incomplete identity on line one", @r#"Line 1 does not contain an email, "input"="just a name""#); + insta::assert_debug_snapshot!(err, "a name without an email reports the incomplete identity on line one", @r#"Line 1 does not contain an email, input="just a name""#); } #[test] fn error_if_there_is_just_an_email() { let err = try_line("<email>").unwrap_err(); - insta::assert_debug_snapshot!(err, "an email needs a name or email to map to", @r#"1: Emails without a name or email to map to are invalid, "input"="<email>""#); + insta::assert_debug_snapshot!(err, "an email needs a name or email to map to", @r#"1: Emails without a name or email to map to are invalid, input="<email>""#); let err = try_line(" \t <email>").unwrap_err(); - insta::assert_debug_snapshot!(err, "an email needs a name or email to map to", @r#"1: Emails without a name or email to map to are invalid, "input"="<email>""#); + insta::assert_debug_snapshot!(err, "an email needs a name or email to map to", @r#"1: Emails without a name or email to map to are invalid, input="<email>""#); } #[test] fn error_if_email_is_empty() { let err = try_line("hello <").unwrap_err(); - insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Missing closing bracket '>' in email, "input"="hello <""#); + insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Missing closing bracket '>' in email, input="hello <""#); let err = try_line("hello < \t").unwrap_err(); - insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Missing closing bracket '>' in email, "input"="hello <""#); + insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Missing closing bracket '>' in email, input="hello <""#); let err = try_line("hello < \t\r >").unwrap_err(); - insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Email must not be empty, "input"="hello < \t\r >""#); + insta::assert_debug_snapshot!(err, "an incomplete or empty email identifies the malformed input on line one", @r#"1: Email must not be empty, input="hello < \t\r >""#); } fn line(input: &str) -> Entry<'_> { diff --git a/gix-object/tests/object/encode.rs b/gix-object/tests/object/encode.rs index 586bb48edba..74fb518f51a 100644 --- a/gix-object/tests/object/encode.rs +++ b/gix-object/tests/object/encode.rs @@ -139,7 +139,7 @@ mod tree { error: Message { message: "Nullbytes are invalid in file paths as they are separators", class: Validation, - values: {"input": Bytes("hi\0ho")}, + values: {input: Bytes("hi\0ho")}, }, } "#); diff --git a/gix-object/tests/object/tree/editor.rs b/gix-object/tests/object/tree/editor.rs index 212dba067e1..41128118e25 100644 --- a/gix-object/tests/object/tree/editor.rs +++ b/gix-object/tests/object/tree/editor.rs @@ -524,7 +524,7 @@ fn from_empty_invalid_write() -> Result { error: Message { message: "Nullbytes are invalid in file paths as they are separators", class: Validation, - values: {"input": Bytes("with\0null")}, + values: {input: Bytes("with\0null")}, }, } "#); diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index 6a245f1432f..401392386d6 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -94,25 +94,25 @@ fn write_failures_preserve_custom_sources_and_metadata() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "write failures preserve custom sources and metadata", @r#" [ - Could not stream loose object data, "path"="<objects>" + Could not stream loose object data, path="<objects>" Caused by: 0: I/O error (Other) 1: custom reader failed 2: temporarily missing input, - Could not stream loose object data, "path"="<objects>" + Could not stream loose object data, path="<objects>" Caused by: 0: I/O error (Other) 1: custom reader failed 2: temporarily missing input, - Could not stream loose object data, "path"="<objects>" + Could not stream loose object data, path="<objects>" Caused by: 0: I/O error (Other) 1: custom reader failed 2: temporarily missing input, - Could not stream loose object data, "path"="<objects>" + Could not stream loose object data, path="<objects>" Caused by: 0: I/O error (Other) @@ -310,38 +310,38 @@ fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "delta lookup distinguishes missing bases from recursion limits", @r#" [ - Could not resolve delta base object: delta base object is missing, "base_id"="Oid(1)", "object_id"="Oid(2)", - Could not resolve delta base object: delta base object is missing, "base_id"="Oid(1)", "object_id"="Oid(2)", - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + Could not resolve delta base object: delta base object is missing, base_id="Oid(1)", object_id="Oid(2)", + Could not resolve delta base object: delta base object is missing, base_id="Oid(1)", object_id="Oid(2)", + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Reached recursion limit while resolving ref delta bases, "max_depth"=0, "object_id"="Oid(2)", - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + 0: Reached recursion limit while resolving ref delta bases, max_depth=0, object_id="Oid(2)", + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Reached recursion limit while resolving ref delta bases, "max_depth"=0, "object_id"="Oid(2)", - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + 0: Reached recursion limit while resolving ref delta bases, max_depth=0, object_id="Oid(2)", + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Could not read loose object, "path"="<base-object-path>" + 0: Could not read loose object, path="<base-object-path>" 1: Could not decode zip stream 2: Invalid input data, - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Could not read loose object header, "path"="<base-object-path>" + 0: Could not read loose object header, path="<base-object-path>" 1: Could not decode zip stream 2: Invalid input data, - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Could not read loose object, "path"="<base-object-path>" + 0: Could not read loose object, path="<base-object-path>" 1: Could not decode zip stream 2: Decompressing this input requires a dictionary, - Could not resolve delta base object, "base_id"="Oid(1)", "object_id"="Oid(2)" + Could not resolve delta base object, base_id="Oid(1)", object_id="Oid(2)" Caused by: - 0: Could not read loose object header, "path"="<base-object-path>" + 0: Could not read loose object header, path="<base-object-path>" 1: Could not decode zip stream 2: Decompressing this input requires a dictionary, ] @@ -1066,7 +1066,7 @@ fn disappearing_loose_objects_keep_retryable_diagnostics() -> Result { insta::assert_debug_snapshot!(diagnostics, "disappearing loose objects keep retryable diagnostics", @r#" [ Objects were deleted during iteration - try again, - Could not verify loose object database, "path"="<objects>" + Could not verify loose object database, path="<objects>" Caused by: 0: Objects were deleted during iteration - try again, diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 6b04132eb0d..18d39944e5b 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -376,10 +376,10 @@ mod find { ) .expect_err("verification must report the invalid object"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&db.object_path(&id).to_string_lossy(), "<object-path>")]), "corrupt objects do not become valid when retried", @r#" - Could not read loose object during verification, "object_id"="Oid(1)" + Could not read loose object during verification, object_id="Oid(1)" Caused by: - 0: Could not read loose object, "path"="<object-path>" + 0: Could not read loose object, path="<object-path>" 1: Empty loose object file "#); assert!(!err.can_retry(), "corrupt objects do not become valid when retried"); @@ -470,14 +470,14 @@ mod find { } insta::assert_debug_snapshot!(error_snapshots, "completed object size is validated before allocation", @r#" [ - Could not read loose object, "path"="<object-path>" + Could not read loose object, path="<object-path>" Caused by: - 0: Loose object size mismatch: invalid size of inflated loose object, "actual"=0, "expected"=1048576, - Could not read loose object, "path"="<object-path>" + 0: Loose object size mismatch: invalid size of inflated loose object, actual=0, expected=1048576, + Could not read loose object, path="<object-path>" Caused by: - 0: Loose object size mismatch: invalid size of inflated loose object, "actual"=0, "expected"=<usize::MAX>, + 0: Loose object size mismatch: invalid size of inflated loose object, actual=0, expected=<usize::MAX>, ] "#); Ok(()) @@ -622,7 +622,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB "the allocation diagnostic and lookup context are the only nodes" ); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&db.object_path(&id).to_string_lossy(), "<object-path>")]), "the allocation limit retains the lookup context and requested byte count", @r#" - Could not read loose object, "path"="<object-path>" + Could not read loose object, path="<object-path>" Caused by: 0: Cannot store loose object of 56915 bytes in memory: the object exceeds the configured allocation limit of 1 bytes @@ -732,7 +732,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB "reservation diagnostics retain the full requested object size in prose" ); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&path.to_string_lossy(), "<object-path>")]), "impossible allocations retain the failed capacity reservation", @r#" - Could not read loose object, "path"="<object-path>" + Could not read loose object, path="<object-path>" Caused by: 0: Cannot store loose object of 18446744073709551615 bytes in memory diff --git a/gix-path/tests/path/relative_path.rs b/gix-path/tests/path/relative_path.rs index b5b9da5f4fd..be7b6e90c86 100644 --- a/gix-path/tests/path/relative_path.rs +++ b/gix-path/tests/path/relative_path.rs @@ -49,25 +49,25 @@ fn dots_in_paths_return_err() { let path_bstring: BString = "./heads".into(); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_str), true), "dots in paths return err", @r#" - Relative path contains an invalid component, "input"="." + Relative path contains an invalid component, input="." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_bstr), true), "dots in paths return err", @r#" - Relative path contains an invalid component, "input"="." + Relative path contains an invalid component, input="." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_u8), true), "dots in paths return err", @r#" - Relative path contains an invalid component, "input"="." + Relative path contains an invalid component, input="." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(&path_bstring), true), "dots in paths return err", @r#" - Relative path contains an invalid component, "input"="." + Relative path contains an invalid component, input="." Caused by: 0: Relative components '.' and '..' are disallowed @@ -90,14 +90,14 @@ fn dots_in_paths_with_backslashes_return_err() { ] { #[cfg(windows)] insta::assert_debug_snapshot!(err, "Windows treats backslashes as separators", @r#" - Relative path contains an invalid component, "input"="." + Relative path contains an invalid component, input="." Caused by: 0: Relative components '.' and '..' are disallowed "#); #[cfg(not(windows))] insta::assert_debug_snapshot!(err, "backslashes are rejected inside Unix path components", @r#" - Relative path contains an invalid component, "input"=".\\heads" + Relative path contains an invalid component, input=".\\heads" Caused by: 0: Path separators like / or \ are not allowed @@ -114,25 +114,25 @@ fn double_dots_in_paths_return_err() { let path_bstring: BString = "../heads".into(); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_str), true), "double dots in paths return err", @r#" - Relative path contains an invalid component, "input"=".." + Relative path contains an invalid component, input=".." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_bstr), true), "double dots in paths return err", @r#" - Relative path contains an invalid component, "input"=".." + Relative path contains an invalid component, input=".." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(path_u8), true), "double dots in paths return err", @r#" - Relative path contains an invalid component, "input"=".." + Relative path contains an invalid component, input=".." Caused by: 0: Relative components '.' and '..' are disallowed "#); insta::assert_debug_snapshot!(assert_validation(TryInto::<&RelativePath>::try_into(&path_bstring), true), "double dots in paths return err", @r#" - Relative path contains an invalid component, "input"=".." + Relative path contains an invalid component, input=".." Caused by: 0: Relative components '.' and '..' are disallowed @@ -155,14 +155,14 @@ fn double_dots_in_paths_with_backslashes_return_err() { ] { #[cfg(windows)] insta::assert_debug_snapshot!(err, "Windows treats backslashes as separators", @r#" - Relative path contains an invalid component, "input"=".." + Relative path contains an invalid component, input=".." Caused by: 0: Relative components '.' and '..' are disallowed "#); #[cfg(not(windows))] insta::assert_debug_snapshot!(err, "backslashes are rejected inside Unix path components", @r#" - Relative path contains an invalid component, "input"="..\\heads" + Relative path contains an invalid component, input="..\\heads" Caused by: 0: Path separators like / or \ are not allowed diff --git a/gix-pathspec/tests/defaults.rs b/gix-pathspec/tests/defaults.rs index fd3e5013483..979b64e8e1c 100644 --- a/gix-pathspec/tests/defaults.rs +++ b/gix-pathspec/tests/defaults.rs @@ -64,7 +64,7 @@ fn invalid_values_retain_the_config_validation_error() { let _env = gix_testtools::Env::new().set("GIT_GLOB_PATHSPECS", "invalid"); let err = Defaults::from_environment(&mut |name| std::env::var_os(name)) .expect_err("the environment value is not a boolean"); - insta::assert_debug_snapshot!(err, "invalid values retain the config validation error", @r#"Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid""#); + insta::assert_debug_snapshot!(err, "invalid values retain the config validation error", @r#"Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid""#); assert_eq!( err.metadata().find_map(|values| values.get("input")), Some(&gix_error::MetadataValue::from(b"invalid".as_slice())) diff --git a/gix-pathspec/tests/normalize/mod.rs b/gix-pathspec/tests/normalize/mod.rs index 5fff3de1c2e..007f921fd1b 100644 --- a/gix-pathspec/tests/normalize/mod.rs +++ b/gix-pathspec/tests/normalize/mod.rs @@ -97,13 +97,13 @@ fn absolute_top_patterns_ignore_the_prefix_but_are_made_relative() -> Result { #[test] fn relative_path_breaks_out_of_working_tree() { let err = normalized_spec("../a", "", "").unwrap_err(); - insta::assert_debug_snapshot!(err, "relative path breaks out of working tree", @r#"The path leaves the repository, "input"="../a""#); + insta::assert_debug_snapshot!(err, "relative path breaks out of working tree", @r#"The path leaves the repository, input="../a""#); assert_eq!( err.metadata().next().expect("diagnostic metadata is retained")["input"], gix_error::MetadataValue::from(b"../a".as_slice()) ); let err = normalized_spec("../../b", "a", "").unwrap_err(); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(r"a\../../b", "a/../../b")]), "relative path breaks out of working tree", @r#"The path leaves the repository, "input"="a/../../b""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(r"a\../../b", "a/../../b")]), "relative path breaks out of working tree", @r#"The path leaves the repository, input="a/../../b""#); assert_eq!( err.metadata().next().expect("diagnostic metadata is retained")["input"], gix_error::MetadataValue::from((if cfg!(windows) { r"a\../../b" } else { "a/../../b" }).as_bytes()) @@ -113,13 +113,13 @@ fn relative_path_breaks_out_of_working_tree() { #[test] fn absolute_path_breaks_out_of_working_tree() { let err = normalized_spec("/path/to/repo/..///./a", "", "/path/to/repo").unwrap_err(); - insta::assert_debug_snapshot!(err, "absolute path breaks out of working tree", @r#"The path leaves the repository, "input"="..///./a""#); + insta::assert_debug_snapshot!(err, "absolute path breaks out of working tree", @r#"The path leaves the repository, input="..///./a""#); assert_eq!( err.metadata().next().expect("diagnostic metadata is retained")["input"], gix_error::MetadataValue::from(b"..///./a".as_slice()) ); let err = normalized_spec("/path/to/repo/../../../dev", "", "/path/to/repo").unwrap_err(); - insta::assert_debug_snapshot!(err, "absolute path breaks out of working tree", @r#"The path leaves the repository, "input"="../../../dev""#); + insta::assert_debug_snapshot!(err, "absolute path breaks out of working tree", @r#"The path leaves the repository, input="../../../dev""#); assert_eq!( err.metadata().next().expect("diagnostic metadata is retained")["input"], gix_error::MetadataValue::from(b"../../../dev".as_slice()) @@ -129,7 +129,7 @@ fn absolute_path_breaks_out_of_working_tree() { #[test] fn absolute_path_escapes_worktree() { let err = normalized_spec("/dev", "", "/path/to/repo").expect_err("the path is outside of the worktree"); - insta::assert_debug_snapshot!(err, "absolute path escapes worktree", @r#"The path is not inside of the worktree "/path/to/repo", "input"="/dev""#); + insta::assert_debug_snapshot!(err, "absolute path escapes worktree", @r#"The path is not inside of the worktree "/path/to/repo", input="/dev""#); assert_eq!( err.metadata().next().expect("diagnostic metadata is retained")["input"], gix_error::MetadataValue::from(b"/dev".as_slice()) diff --git a/gix-pathspec/tests/parse/invalid.rs b/gix-pathspec/tests/parse/invalid.rs index 3b8391105a9..84d0ce3cb44 100644 --- a/gix-pathspec/tests/parse/invalid.rs +++ b/gix-pathspec/tests/parse/invalid.rs @@ -22,7 +22,7 @@ fn empty_input() { Message { message: "An empty string is not a valid pathspec", class: Validation, - values: {"input": Bytes("")}, + values: {input: Bytes("")}, } "#); assert_eq!(err.values.get("input"), Some(&MetadataValue::from(b"".as_slice()))); @@ -51,77 +51,77 @@ fn invalid_short_signatures() { Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("\"")}, + values: {input: Bytes("\"")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("#")}, + values: {input: Bytes("#")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("%")}, + values: {input: Bytes("%")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("&")}, + values: {input: Bytes("&")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("\'")}, + values: {input: Bytes("\'")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes(",")}, + values: {input: Bytes(",")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("-")}, + values: {input: Bytes("-")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes(";")}, + values: {input: Bytes(";")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("<")}, + values: {input: Bytes("<")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("=")}, + values: {input: Bytes("=")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes(">")}, + values: {input: Bytes(">")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("@")}, + values: {input: Bytes("@")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("_")}, + values: {input: Bytes("_")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("`")}, + values: {input: Bytes("`")}, }, Message { message: "Unimplemented short keyword", class: Validation, - values: {"input": Bytes("~")}, + values: {input: Bytes("~")}, }, ] "##); @@ -152,22 +152,22 @@ fn invalid_keywords() { Message { message: "Found invalid keyword in pathspec signature", class: Validation, - values: {"input": Bytes(" ")}, + values: {input: Bytes(" ")}, }, Message { message: "Found invalid keyword in pathspec signature", class: Validation, - values: {"input": Bytes("tp")}, + values: {input: Bytes("tp")}, }, Message { message: "Found invalid keyword in pathspec signature", class: Validation, - values: {"input": Bytes(" exclude")}, + values: {input: Bytes(" exclude")}, }, Message { message: "Found invalid keyword in pathspec signature", class: Validation, - values: {"input": Bytes("icse")}, + values: {input: Bytes("icse")}, }, ] "#); @@ -205,42 +205,42 @@ fn invalid_attributes() { Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("+invalidAttr")}, + values: {input: Bytes("+invalidAttr")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("+invalidAttr")}, + values: {input: Bytes("+invalidAttr")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("+invalidAttr")}, + values: {input: Bytes("+invalidAttr")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("inva\\lid")}, + values: {input: Bytes("inva\\lid")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("a\tb")}, + values: {input: Bytes("a\tb")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("a\rb")}, + values: {input: Bytes("a\rb")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("a=b")}, + values: {input: Bytes("a=b")}, }, Message { message: "Attribute has non-ascii characters or starts with '-'", class: Validation, - values: {"input": Bytes("a=b")}, + values: {input: Bytes("a=b")}, }, ] "#); @@ -259,7 +259,7 @@ fn attribute_values_are_not_split_on_non_space_blanks() { Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("\t")}, + values: {input: Bytes("\t")}, } "#); assert_eq!(err.values.get("input"), Some(&MetadataValue::from(b"\t".as_slice()))); @@ -294,42 +294,42 @@ fn invalid_attribute_values() { Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("#")}, + values: {input: Bytes("#")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("\\")}, + values: {input: Bytes("\\")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("\\")}, + values: {input: Bytes("\\")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("#")}, + values: {input: Bytes("#")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("=")}, + values: {input: Bytes("=")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("#")}, + values: {input: Bytes("#")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("\xef")}, + values: {input: Bytes("\xef")}, }, Message { message: "Invalid character in attribute value", class: Validation, - values: {"input": Bytes("\xef")}, + values: {input: Bytes("\xef")}, }, ] "##); @@ -362,17 +362,17 @@ fn escape_character_at_end_of_attribute_value() { Message { message: "Escape character '\\' is not allowed as the last character in an attribute value", class: Validation, - values: {"input": Bytes("invalid\\")}, + values: {input: Bytes("invalid\\")}, }, Message { message: "Escape character '\\' is not allowed as the last character in an attribute value", class: Validation, - values: {"input": Bytes("invalid\\")}, + values: {input: Bytes("invalid\\")}, }, Message { message: "Escape character '\\' is not allowed as the last character in an attribute value", class: Validation, - values: {"input": Bytes("invalid\\")}, + values: {input: Bytes("invalid\\")}, }, ] "#); @@ -410,7 +410,7 @@ fn multiple_attribute_specifications() { Message { message: "Only one attribute specification is allowed in the same pathspec", class: Validation, - values: {"input": Bytes("attr:two")}, + values: {input: Bytes("attr:two")}, } "#); assert!( @@ -433,7 +433,7 @@ fn missing_parentheses() { Message { message: "Missing ')' at the end of pathspec signature", class: Validation, - values: {"input": Bytes(":(top")}, + values: {input: Bytes(":(top")}, } "#); assert_eq!(err.values.get("input"), Some(&MetadataValue::from(input.as_bytes()))); @@ -453,7 +453,7 @@ fn glob_and_literal_keywords_present() { Message { message: "'literal' and 'glob' keywords cannot be used together in the same pathspec", class: Validation, - values: {"input": Bytes("literal")}, + values: {input: Bytes("literal")}, } "#); assert_eq!( diff --git a/gix-protocol/tests/protocol/fetch/ref_map.rs b/gix-protocol/tests/protocol/fetch/ref_map.rs index a02753be7c7..c6e405dce3e 100644 --- a/gix-protocol/tests/protocol/fetch/ref_map.rs +++ b/gix-protocol/tests/protocol/fetch/ref_map.rs @@ -30,7 +30,7 @@ mod from_refs { fn unknown_object_format_errors() { let caps = caps_with(b"symref=HEAD:refs/heads/main object-format=sha999 agent=git/2.54.0"); let err = RefMap::from_refs(Vec::new(), &caps, ctx()).expect_err("unknown format must error"); - insta::assert_debug_snapshot!(err, "unknown formats are unsupported", @r#"The object format used by the remote is unsupported: sha999, "input"="sha999""#); + insta::assert_debug_snapshot!(err, "unknown formats are unsupported", @r#"The object format used by the remote is unsupported: sha999, input="sha999""#); assert!(err.is_unsupported(), "unknown formats require another implementation"); let metadata = err.metadata().next().expect("the unsupported format is retained"); assert_eq!( diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index fa0f6e2eac9..e2ff3a5a588 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -202,7 +202,7 @@ fn non_empty_reflog_directory_preserves_open_error_context() -> Result { ) .expect_err("a non-empty directory cannot be replaced with a reflog"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "the conflicting directory exists", @r#" - Could not open reflog for appending, "path"="<git-dir>/logs/refs/heads/main" + Could not open reflog for appending, path="<git-dir>/logs/refs/heads/main" Caused by: 0: I/O error (Other) diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 414e0f8cc58..27efdea57a2 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -195,8 +195,8 @@ impl Transaction<'_, '_> { /// Rollbacks happen automatically on failure and they tend to be perfect. /// This method is idempotent. /// - /// Failed edits identify the requested and resolved names in [metadata](gix_error::Error::metadata()) `reference` and - /// `referent` (bytes). + /// Failed edits identify the requested name in [metadata](gix_error::Error::metadata()) `reference` (bytes). + /// `referent` records the resolved name only when it differs from the requested name. /// [`ReferenceOutOfDate`] and [`MustNotExist`] retain the actual target observed while holding the lock. pub fn prepare( self, @@ -358,11 +358,12 @@ impl Transaction<'_, '_> { ref_name = parent.name(); cursor = parent.parent_index; } - return Err(err.and_raise( - Message::new("Could not prepare reference edit") - .with("reference", ref_name) - .with("referent", referent), - )); + let differs = ref_name != referent; + let mut context = Message::new("Could not prepare reference edit").with("reference", ref_name); + if differs { + context = context.with("referent", referent); + } + return Err(err.and_raise(context)); } // traverse parent chain from leaf/peeled ref and set the leaf previous oid accordingly diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs index e3c23e28f9a..3a16b8a6123 100644 --- a/gix-ref/tests/refs/error.rs +++ b/gix-ref/tests/refs/error.rs @@ -122,8 +122,8 @@ fn peeling_missing_targets_is_classified() -> Result { [ The ref partially named "refs/heads/missing" could not be found, The ref partially named "refs/heads/missing" could not be found, - Could not peel reference to an object: object could not be found, "object_id"="Oid(1)", "reference"="refs/heads/main", - Could not peel packed reference: object could not be found, "object_id"="Oid(1)", "reference"="refs/tags/missing", + Could not peel reference to an object: object could not be found, object_id="Oid(1)", reference="refs/heads/main", + Could not peel packed reference: object could not be found, object_id="Oid(1)", reference="refs/tags/missing", ] "#); Ok(()) @@ -160,8 +160,8 @@ fn peeling_missing_objects_has_one_classified_diagnostic() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "peeling missing objects has one classified diagnostic", @r#" [ - Could not peel reference to an object: object could not be found, "object_id"="Oid(1)", "reference"="refs/tags/tag", - Could not peel packed reference: object could not be found, "object_id"="Oid(1)", "reference"="refs/tags/tag", + Could not peel reference to an object: object could not be found, object_id="Oid(1)", reference="refs/tags/tag", + Could not peel packed reference: object could not be found, object_id="Oid(1)", reference="refs/tags/tag", ] "#); Ok(()) @@ -223,22 +223,22 @@ fn object_lookup_failures_retain_their_causes() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "object lookup failures retain their causes", @r#" [ - Could not peel reference to an object, "object_id"="Oid(1)", "reference"="refs/tags/tag" + Could not peel reference to an object, object_id="Oid(1)", reference="refs/tags/tag" Caused by: 0: I/O error (PermissionDenied) 1: object database unavailable, - Could not peel packed reference, "object_id"="Oid(1)", "reference"="refs/tags/tag" + Could not peel packed reference, object_id="Oid(1)", reference="refs/tags/tag" Caused by: 0: I/O error (PermissionDenied) 1: object database unavailable, - Could not peel reference to an object, "object_id"="Oid(1)", "reference"="refs/tags/tag" + Could not peel reference to an object, object_id="Oid(1)", reference="refs/tags/tag" Caused by: 0: I/O error (TimedOut) 1: object database unavailable, - Could not peel packed reference, "object_id"="Oid(1)", "reference"="refs/tags/tag" + Could not peel packed reference, object_id="Oid(1)", reference="refs/tags/tag" Caused by: 0: I/O error (TimedOut) @@ -380,29 +380,29 @@ fn malformed_reference_data_is_classified() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "malformed reference data is classified", @r#" [ - Reference content could not be parsed, "input"="invalid", + Reference content could not be parsed, input="invalid", The header could not be parsed, even though first line started with '#', - Could not decode packed reference, "name"="refs/main" + Could not decode packed reference, name="refs/main" Caused by: 0: Malformed packed reference record, - Invalid packed reference, "input"="bogus refs/heads/main", "line"=1 + Invalid packed reference, input="bogus refs/heads/main", line=1 Caused by: 0: Malformed packed reference, - Invalid packed reference, "input"="bogus refs/heads/main", "line"=1 + Invalid packed reference, input="bogus refs/heads/main", line=1 Caused by: 0: Malformed packed reference, - Aborting symbolic reference cycle, "path"="<git-dir>/refs/loop-a", - Could not decode reflog line, "input"="invalid" + Aborting symbolic reference cycle, path="<git-dir>/refs/loop-a", + Could not decode reflog line, input="invalid" Caused by: 0: Malformed reflog line, - Invalid reflog entry, "from_end"=false, "line"=1 + Invalid reflog entry, from_end=false, line=1 Caused by: - 0: Could not decode reflog line, "input"="invalid" + 0: Could not decode reflog line, input="invalid" 1: Malformed reflog line, ] "#); @@ -434,11 +434,11 @@ fn missing_transaction_targets_are_classified() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "missing transaction targets are classified", @r#" [ - Could not prepare reference edit, "reference"="refs/heads/missing", "referent"="refs/heads/missing" + Could not prepare reference edit, reference="refs/heads/missing" Caused by: 0: The reference to delete must exist, - Could not prepare reference edit, "reference"="refs/heads/missing", "referent"="refs/heads/missing" + Could not prepare reference edit, reference="refs/heads/missing" Caused by: 0: The reference to update must exist, @@ -495,7 +495,7 @@ fn invalid_reflog_input_is_classified() -> Result { kind: Other, error: Messages must not contain newlines (\n), }, - Could not update reflog, "reference"="refs/heads/new" + Could not update reflog, reference="refs/heads/new" Caused by: 0: reflog messages need a committer which isn't set, @@ -517,7 +517,7 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> Result .expect("one packed ref") .expect_err("the name is invalid"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "malformed packed names and reflog signatures retain parser errors", @r#" - Invalid packed reference, "input"="Oid(1) refs/heads/bad..name", "line"=1 + Invalid packed reference, input="Oid(1) refs/heads/bad..name", line=1 Caused by: 0: Malformed packed reference @@ -532,7 +532,7 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> Result let line = format!("{0} {0} invalid signature\tmessage", hash.null()); let err = gix_ref::file::log::LineRef::from_bytes(line.as_bytes()).expect_err("the signature is invalid"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "malformed packed names and reflog signatures retain parser errors", @r#" - Could not decode reflog line, "input"="Oid(1) Oid(1) invalid signature\tmessage" + Could not decode reflog line, input="Oid(1) Oid(1) invalid signature\tmessage" Caused by: 0: Invalid reflog signature @@ -628,7 +628,7 @@ fn a_depth_limit_does_not_imply_corruption() -> Result { .find("r0")? .follow_to_object_packed(&store, None) .expect_err("the valid symbolic chain exceeds the depth limit"); - insta::assert_debug_snapshot!(err, "a valid symbolic chain need not be corrupted", @r#"Symbolic reference depth limit exceeded, "max_depth"=5"#); + insta::assert_debug_snapshot!(err, "a valid symbolic chain need not be corrupted", @r#"Symbolic reference depth limit exceeded, max_depth=5"#); assert!(!err.is_corrupted(), "a valid symbolic chain need not be corrupted"); assert!(!err.is_not_found(), "all symbolic targets exist"); let details = err.metadata().next().expect("limit details"); @@ -655,7 +655,7 @@ fn loose_reference_diagnostics_keep_input_with_the_failure() -> Result { let contents = b"invalid\xff"; let err = gix_ref::file::loose::Reference::try_from_path("HEAD".try_into()?, contents, hash) .expect_err("a malformed object id is corruption"); - insta::assert_debug_snapshot!(err, "scalar context does not invent a validation failure", @r#"Reference content could not be parsed, "input"="invalid\xff""#); + insta::assert_debug_snapshot!(err, "scalar context does not invent a validation failure", @r#"Reference content could not be parsed, input="invalid\xff""#); assert!(err.is_corrupted(), "malformed reference contents are corruption"); assert_eq!( err.iter_errors().count(), @@ -671,10 +671,10 @@ fn loose_reference_diagnostics_keep_input_with_the_failure() -> Result { let err = gix_ref::file::loose::Reference::try_from_path("HEAD".try_into()?, b"ref: refs/heads/.bad\n", hash) .expect_err("a symbolic target must be a valid reference name"); insta::assert_debug_snapshot!(err, "the callee's validation class remains available", @r#" - Could not decode reference, "input"="ref: refs/heads/.bad\n" + Could not decode reference, input="ref: refs/heads/.bad\n" Caused by: - 0: Invalid symbolic reference target, "target"="refs/heads/.bad" + 0: Invalid symbolic reference target, target="refs/heads/.bad" 1: Reference name cannot start with a dot "#); assert!(err.is_validation(), "the callee's validation class remains available"); diff --git a/gix-ref/tests/refs/file/log.rs b/gix-ref/tests/refs/file/log.rs index 989aca539a4..327b99b939d 100644 --- a/gix-ref/tests/refs/file/log.rs +++ b/gix-ref/tests/refs/file/log.rs @@ -245,10 +245,10 @@ mod iter { let mut iter = gix_ref::file::log::iter::forward(log_first_broken.as_bytes()); let err = iter.next().expect("error is not none").expect_err("the line is broken"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "a single failure does not abort iteration", @r#" - Invalid reflog entry, "from_end"=false, "line"=1 + Invalid reflog entry, from_end=false, line=1 Caused by: - 0: Could not decode reflog line, "input"="Oid(1) 134385fbroken7062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000\tcommit" + 0: Could not decode reflog line, input="Oid(1) 134385fbroken7062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000\tcommit" 1: Malformed reflog line "#); assert!(err.is_corrupted()); diff --git a/gix-ref/tests/refs/file/reference.rs b/gix-ref/tests/refs/file/reference.rs index 8ccb3cc3d3a..1f1afdd0905 100644 --- a/gix-ref/tests/refs/file/reference.rs +++ b/gix-ref/tests/refs/file/reference.rs @@ -196,7 +196,7 @@ mod peel { assert_eq!(r, "refs/loop-a"); let err = r.peel_to_id(&store, &gix_object::find::Never).expect_err("cyclic refs"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "a symbolic cycle is corruption", @r#"Aborting symbolic reference cycle, "path"="<git-dir>/refs/loop-a""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "a symbolic cycle is corruption", @r#"Aborting symbolic reference cycle, path="<git-dir>/refs/loop-a""#); assert!(err.is_corrupted(), "a symbolic cycle is corruption"); assert_eq!(err.iter_errors().count(), 1, "a cycle does not need a synthetic cause"); let details = err.metadata().next().expect("cycle details"); @@ -220,7 +220,7 @@ mod peel { let err = r .follow_to_object_packed(&store, store.cached_packed_buffer()?.as_ref().map(|p| &***p)) .expect_err("the symbolic references form a cycle"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "following also reports the cycle", @r#"Aborting symbolic reference cycle, "path"="<git-dir>/refs/loop-a""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "following also reports the cycle", @r#"Aborting symbolic reference cycle, path="<git-dir>/refs/loop-a""#); assert!(err.is_corrupted(), "following also reports the cycle"); Ok(()) } diff --git a/gix-ref/tests/refs/file/store/find.rs b/gix-ref/tests/refs/file/store/find.rs index 4bc6b5ad3a5..189a5e62f17 100644 --- a/gix-ref/tests/refs/file/store/find.rs +++ b/gix-ref/tests/refs/file/store/find.rs @@ -215,7 +215,7 @@ mod loose { .try_find_loose(name) .expect_err("reserved device names cannot be read when prohibited"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "rejecting a device name retains the original I/O error kind", @r#" - Could not read reference, "path"="<git-dir>/refs/heads/CON" + Could not read reference, path="<git-dir>/refs/heads/CON" Caused by: 0: I/O error (Other) diff --git a/gix-ref/tests/refs/file/store/iter.rs b/gix-ref/tests/refs/file/store/iter.rs index a5f819d9712..285a46c288a 100644 --- a/gix-ref/tests/refs/file/store/iter.rs +++ b/gix-ref/tests/refs/file/store/iter.rs @@ -313,7 +313,7 @@ fn loose_iter_with_broken_refs() -> Result { The reference at "refs/broken" could not be decoded Caused by: - 0: Reference content could not be parsed, "input"="notahexsha\n" + 0: Reference content could not be parsed, input="notahexsha\n" "#); let ref_paths: Vec<_> = actual .drain(..first_error) diff --git a/gix-ref/tests/refs/file/store/reflog.rs b/gix-ref/tests/refs/file/store/reflog.rs index eb0cceeaf1a..f15f725a6d9 100644 --- a/gix-ref/tests/refs/file/store/reflog.rs +++ b/gix-ref/tests/refs/file/store/reflog.rs @@ -55,11 +55,11 @@ mod iter_and_iter_rev { } insta::assert_debug_snapshot!(error_snapshots, "read failures preserve context", @r#" [ - Could not read reflog, "path"="<git-dir>/logs/refs/heads/main/child" + Could not read reflog, path="<git-dir>/logs/refs/heads/main/child" Caused by: 0: NotADirectory, - Could not read reflog, "path"="<git-dir>/logs/refs/heads/main/child" + Could not read reflog, path="<git-dir>/logs/refs/heads/main/child" Caused by: 0: NotADirectory, diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs index 35db55b524a..8c534b1f04c 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs @@ -36,7 +36,7 @@ fn conflicting_creation_without_packed_refs() -> Result { Err(err) if case_sensitive => panic!("should work as case sensitivity allows 'a' and 'A' to coexist: {err:?}"), Err(err) if !case_sensitive => { insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&store.git_dir().to_string_lossy(), "<git-dir>")]), "case-insensitive filesystems reject simultaneous locks for refs/a and refs/A", @r#" - Could not prepare reference edit, "reference"="refs/A", "referent"="refs/A" + Could not prepare reference edit, reference="refs/A" Caused by: 0: The lock for resource "<git-dir>/refs/A" could not be obtained immediately after 1 attempt(s). The lockfile at "<git-dir>/refs/A.lock" might need manual deletion. @@ -130,7 +130,7 @@ fn conflicting_creation_into_packed_refs() -> Result { if !case_sensitive(dir.path()) { let err = transaction.expect_err("case-insensitive collision"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&store.git_dir().to_string_lossy(), "<git-dir>")]), "packed ref updates still acquire loose locks before their CAS read", @r#" - Could not prepare reference edit, "reference"="refs/A", "referent"="refs/A" + Could not prepare reference edit, reference="refs/A" Caused by: 0: The lock for resource "<git-dir>/refs/A" could not be obtained immediately after 1 attempt(s). The lockfile at "<git-dir>/refs/A.lock" might need manual deletion. diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs index 3d51225b428..cdf28724e67 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -102,7 +102,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca #[cfg(not(windows))] insta::assert_debug_snapshot!(error_snapshots, "reference with equally named empty or non empty directory already in place can potentially recover", @r#" [ - Could not commit reference, "reference"="HEAD" + Could not commit reference, reference="HEAD" Caused by: 0: I/O error (Other) @@ -131,7 +131,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> Result { let err = res.expect_err("the previous reference must exist"); insta::assert_debug_snapshot!(err, "reference with old value must exist when creating it", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="HEAD" + Could not prepare reference edit, reference="HEAD" Caused by: 0: The reference to update must exist @@ -144,6 +144,79 @@ fn reference_with_old_value_must_exist_when_creating_it() -> Result { Ok(()) } +#[test] +fn target_mismatch_sources_retain_expected_and_actual_content_and_resolved_name() -> gix_error::TestResult { + for deref in [false, true] { + for delete in [false, true] { + for must_exist in [false, true] { + let (_keep, store) = empty_store()?; + let actual = Target::Object(crate::fixture_hash_kind().null()); + let referent = if deref { "refs/heads/main" } else { "HEAD" }; + // Deletion checks the symbolic parent as well as the referent, so let the parent check succeed. + let expected = if deref && delete { + Target::Symbolic(referent.try_into()?) + } else { + Target::Object(ObjectId::empty_blob(crate::fixture_hash_kind())) + }; + if deref { + std::fs::write(store.git_dir().join("HEAD"), b"ref: refs/heads/main\n")?; + std::fs::create_dir_all(store.git_dir().join("refs/heads"))?; + } + std::fs::write(store.git_dir().join(referent), format!("{actual}\n"))?; + let previous = if must_exist { + PreviousValue::MustExistAndMatch(expected.clone()) + } else { + PreviousValue::ExistingMustMatch(expected.clone()) + }; + let edit = if delete { + RefEdit::delete("HEAD".try_into()?, previous) + } else { + RefEdit::update("HEAD".try_into()?, expected.clone(), previous, "") + } + .with_deref(deref); + let err = store + .transaction() + .prepare([edit], Fail::Immediately, Fail::Immediately) + .expect_err("the locked reference has a different target"); + + let sources = std::iter::successors(Some(&err as &dyn std::error::Error), |error| error.source()) + .map(ToString::to_string) + .collect::<Vec<_>>(); + assert!( + sources + .iter() + .any(|cause| cause.contains(&format!("Expected reference content {expected}"))), + "standard cause traversal retains the expected target: {sources:?}" + ); + assert!( + sources + .iter() + .any(|cause| cause.contains(&format!("The reference {referent:?} changed to {actual}"))), + "standard cause traversal reaches the actual target: {sources:?}" + ); + let details = err.metadata().next().expect("failed edit identifies its reference"); + assert_eq!(details["reference"], gix_error::MetadataValue::from(b"HEAD".as_slice())); + assert_eq!( + details.get("referent"), + deref + .then(|| gix_error::MetadataValue::from(referent.as_bytes())) + .as_ref(), + "the resolved name is recorded only when it differs from the requested name" + ); + let stale = err + .downcast_any_ref::<transaction::prepare::ReferenceOutOfDate>() + .expect("the actual target remains available for typed recovery"); + assert_eq!(stale.actual, actual, "typed recovery retains the locked target"); + assert!( + err.is_conflict() && !err.can_retry(), + "stale targets require reconciliation" + ); + } + } + } + Ok(()) +} + #[test] fn reference_with_explicit_value_must_match_the_value_on_update() -> Result { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; @@ -162,7 +235,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> Result { ); let err = res.expect_err("the transaction constraint is violated"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "retrying requires reconciling the current value", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="HEAD" + Could not prepare reference edit, reference="HEAD" Caused by: 0: Expected reference content Oid(1) @@ -233,7 +306,7 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g ); let err = res.expect_err("the transaction constraint is violated"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "retrying requires reconciling the current value", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="HEAD" + Could not prepare reference edit, reference="HEAD" Caused by: 0: Expected reference content Oid(1) @@ -263,7 +336,7 @@ fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_alrea .prepare(Some(create_at("HEAD")), Fail::Immediately, Fail::Immediately); let err = res.expect_err("the transaction constraint is violated"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "retrying requires reconciling the current value", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="HEAD" + Could not prepare reference edit, reference="HEAD" Caused by: 0: Expected the reference not to exist when writing Oid(1) @@ -498,13 +571,13 @@ fn windows_device_name_is_illegal_with_enabled_windows_protections() -> Result { insta::assert_debug_snapshot!(error_snapshots, "windows device name is illegal with enabled windows protections", @r#" [ - Could not prepare reference edit, "reference"="refs/heads/CON", "referent"="refs/heads/CON" + Could not prepare reference edit, reference="refs/heads/CON" Caused by: 0: Invalid reference filename 1: I/O error (Other) 2: Illegal use of reserved Windows device name in "refs/heads/CON", - Could not prepare reference edit, "reference"="refs/CON/still-invalid", "referent"="refs/CON/still-invalid" + Could not prepare reference edit, reference="refs/CON/still-invalid" Caused by: 0: Invalid reference filename @@ -583,7 +656,7 @@ fn lock_failure_on_symbolic_referent_is_reported_for_the_symbolic_ref() -> Resul ) .unwrap_err(); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(store.git_dir()).to_string_lossy(), "<git-dir>")]), "the original lock failure is classifiable", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="refs/heads/main" + Could not prepare reference edit, reference="HEAD", referent="refs/heads/main" Caused by: 0: The lock for resource "<git-dir>/refs/heads/main" could not be obtained immediately after 1 attempt(s). The lockfile at "<git-dir>/refs/heads/main.lock" might need manual deletion. diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs index 5e990ddfaa8..9dfd6198747 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs @@ -39,7 +39,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> Result { ); let err = res.expect_err("the reference must exist"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&store.git_dir().to_string_lossy(), "<git-dir>")]), "delete a ref which is gone but must exist fails", @r#" - Could not prepare reference edit, "reference"="DOES_NOT_EXIST", "referent"="DOES_NOT_EXIST" + Could not prepare reference edit, reference="DOES_NOT_EXIST" Caused by: 0: The reference to delete must exist @@ -102,7 +102,7 @@ fn delete_ref_with_incorrect_previous_value_fails() -> Result { let err = res.expect_err("the expected target differs"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "reference deletion retains the expected and actual target when they differ", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="refs/heads/main" + Could not prepare reference edit, reference="HEAD", referent="refs/heads/main" Caused by: 0: Expected reference content ref: refs/heads/main @@ -207,7 +207,7 @@ fn rename_a_to_a_slash_b_in_one_transaction() -> Result { ) .unwrap_err(); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "path-prefix collisions are reported early, without losing the I/O kind", @r#" - Could not prepare reference edit, "reference"="refs/heads/old/new", "referent"="refs/heads/old/new" + Could not prepare reference edit, reference="refs/heads/old/new" Caused by: 0: Another IO error occurred while obtaining the lock @@ -263,7 +263,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> Result { ); let err = res.expect_err("a valid existing reference is required"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&store.git_dir().to_string_lossy(), "<git-dir>")]), "delete broken ref that must exist fails as it is no valid ref", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="HEAD" + Could not prepare reference edit, reference="HEAD" Caused by: 0: The reference to delete must exist diff --git a/gix-ref/tests/refs/file/worktree.rs b/gix-ref/tests/refs/file/worktree.rs index 1ecc594952b..0be49a721d4 100644 --- a/gix-ref/tests/refs/file/worktree.rs +++ b/gix-ref/tests/refs/file/worktree.rs @@ -458,13 +458,13 @@ mod writable { insta::assert_debug_snapshot!(error_snapshots, "prefixed and unprefixed names share the main worktree reference lock", @r#" [ - Could not prepare reference edit, "reference"="refs/heads/foo", "referent"="refs/heads/foo" + Could not prepare reference edit, reference="refs/heads/foo" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/foo" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/foo.lock" might need manual deletion. 1: I/O error (AlreadyExists) 2: AlreadyExists at path "<common-git-dir>/refs/heads/foo.lock", - Could not prepare reference edit, "reference"="refs/heads/foo", "referent"="refs/heads/foo" + Could not prepare reference edit, reference="refs/heads/foo" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/foo" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/foo.lock" might need manual deletion. @@ -669,25 +669,25 @@ mod writable { insta::assert_debug_snapshot!(error_snapshots, "worktree-prefixed names resolve to the same shared reference lock", @r#" [ - Could not prepare reference edit, "reference"="refs/heads/shared", "referent"="refs/heads/shared" + Could not prepare reference edit, reference="refs/heads/shared" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/shared" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/shared.lock" might need manual deletion. 1: I/O error (AlreadyExists) 2: AlreadyExists at path "<common-git-dir>/refs/heads/shared.lock", - Could not prepare reference edit, "reference"="refs/heads/shared", "referent"="refs/heads/shared" + Could not prepare reference edit, reference="refs/heads/shared" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/shared" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/shared.lock" might need manual deletion. 1: I/O error (AlreadyExists) 2: AlreadyExists at path "<common-git-dir>/refs/heads/shared.lock", - Could not prepare reference edit, "reference"="refs/heads/shared", "referent"="refs/heads/shared" + Could not prepare reference edit, reference="refs/heads/shared" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/shared" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/shared.lock" might need manual deletion. 1: I/O error (AlreadyExists) 2: AlreadyExists at path "<common-git-dir>/refs/heads/shared.lock", - Could not prepare reference edit, "reference"="refs/heads/shared", "referent"="refs/heads/shared" + Could not prepare reference edit, reference="refs/heads/shared" Caused by: 0: The lock for resource "<common-git-dir>/refs/heads/shared" could not be obtained immediately after 1 attempt(s). The lockfile at "<common-git-dir>/refs/heads/shared.lock" might need manual deletion. diff --git a/gix-ref/tests/refs/packed/find.rs b/gix-ref/tests/refs/packed/find.rs index 200995ac903..0c7072156a1 100644 --- a/gix-ref/tests/refs/packed/find.rs +++ b/gix-ref/tests/refs/packed/find.rs @@ -188,11 +188,11 @@ bogus refs/tags/gix-actor-v0.1.0 } insta::assert_debug_snapshot!(error_snapshots, "invalid refs within a file do not lead to incorrect results", @r#" [ - Could not decode packed reference, "name"="refs/tags/TEST-0.0.1" + Could not decode packed reference, name="refs/tags/TEST-0.0.1" Caused by: 0: Malformed packed reference record, - Could not decode packed reference, "name"="refs/tags/gix-actor-v0.1.0" + Could not decode packed reference, name="refs/tags/gix-actor-v0.1.0" Caused by: 0: Malformed packed reference record, diff --git a/gix-ref/tests/refs/packed/iter.rs b/gix-ref/tests/refs/packed/iter.rs index 8d8d39e8aad..d7ca855d42c 100644 --- a/gix-ref/tests/refs/packed/iter.rs +++ b/gix-ref/tests/refs/packed/iter.rs @@ -52,9 +52,9 @@ fn invalid_header_has_one_classified_diagnostic() { } insta::assert_debug_snapshot!(error_snapshots, "invalid header has one classified diagnostic", @r##" [ - Invalid packed reference header, "input"="# invalid", - Invalid packed reference header, "input"="# invalid", - Invalid packed reference header, "input"="# pack-refs with: sorted", + Invalid packed reference header, input="# invalid", + Invalid packed reference header, input="# invalid", + Invalid packed reference header, input="# pack-refs with: sorted", ] "##); } @@ -168,11 +168,11 @@ buggy-hash refs/wrong assert!(iter.next().is_none(), "exhausted"); insta::assert_debug_snapshot!(error_snapshots, "broken ref doesnt end the iteration", @r#" [ - Invalid packed reference, "input"="buggy-hash refs/wrong", "line"=2 + Invalid packed reference, input="buggy-hash refs/wrong", line=2 Caused by: 0: Malformed packed reference, - Invalid packed reference, "input"="^buggy-hash-too", "line"=3 + Invalid packed reference, input="^buggy-hash-too", line=3 Caused by: 0: Malformed packed reference, diff --git a/gix-ref/tests/refs/packed/open.rs b/gix-ref/tests/refs/packed/open.rs index fa35d3e8816..0656c9b4507 100644 --- a/gix-ref/tests/refs/packed/open.rs +++ b/gix-ref/tests/refs/packed/open.rs @@ -48,7 +48,7 @@ fn bogus_content_triggers_an_error() -> Result { let err = gix_ref::packed::Buffer::open(path, 32, HASH_KIND).expect_err("malformed packed refs"); insta::assert_debug_snapshot!(err, "bogus content triggers an error", @r#" - Invalid packed reference, "input"="starts with a bogus record, not a header anyway", "line"=1 + Invalid packed reference, input="starts with a bogus record, not a header anyway", line=1 Caused by: 0: Malformed packed reference diff --git a/gix-refspec/tests/refspec/parse/fetch.rs b/gix-refspec/tests/refspec/parse/fetch.rs index 0dc74ecd767..81290d44c21 100644 --- a/gix-refspec/tests/refspec/parse/fetch.rs +++ b/gix-refspec/tests/refspec/parse/fetch.rs @@ -221,9 +221,9 @@ fn patterns_with_multiple_asterisks_are_rejected() { } insta::assert_debug_snapshot!(diagnostics, "patterns with multiple asterisks are rejected", @r#" [ - refspec patterns may only contain a single '*' character, "input"="refs/*/foo/*", - refspec patterns may only contain a single '*' character, "input"="refs/*/*", - refspec patterns may only contain a single '*' character, "input"="a/*/c/*", + refspec patterns may only contain a single '*' character, input="refs/*/foo/*", + refspec patterns may only contain a single '*' character, input="refs/*/*", + refspec patterns may only contain a single '*' character, input="a/*/c/*", ] "#); } diff --git a/gix-refspec/tests/refspec/parse/invalid.rs b/gix-refspec/tests/refspec/parse/invalid.rs index 4bd7f5bbf00..c251401dd4d 100644 --- a/gix-refspec/tests/refspec/parse/invalid.rs +++ b/gix-refspec/tests/refspec/parse/invalid.rs @@ -61,7 +61,7 @@ fn patterns_may_contain_only_one_asterisk() { } } - insta::assert_debug_snapshot!(assert_unsupported_pattern("^*/*", Operation::Fetch), "patterns may contain only one asterisk", @r#"refspec patterns may only contain a single '*' character, "input"="*/*""#); + insta::assert_debug_snapshot!(assert_unsupported_pattern("^*/*", Operation::Fetch), "patterns may contain only one asterisk", @r#"refspec patterns may only contain a single '*' character, input="*/*""#); // Negative refspec patterns follow Git's single-asterisk refspec-pattern rule. for op in [Operation::Fetch, Operation::Push] { diagnostics.push(gix_testtools::redact_debug_snapshot( @@ -82,20 +82,20 @@ fn patterns_may_contain_only_one_asterisk() { } insta::assert_debug_snapshot!(diagnostics, "patterns may contain only one asterisk", @r#" [ - refspec patterns may only contain a single '*' character, "input"="a/*/c/*", - refspec patterns may only contain a single '*' character, "input"="a/*/c/*", - refspec patterns may only contain a single '*' character, "input"="a**", - refspec patterns may only contain a single '*' character, "input"="**/", - refspec patterns may only contain a single '*' character, "input"="a/*/c/*", - refspec patterns may only contain a single '*' character, "input"="a/*/c/*", - refspec patterns may only contain a single '*' character, "input"="a**", - refspec patterns may only contain a single '*' character, "input"="**/", - refspec patterns may only contain a single '*' character, "input"="refs/heads/qa/*/*", + refspec patterns may only contain a single '*' character, input="a/*/c/*", + refspec patterns may only contain a single '*' character, input="a/*/c/*", + refspec patterns may only contain a single '*' character, input="a**", + refspec patterns may only contain a single '*' character, input="**/", + refspec patterns may only contain a single '*' character, input="a/*/c/*", + refspec patterns may only contain a single '*' character, input="a/*/c/*", + refspec patterns may only contain a single '*' character, input="a**", + refspec patterns may only contain a single '*' character, input="**/", + refspec patterns may only contain a single '*' character, input="refs/heads/qa/*/*", Reference name contains invalid byte: "?", Reference name contains invalid byte: "[", Reference name cannot contain repeated dots, Reference name cannot end with a slash, - refspec patterns may only contain a single '*' character, "input"="refs/heads/qa/*/*", + refspec patterns may only contain a single '*' character, input="refs/heads/qa/*/*", Reference name contains invalid byte: "?", Reference name contains invalid byte: "[", Reference name cannot contain repeated dots, diff --git a/gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs index 90e43f0ebeb..5ed5cdcfedd 100644 --- a/gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs +++ b/gix-revision/tests/revision/spec/parse/anchor/at_symbol.rs @@ -79,7 +79,7 @@ fn reflog_by_unix_timestamp_for_current_branch() { #[test] fn reflog_by_date_with_date_parse_failure() { let err = try_parse("@{foo}").unwrap_err(); - insta::assert_snapshot!(err, @r#"could not parse time for reflog lookup, "input"="foo""#); + insta::assert_snapshot!(err, @r#"could not parse time for reflog lookup, input="foo""#); } #[test] @@ -102,9 +102,9 @@ fn reflog_by_date_for_hash_is_invalid() { } insta::assert_debug_snapshot!(message_diagnostics, "reflog by date for hash is invalid", @r#" [ - reflog entries require a ref name, "input"="1234", - reflog entries require a ref name, "input"="abcd-dirty", - reflog entries require a ref name, "input"="v1.2.3-0-g1234", + reflog entries require a ref name, input="1234", + reflog entries require a ref name, input="abcd-dirty", + reflog entries require a ref name, input="v1.2.3-0-g1234", ] "#); } @@ -163,9 +163,9 @@ fn reflog_by_entry_for_hash_is_invalid() { } insta::assert_debug_snapshot!(message_diagnostics, "reflog by entry for hash is invalid", @r#" [ - reflog entries require a ref name, "input"="1234", - reflog entries require a ref name, "input"="abcd-dirty", - reflog entries require a ref name, "input"="v1.2.3-0-g1234", + reflog entries require a ref name, input="1234", + reflog entries require a ref name, input="abcd-dirty", + reflog entries require a ref name, input="v1.2.3-0-g1234", ] "#); } @@ -224,9 +224,9 @@ fn sibling_branch_for_hash_is_invalid() { } insta::assert_debug_snapshot!(message_diagnostics, "sibling branch for hash is invalid", @r#" [ - sibling branches like 'upstream' or 'push' require a branch name with remote configuration, "input"="1234", - sibling branches like 'upstream' or 'push' require a branch name with remote configuration, "input"="abcd-dirty", - sibling branches like 'upstream' or 'push' require a branch name with remote configuration, "input"="v1.2.3-0-g1234", + sibling branches like 'upstream' or 'push' require a branch name with remote configuration, input="1234", + sibling branches like 'upstream' or 'push' require a branch name with remote configuration, input="abcd-dirty", + sibling branches like 'upstream' or 'push' require a branch name with remote configuration, input="v1.2.3-0-g1234", ] "#); } @@ -235,7 +235,7 @@ fn sibling_branch_for_hash_is_invalid() { fn nth_checked_out_branch_for_refname_is_invalid() { let err = try_parse("r1@{-1}").unwrap_err(); // its undefined how to handle negative numbers and specified ref names - insta::assert_snapshot!(err, @r#"reference name must be followed by positive numbers in @{n}, "input"="-1""#); + insta::assert_snapshot!(err, @r#"reference name must be followed by positive numbers in @{n}, input="-1""#); } #[test] @@ -258,7 +258,7 @@ fn nth_checked_out_branch() { fn numbers_within_braces_cannot_be_negative_zero() { let err = try_parse("@{-0}").unwrap_err(); // negative zero is not accepted, even though it could easily be defaulted to 0 which is a valid value - insta::assert_snapshot!(err, @r#"negative zero is invalid - remove the minus sign, "input"="-0""#); + insta::assert_snapshot!(err, @r#"negative zero is invalid - remove the minus sign, input="-0""#); } #[test] diff --git a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs index de0ce526151..20daa20b6d1 100644 --- a/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs +++ b/gix-revision/tests/revision/spec/parse/anchor/colon_symbol.rs @@ -117,7 +117,7 @@ fn regex_with_empty_exclamation_mark_prefix_is_invalid() { .get("input"), Some(&gix_error::MetadataValue::from(b"!hello".as_ref())) ); - insta::assert_snapshot!(err, @r#"need one character after /!, typically -, "input"="!hello""#); + insta::assert_snapshot!(err, @r#"need one character after /!, typically -, input="!hello""#); } #[test] diff --git a/gix-revision/tests/revision/spec/parse/anchor/refnames.rs b/gix-revision/tests/revision/spec/parse/anchor/refnames.rs index e3496adafa2..78e3ee18340 100644 --- a/gix-revision/tests/revision/spec/parse/anchor/refnames.rs +++ b/gix-revision/tests/revision/spec/parse/anchor/refnames.rs @@ -35,7 +35,7 @@ fn at_in_ranges_is_allowed() { #[test] fn strange_revspecs_do_not_panic() { let err = try_parse(".@.").unwrap_err(); - insta::assert_snapshot!(err, @r#"@ character must be standalone or followed by {<content>}, "input"="@.""#); + insta::assert_snapshot!(err, @r#"@ character must be standalone or followed by {<content>}, input="@.""#); } #[test] diff --git a/gix-revision/tests/revision/spec/parse/kind.rs b/gix-revision/tests/revision/spec/parse/kind.rs index 2ba4bed8547..cb1c7783af3 100644 --- a/gix-revision/tests/revision/spec/parse/kind.rs +++ b/gix-revision/tests/revision/spec/parse/kind.rs @@ -79,7 +79,7 @@ mod include_parents { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("~1")}, + values: {input: Bytes("~1")}, } "#); } @@ -133,7 +133,7 @@ mod exclude_parents { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("~1")}, + values: {input: Bytes("~1")}, } "#); } @@ -271,7 +271,7 @@ mod range { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("^")}, + values: {input: Bytes("^")}, } "#); } @@ -286,7 +286,7 @@ mod range { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("^")}, + values: {input: Bytes("^")}, } "#); } @@ -301,7 +301,7 @@ mod range { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("..")}, + values: {input: Bytes("..")}, } "#); } @@ -316,7 +316,7 @@ mod range { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("..")}, + values: {input: Bytes("..")}, } "#); } diff --git a/gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs b/gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs index f2e3d97ddf5..80e5c2f14f8 100644 --- a/gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs +++ b/gix-revision/tests/revision/spec/parse/navigate/caret_symbol.rs @@ -67,7 +67,7 @@ fn explicitly_positive_numbers_are_invalid() { Message { message: "explicitly positive numbers are invalid here", class: Validation, - values: {"input": Bytes("+1")}, + values: {input: Bytes("+1")}, } "#); } @@ -200,7 +200,7 @@ fn invalid_object_type() { Message { message: "cannot peel to unknown target", class: Validation, - values: {"input": Bytes("invalid")}, + values: {input: Bytes("invalid")}, } "#); @@ -212,7 +212,7 @@ fn invalid_object_type() { Message { message: "cannot peel to unknown target", class: Validation, - values: {"input": Bytes("Commit")}, + values: {input: Bytes("Commit")}, } "#); assert!( @@ -246,12 +246,12 @@ fn invalid_caret_without_previous_refname() { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("HEAD")}, + values: {input: Bytes("HEAD")}, }, Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("HEAD")}, + values: {input: Bytes("HEAD")}, }, ] "#); @@ -268,7 +268,7 @@ fn incomplete_escaped_braces_in_regex_are_invalid() { Message { message: "unconsumed input", class: Validation, - values: {"input": Bytes("}")}, + values: {input: Bytes("}")}, } "#); @@ -280,7 +280,7 @@ fn incomplete_escaped_braces_in_regex_are_invalid() { Message { message: "unclosed brace pair", class: Validation, - values: {"input": Bytes("{/a{1\\}}")}, + values: {input: Bytes("{/a{1\\}}")}, } "#); assert!( @@ -300,7 +300,7 @@ fn regex_with_empty_exclamation_mark_prefix_is_invalid() { Message { message: "need one character after /!, typically -", class: Validation, - values: {"input": Bytes("!hello")}, + values: {input: Bytes("!hello")}, } "#); } @@ -315,7 +315,7 @@ fn bad_escapes_can_cause_brace_mismatch() { Message { message: "unclosed brace pair", class: Validation, - values: {"input": Bytes("{\\}")}, + values: {input: Bytes("{\\}")}, } "#); assert!( @@ -332,7 +332,7 @@ fn bad_escapes_can_cause_brace_mismatch() { Message { message: "unclosed brace pair", class: Validation, - values: {"input": Bytes("{{\\}}")}, + values: {input: Bytes("{{\\}}")}, } "#); assert!( diff --git a/gix-submodule/tests/file/mod.rs b/gix-submodule/tests/file/mod.rs index 95c9f1c2c45..7cf4d559427 100644 --- a/gix-submodule/tests/file/mod.rs +++ b/gix-submodule/tests/file/mod.rs @@ -193,7 +193,7 @@ mod path { Message { message: "The path of submodule 'a' needs to be relative", class: Validation, - values: {"input": Bytes("<absolute-path>")}, + values: {input: Bytes("<absolute-path>")}, } "#); insta::assert_debug_snapshot!(submodule_path("").error(), "validate upon retrieval", @r#" @@ -206,7 +206,7 @@ mod path { Message { message: "The path would lead outside of the repository worktree", class: Validation, - values: {"input": Bytes("../attack")}, + values: {input: Bytes("../attack")}, } "#); @@ -271,10 +271,10 @@ mod url { } insta::assert_debug_snapshot!(submodule_url("file://"), "validate upon retrieval", @r#" - The url of submodule 'a' could not be parsed, "input"="file://" + The url of submodule 'a' could not be parsed, input="file://" Caused by: - 0: URL does not specify a path to a repository, "input"="file://" + 0: URL does not specify a path to a repository, input="file://" "#); insta::assert_debug_snapshot!(message_diagnostics, "validate upon retrieval", @r#" [ @@ -350,21 +350,21 @@ mod update { Message { message: "The 'update' field of submodule 'a' was invalid", class: Validation, - values: {"input": Bytes("")}, + values: {input: Bytes("")}, } "#); insta::assert_debug_snapshot!(submodule_update("bogus").error(), "validate upon retrieval", @r#" Message { message: "The 'update' field of submodule 'a' was invalid", class: Validation, - values: {"input": Bytes("bogus")}, + values: {input: Bytes("bogus")}, } "#); insta::assert_debug_snapshot!(submodule_update("!dangerous").error(), "forbidden unless it's an override", @r#" Message { message: "The 'update' field of submodule 'a' tried to set a command to be shared", class: Validation, - values: {"input": Bytes("dangerous")}, + values: {input: Bytes("dangerous")}, } "#); } @@ -393,7 +393,7 @@ mod update { Message { message: "The 'update' field of submodule 'a' tried to set a command to be shared", class: Validation, - values: {"input": Bytes("dangerous")}, + values: {input: Bytes("dangerous")}, } "#); Ok(()) diff --git a/gix-url/tests/url/parse/file.rs b/gix-url/tests/url/parse/file.rs index dec2bc3b7d6..7167a4922c7 100644 --- a/gix-url/tests/url/parse/file.rs +++ b/gix-url/tests/url/parse/file.rs @@ -114,7 +114,7 @@ fn no_relative_paths_if_protocol() -> gix_error::TestResult { ); } else { let err = gix_url::parse(r"file://.\").expect_err("the input must be rejected"); - insta::assert_debug_snapshot!(err, "Unix requires a forward slash after the host", @r#"URL does not specify a path to a repository, "input"="file://.\\""#); + insta::assert_debug_snapshot!(err, "Unix requires a forward slash after the host", @r#"URL does not specify a path to a repository, input="file://.\\""#); } Ok(()) } diff --git a/gix-url/tests/url/parse/http.rs b/gix-url/tests/url/parse/http.rs index 4ffed8d6c78..f6b222d4bf3 100644 --- a/gix-url/tests/url/parse/http.rs +++ b/gix-url/tests/url/parse/http.rs @@ -376,6 +376,6 @@ fn authority_length_limit_excludes_the_scheme_separator() -> gix_error::TestResu let over_limit = format!("https://{}", "a".repeat(1025)); insta::assert_debug_snapshot!(gix_url::parse(over_limit) .expect_err("the input must be rejected") - , "one byte beyond the authority limit is rejected", @r#"The host portion of the URL is too long (1032 bytes shown, 1033 bytes total), "input"="https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""#); + , "one byte beyond the authority limit is rejected", @r#"The host portion of the URL is too long (1032 bytes shown, 1033 bytes total), input="https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""#); Ok(()) } diff --git a/gix-url/tests/url/parse/invalid.rs b/gix-url/tests/url/parse/invalid.rs index 4cc2f0e8c01..4b6db8c4ee6 100644 --- a/gix-url/tests/url/parse/invalid.rs +++ b/gix-url/tests/url/parse/invalid.rs @@ -23,7 +23,7 @@ fn relative_path_due_to_double_colon() { // Note that a non-empty name before the `::` makes this a remote-helper location instead, // as covered by `parse::remote_helper`. insta::assert_debug_snapshot!(assert_validation(":://host.xz/path/to/repo.git/", true), "relative path due to double colon", @r#" - URL can not be parsed as valid URL, "input"=":://host.xz/path/to/repo.git/" + URL can not be parsed as valid URL, input=":://host.xz/path/to/repo.git/" Caused by: 0: relative URL without a base @@ -32,22 +32,22 @@ fn relative_path_due_to_double_colon() { #[test] fn ssh_missing_path() { - insta::assert_debug_snapshot!(assert_validation("ssh://host.xz", false), "ssh missing path", @r#"URL does not specify a path to a repository, "input"="ssh://host.xz""#); + insta::assert_debug_snapshot!(assert_validation("ssh://host.xz", false), "ssh missing path", @r#"URL does not specify a path to a repository, input="ssh://host.xz""#); } #[test] fn git_missing_path() { - insta::assert_debug_snapshot!(assert_validation("git://host.xz", false), "git missing path", @r#"URL does not specify a path to a repository, "input"="git://host.xz""#); + insta::assert_debug_snapshot!(assert_validation("git://host.xz", false), "git missing path", @r#"URL does not specify a path to a repository, input="git://host.xz""#); } #[test] fn file_missing_path() { - insta::assert_debug_snapshot!(assert_validation("file://", false), "file missing path", @r#"URL does not specify a path to a repository, "input"="file://""#); + insta::assert_debug_snapshot!(assert_validation("file://", false), "file missing path", @r#"URL does not specify a path to a repository, input="file://""#); } #[test] fn empty_input() { - insta::assert_debug_snapshot!(assert_validation("", false), "empty input", @r#"local path is empty and does not specify a path to a repository, "input"="""#); + insta::assert_debug_snapshot!(assert_validation("", false), "empty input", @r#"local path is empty and does not specify a path to a repository, input="""#); } #[test] @@ -58,16 +58,16 @@ fn file_missing_host_path_separator() { } insta::assert_debug_snapshot!(diagnostics, "file missing host path separator", @r#" [ - URL does not specify a path to a repository, "input"="file://..", - URL does not specify a path to a repository, "input"="file://.", - URL does not specify a path to a repository, "input"="file://a", + URL does not specify a path to a repository, input="file://..", + URL does not specify a path to a repository, input="file://.", + URL does not specify a path to a repository, input="file://a", ] "#); } #[test] fn missing_port_despite_indication() { - insta::assert_debug_snapshot!(assert_validation("ssh://host.xz:", false), "missing port despite indication", @r#"URL does not specify a path to a repository, "input"="ssh://host.xz:""#); + insta::assert_debug_snapshot!(assert_validation("ssh://host.xz:", false), "missing port despite indication", @r#"URL does not specify a path to a repository, input="ssh://host.xz:""#); } #[test] @@ -97,25 +97,25 @@ fn textual_and_overflowing_ssh_and_git_ports_are_rejected_despite_git() { } insta::assert_debug_snapshot!(diagnostics, "textual and overflowing ssh and git ports are rejected despite git", @r#" [ - URL can not be parsed as valid URL, "input"="ssh://host.xz:abc/path" + URL can not be parsed as valid URL, input="ssh://host.xz:abc/path" Caused by: 0: invalid port number - must be between 1-65535, - URL can not be parsed as valid URL, "input"="git://host.xz:abc/path" + URL can not be parsed as valid URL, input="git://host.xz:abc/path" Caused by: 0: invalid port number - must be between 1-65535, - URL can not be parsed as valid URL, "input"="ssh://host.xz:65536/path" + URL can not be parsed as valid URL, input="ssh://host.xz:65536/path" Caused by: 0: invalid port number - must be between 1-65535 1: number too large to fit in target type, - URL can not be parsed as valid URL, "input"="ssh://host.xz:99999/path" + URL can not be parsed as valid URL, input="ssh://host.xz:99999/path" Caused by: 0: invalid port number - must be between 1-65535 1: number too large to fit in target type, - URL can not be parsed as valid URL, "input"="git://host.xz:65536/path" + URL can not be parsed as valid URL, input="git://host.xz:65536/path" Caused by: 0: invalid port number - must be between 1-65535 @@ -136,15 +136,15 @@ fn host_with_space() { } insta::assert_debug_snapshot!(diagnostics, "host with space", @r#" [ - URL can not be parsed as valid URL, "input"="http://has a space" + URL can not be parsed as valid URL, input="http://has a space" Caused by: 0: invalid domain character, - URL can not be parsed as valid URL, "input"="http://has a space/path" + URL can not be parsed as valid URL, input="http://has a space/path" Caused by: 0: invalid domain character, - URL can not be parsed as valid URL, "input"="https://example.com with space/path" + URL can not be parsed as valid URL, input="https://example.com with space/path" Caused by: 0: invalid domain character, @@ -156,7 +156,7 @@ fn host_with_space() { fn url_with_space_in_path() { // Spaces in path should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://example.com/ path", true), "url with space in path", @r#" - URL can not be parsed as valid URL, "input"="http://example.com/ path" + URL can not be parsed as valid URL, input="http://example.com/ path" Caused by: 0: invalid domain character @@ -167,7 +167,7 @@ fn url_with_space_in_path() { fn url_with_space_in_username() { // Spaces in username should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://user name@example.com/path", true), "url with space in username", @r#" - URL can not be parsed as valid URL, "input"="http://user name@example.com/path" + URL can not be parsed as valid URL, input="http://user name@example.com/path" Caused by: 0: invalid domain character @@ -178,7 +178,7 @@ fn url_with_space_in_username() { fn url_with_space_in_password() { // Spaces in password should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://user:pass word@example.com/path", true), "url with space in password", @r#" - URL can not be parsed as valid URL, "input"="http://user:pass word@example.com/path" + URL can not be parsed as valid URL, input="http://user:pass word@example.com/path" Caused by: 0: invalid domain character @@ -189,7 +189,7 @@ fn url_with_space_in_password() { fn url_with_tab_in_path() { // Tabs in path should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://example.com/\tpath", true), "url with tab in path", @r#" - URL can not be parsed as valid URL, "input"="http://example.com/\tpath" + URL can not be parsed as valid URL, input="http://example.com/\tpath" Caused by: 0: invalid domain character @@ -200,7 +200,7 @@ fn url_with_tab_in_path() { fn url_with_newline_in_path() { // Newlines in path should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://example.com/\npath", true), "url with newline in path", @r#" - URL can not be parsed as valid URL, "input"="http://example.com/\npath" + URL can not be parsed as valid URL, input="http://example.com/\npath" Caused by: 0: invalid domain character @@ -211,7 +211,7 @@ fn url_with_newline_in_path() { fn url_with_tab_in_username() { // Tabs in username should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://user\tname@example.com/path", true), "url with tab in username", @r#" - URL can not be parsed as valid URL, "input"="http://user\tname@example.com/path" + URL can not be parsed as valid URL, input="http://user\tname@example.com/path" Caused by: 0: invalid domain character @@ -222,7 +222,7 @@ fn url_with_tab_in_username() { fn url_with_tab_in_password() { // Tabs in password should be rejected for http URLs per RFC 3986 insta::assert_debug_snapshot!(assert_validation("http://user:pass\tword@example.com/path", true), "url with tab in password", @r#" - URL can not be parsed as valid URL, "input"="http://user:pass\tword@example.com/path" + URL can not be parsed as valid URL, input="http://user:pass\tword@example.com/path" Caused by: 0: invalid domain character diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index 6036cad426e..a88d11f7fa9 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -403,11 +403,11 @@ fn filter_spawn_errors_are_not_collisions() -> Result { #[cfg(not(windows))] insta::assert_debug_snapshot!(error_snapshots, "filter spawn errors are not collisions", @r#" [ - Failed to spawn driver: "<filter-program>", "program"="<filter-program>" + Failed to spawn driver: "<filter-program>", program="<filter-program>" Caused by: 0: FilesystemLoop, - Failed to spawn driver: "<filter-program>", "program"="<filter-program>" + Failed to spawn driver: "<filter-program>", program="<filter-program>" Caused by: 0: FilesystemLoop, @@ -416,11 +416,11 @@ fn filter_spawn_errors_are_not_collisions() -> Result { #[cfg(windows)] insta::assert_debug_snapshot!(error_snapshots, "filter spawn errors are not collisions", @r#" [ - Failed to spawn driver: "<filter-program>", "program"="<filter-program>" + Failed to spawn driver: "<filter-program>", program="<filter-program>" Caused by: 0: PermissionDenied, - Failed to spawn driver: "<filter-program>", "program"="<filter-program>" + Failed to spawn driver: "<filter-program>", program="<filter-program>" Caused by: 0: PermissionDenied, diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index b71883e0aef..76b4fc600fe 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -183,7 +183,7 @@ pub(crate) mod key { } assert_eq!( error_with_value(&Core::BARE, "Invalid boolean", b"bad".as_bstr()).to_string(), - "Invalid boolean, \"input\"=\"bad\", \"key\"=\"core.bare\"", + "Invalid boolean, input=\"bad\", key=\"core.bare\"", "the standard message display includes its diagnostic metadata" ); } diff --git a/gix/src/config/tree/keys.rs b/gix/src/config/tree/keys.rs index c84cafbe1ab..32c41384cf1 100644 --- a/gix/src/config/tree/keys.rs +++ b/gix/src/config/tree/keys.rs @@ -145,7 +145,7 @@ impl<T: Validate> Key for Any<T> { if !err.metadata().any(|metadata| { matches!(metadata.get("input"), Some(gix_error::MetadataValue::Bytes(input)) if input == value) }) { - context.values.insert("input".into(), value.into()); + context.values.insert("input", value); } if let Some(environment) = self.environment_override() { context = context.with("environment_override", environment); diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index 2aa5cda15ca..bd27a918113 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -553,12 +553,12 @@ mod update { Caused by: 0: The reference at "refs/tags/malformed" could not be decoded - 1: Reference content could not be parsed, "input"="invalid", + 1: Reference content could not be parsed, input="invalid", Could not peel symbolic local reference to its ID Caused by: 0: The reference at "refs/tags/malformed" could not be decoded - 1: Reference content could not be parsed, "input"="invalid", + 1: Reference content could not be parsed, input="invalid", ] "#); Ok(()) @@ -616,11 +616,11 @@ mod update { Could not peel symbolic local reference to its ID Caused by: - 0: Could not peel reference to an object: object could not be found, "object_id"="Oid(1)", "reference"="refs/tags/missing", + 0: Could not peel reference to an object: object could not be found, object_id="Oid(1)", reference="refs/tags/missing", Could not peel symbolic local reference to its ID Caused by: - 0: Could not peel reference to an object: object could not be found, "object_id"="Oid(1)", "reference"="refs/tags/missing", + 0: Could not peel reference to an object: object could not be found, object_id="Oid(1)", reference="refs/tags/missing", ] "#); Ok(()) diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 0d685f8a2ec..04d2ed758b6 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -571,10 +571,10 @@ mod config_mut { .err() .expect("section filtering exposes the invalid CLI timeout in strict mode"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(temp.path()).to_string_lossy(), "<tmp>")]), "section filtering exposes the invalid CLI timeout in strict mode", @r#" - Invalid lock timeout, "key"="core.configLockTimeout" + Invalid lock timeout, key="core.configLockTimeout" Caused by: - 0: Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid" + 0: Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, input="invalid" "#); let options = options.strict_config(false); let err = gix::config_mut(Source::System, &options) @@ -627,10 +627,10 @@ mod config_mut { .err() .expect("disabling includes exposes the invalid physical timeout"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(temp.path()).to_string_lossy(), "<tmp>")]), "disabling includes exposes the invalid physical timeout", @r#" - Invalid lock timeout, "key"="core.configLockTimeout" + Invalid lock timeout, key="core.configLockTimeout" Caused by: - 0: Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, "input"="invalid" + 0: Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G, input="invalid" "#); Ok(()) } @@ -702,10 +702,10 @@ mod config_mut { .err() .expect("invalid sharing policies in overrides are rejected even for existing files"); insta::assert_debug_snapshot!(err, "invalid sharing policies in overrides are rejected even for existing files", @r#" - Invalid configuration value, "key"="core.sharedRepository" + Invalid configuration value, key="core.sharedRepository" Caused by: - 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid" + 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid" "#); Ok(()) } diff --git a/gix/tests/gix/clone.rs b/gix/tests/gix/clone.rs index 3d49fe1f958..dd29c502c44 100644 --- a/gix/tests/gix/clone.rs +++ b/gix/tests/gix/clone.rs @@ -860,7 +860,7 @@ mod blocking_io { ) .map(drop) .expect_err("an existing .git directory must not be reused for clone"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(fixture.path()).to_string_lossy(), "<fixture>")]), "fetch and checkout into non empty directory with existing dot git is rejected", @r#"Refusing to initialize an existing directory, "input"="<fixture>/non-empty-with-dot-git/.git""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(fixture.path()).to_string_lossy(), "<fixture>")]), "fetch and checkout into non empty directory with existing dot git is rejected", @r#"Refusing to initialize an existing directory, input="<fixture>/non-empty-with-dot-git/.git""#); assert!(matches!( err.classify().filter(|classification| classification.class() == gix_error::Class::Validation) @@ -1460,7 +1460,7 @@ fn clone_and_destination_must_be_empty() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "clone and destination must be empty", @r#" [ - Refusing to initialize the non-empty directory as, "input"="<destination>", + Refusing to initialize the non-empty directory as, input="<destination>", ] "#); Ok(()) @@ -1479,7 +1479,7 @@ fn clone_with_worktree_and_destination_must_be_empty() -> Result { ) .map(drop) .expect_err("this should fail as the directory isn't empty"); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(fixture.path()).to_string_lossy(), "<fixture>")]), "clone with worktree and destination must be empty", @r#"Refusing to initialize the non-empty directory as, "input"="<fixture>/non-empty""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(fixture.path()).to_string_lossy(), "<fixture>")]), "clone with worktree and destination must be empty", @r#"Refusing to initialize the non-empty directory as, input="<fixture>/non-empty""#); assert!(err.is_validation()); let validation = err .classify() diff --git a/gix/tests/gix/commit.rs b/gix/tests/gix/commit.rs index 00974f2d18a..00c0df623d1 100644 --- a/gix/tests/gix/commit.rs +++ b/gix/tests/gix/commit.rs @@ -239,7 +239,7 @@ mod signature { Could not verify the commit signature Caused by: - 0: Could not execute signature verifier, "program"="<home>/bin/missing-gpg" + 0: Could not execute signature verifier, program="<home>/bin/missing-gpg" 1: NotFound "#); Ok(()) diff --git a/gix/tests/gix/config/file_mut.rs b/gix/tests/gix/config/file_mut.rs index d43d24b73da..117235b09b3 100644 --- a/gix/tests/gix/config/file_mut.rs +++ b/gix/tests/gix/config/file_mut.rs @@ -301,7 +301,7 @@ fn semantic_validation_happens_on_reload() -> Result { Message { message: "Unsupported repository format version; only versions 0 and 1 are supported", class: Unsupported, - values: {"input": I64(2), "key": String("core.repositoryFormatVersion")}, + values: {input: I64(2), key: String("core.repositoryFormatVersion")}, } "#); assert_eq!( diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index 93ca5eba580..8495661772b 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -236,35 +236,35 @@ mod keys { Message { message: "Integers needs to be positive or negative numbers which may have a suffix like 1k, 42, or 50G", class: Validation, - values: {"input": Bytes("9223372036854775808")}, + values: {input: Bytes("9223372036854775808")}, } "#); assert!(err.is_validation()); insta::assert_debug_snapshot!(error_snapshots, "unsigned integer", @r#" [ - Invalid configuration value, "environment_override"="GIX_PACK_CACHE_MEMORY", "key"="core.deltaBaseCacheLimit" + Invalid configuration value, environment_override="GIX_PACK_CACHE_MEMORY", key="core.deltaBaseCacheLimit" Caused by: - 0: integer is out of range for `usize`, "input"="-1" + 0: integer is out of range for `usize`, input="-1" 1: out of range integral type conversion attempted, - Invalid configuration value, "environment_override"="GIX_PACK_CACHE_MEMORY", "key"="core.deltaBaseCacheLimit" + Invalid configuration value, environment_override="GIX_PACK_CACHE_MEMORY", key="core.deltaBaseCacheLimit" Caused by: - 0: integer is out of range for `usize`, "input"="-100" + 0: integer is out of range for `usize`, input="-100" 1: out of range integral type conversion attempted, ] "#); insta::assert_debug_snapshot!(diagnostics, "unsigned integer", @r#" [ - Invalid configuration value, "environment_override"="GIX_PACK_CACHE_MEMORY", "key"="core.deltaBaseCacheLimit" + Invalid configuration value, environment_override="GIX_PACK_CACHE_MEMORY", key="core.deltaBaseCacheLimit" Caused by: - 0: integer is out of range for `usize`, "input"="-1" + 0: integer is out of range for `usize`, input="-1" 1: out of range integral type conversion attempted, - Invalid configuration value, "environment_override"="GIX_PACK_CACHE_MEMORY", "key"="core.deltaBaseCacheLimit" + Invalid configuration value, environment_override="GIX_PACK_CACHE_MEMORY", key="core.deltaBaseCacheLimit" Caused by: - 0: integer is out of range for `usize`, "input"="-100" + 0: integer is out of range for `usize`, input="-100" 1: out of range integral type conversion attempted, ] "#); @@ -890,11 +890,11 @@ mod core { .validate("invalid".into()) .expect_err("the value is neither a boolean nor 'always'"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "log all ref updates", @r#" - Invalid configuration value, "key"="core.logAllRefUpdates" + Invalid configuration value, key="core.logAllRefUpdates" Caused by: - 0: Invalid configuration value, "key"="core.logAllRefUpdates" - 1: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid" + 0: Invalid configuration value, key="core.logAllRefUpdates" + 1: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid" "#); assert!(err.is_validation()); Ok(()) @@ -1272,7 +1272,7 @@ mod protocol { .expect_err("protocol permissions are case-sensitive"); assert_eq!( err.probable_cause().to_string(), - r#"Unknown protocol permission "User", "input"="User""#, + r#"Unknown protocol permission "User", input="User""#, "the configuration context preserves the parser's error" ); assert_eq!( diff --git a/gix/tests/gix/init.rs b/gix/tests/gix/init.rs index d3434bf1a46..29236d06a8f 100644 --- a/gix/tests/gix/init.rs +++ b/gix/tests/gix/init.rs @@ -49,7 +49,7 @@ mod bare { std::fs::write(tmp.path().join("existing.txt"), b"I was here before you")?; insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(gix::init_bare(tmp.path()) - .expect_err("init into non empty directory is not allowed")), &[(&(tmp.path()).to_string_lossy(), "<destination>")]), "init into non empty directory is not allowed", @r#"Refusing to initialize the non-empty directory as, "input"="<destination>""#); + .expect_err("init into non empty directory is not allowed")), &[(&(tmp.path()).to_string_lossy(), "<destination>")]), "init into non empty directory is not allowed", @r#"Refusing to initialize the non-empty directory as, input="<destination>""#); Ok(()) } } @@ -110,7 +110,7 @@ mod non_bare { ) .unwrap_err(); insta::assert_debug_snapshot!(err, "init bare rejects reserved branch name", @r#" - Invalid default branch name, "input"="HEAD" + Invalid default branch name, input="HEAD" Caused by: 0: Reference name is reserved and cannot be used: "refs/heads/HEAD" @@ -142,7 +142,7 @@ mod non_bare { ) .unwrap_err(); insta::assert_debug_snapshot!(err, "init bare rejects reserved fully qualified branch name", @r#" - Invalid default branch name, "input"="refs/heads/HEAD" + Invalid default branch name, input="refs/heads/HEAD" Caused by: 0: Reference name is reserved and cannot be used: "refs/heads/HEAD" @@ -220,7 +220,7 @@ mod non_bare { gix::open::Options::isolated(), ) .unwrap_err(); - insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(tmp.path()).to_string_lossy(), "<destination>")]), "init into non empty directory is not allowed if option is true", @r#"Refusing to initialize the non-empty directory as, "input"="<destination>""#); + insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(tmp.path()).to_string_lossy(), "<destination>")]), "init into non empty directory is not allowed if option is true", @r#"Refusing to initialize the non-empty directory as, input="<destination>""#); Ok(()) } } diff --git a/gix/tests/gix/reference/mod.rs b/gix/tests/gix/reference/mod.rs index ca816a54cda..054231787c0 100644 --- a/gix/tests/gix/reference/mod.rs +++ b/gix/tests/gix/reference/mod.rs @@ -303,7 +303,7 @@ fn set_target_id() { .set_target_id(prev_id, "fails") .expect_err("the reference was deleted"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[]), "updating a deleted reference requires reconciling its absence", @r#" - Could not prepare reference edit, "reference"="refs/heads/main", "referent"="refs/heads/main" + Could not prepare reference edit, reference="refs/heads/main" Caused by: 0: The reference must exist with content Oid(1) diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index 677d985ad65..0f0842d0eda 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -128,8 +128,8 @@ mod blocking_io { } insta::assert_debug_snapshot!(error_snapshots, "deny", @r#" [ - Protocol File is denied per configuration, "input"="<fixture>/base", - Protocol File is denied per configuration, "input"="<fixture>/base", + Protocol File is denied per configuration, input="<fixture>/base", + Protocol File is denied per configuration, input="<fixture>/base", ] "#); } diff --git a/gix/tests/gix/repository/config/mod.rs b/gix/tests/gix/repository/config/mod.rs index ad7087ef8fd..a23c484a3f7 100644 --- a/gix/tests/gix/repository/config/mod.rs +++ b/gix/tests/gix/repository/config/mod.rs @@ -68,28 +68,28 @@ fn invalid_stat_boolean_is_validation_error() -> Result { [ ( "core.trustCTime", - Invalid boolean, "key"="core.trustCTime" + Invalid boolean, key="core.trustCTime" Caused by: - 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", + 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid", ), ( "gitoxide.core.useNsec", - Invalid boolean, "key"="gitoxide.core.useNsec" + Invalid boolean, key="gitoxide.core.useNsec" Caused by: - 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", + 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid", ), ( "gitoxide.core.useStdev", - Invalid boolean, "key"="gitoxide.core.useStdev" + Invalid boolean, key="gitoxide.core.useStdev" Caused by: - 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, "input"="invalid", + 0: Booleans need to be 'no', 'off', 'false', '' or 'yes', 'on', 'true' or any number, input="invalid", ), ( "core.checkStat", - Invalid configuration value, "input"="invalid", "key"="core.checkStat", + Invalid configuration value, input="invalid", key="core.checkStat", ), ] "#); diff --git a/gix/tests/gix/repository/object.rs b/gix/tests/gix/repository/object.rs index 584ef90fe87..b8f201c6340 100644 --- a/gix/tests/gix/repository/object.rs +++ b/gix/tests/gix/repository/object.rs @@ -318,7 +318,7 @@ mod write_object { I/O error (Other) Caused by: - 0: Signature name or email must not contain '<', '>' or \n, "input"="1 < 0" + 0: Signature name or email must not contain '<', '>' or \n, input="1 < 0" "#); Ok(()) } @@ -745,7 +745,7 @@ mod commit { .commit("HEAD", "initial", empty_tree_id, [empty_tree_id]) .expect_err("an initial commit cannot have a parent"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[]), "the expected previous reference value is absent", @r#" - Could not prepare reference edit, "reference"="HEAD", "referent"="refs/heads/main" + Could not prepare reference edit, reference="HEAD", referent="refs/heads/main" Caused by: 0: The reference must exist with content Oid(1) diff --git a/gix/tests/gix/repository/open.rs b/gix/tests/gix/repository/open.rs index 8f49fa1d277..e035a48f2e7 100644 --- a/gix/tests/gix/repository/open.rs +++ b/gix/tests/gix/repository/open.rs @@ -263,7 +263,7 @@ fn git_index_file_empty_is_invalid_even_with_lenient_config() -> Result { .expect_err("an empty index path must be rejected"); insta::assert_debug_snapshot!(err, "an empty index path is never ignored, even though configuration is lenient by default", @r#" - Invalid configuration value, "environment_override"="GIT_INDEX_FILE", "input"="", "key"="gitoxide.core.indexFile" + Invalid configuration value, environment_override="GIT_INDEX_FILE", input="", key="gitoxide.core.indexFile" Caused by: 0: index file path must not be empty @@ -510,7 +510,7 @@ fn non_bare_split_worktree_invalid_worktree_path_boolean() -> Result { ) .expect_err("a bare worktree-path key is invalid in strict mode"); assert!(err.is_validation(), "in strict mode, we fail just like git does"); - insta::assert_debug_snapshot!(err, "non bare split worktree invalid worktree path boolean", @r#"Invalid configuration value, "environment_override"="GIT_WORK_TREE", "key"="core.worktree""#); + insta::assert_debug_snapshot!(err, "non bare split worktree invalid worktree path boolean", @r#"Invalid configuration value, environment_override="GIT_WORK_TREE", key="core.worktree""#); Ok(()) } @@ -524,7 +524,7 @@ fn non_bare_split_worktree_invalid_worktree_path_empty() -> Result { ) .unwrap_err(); insta::assert_debug_snapshot!(err, "DEVIATION: could not read path at core.worktree as empty is always invalid, git tries to use an empty path, even though it's better to reject it", @r#" - The path at the 'core.worktree' configuration could not be interpolated, "input"="" + The path at the 'core.worktree' configuration could not be interpolated, input="" Caused by: 0: path is missing @@ -542,7 +542,7 @@ fn non_bare_split_worktree_invalid_worktree_path_empty() -> Result { Message { message: "The path at the 'core.worktree' configuration could not be interpolated", class: Validation, - values: {"input": Bytes("")}, + values: {input: Bytes("")}, } "#); Ok(()) @@ -712,7 +712,7 @@ mod object_format_extension { Message { message: "Unsupported repository format version; only versions 0 and 1 are supported", class: Unsupported, - values: {"input": I64(2), "key": String("core.repositoryFormatVersion")}, + values: {input: I64(2), key: String("core.repositoryFormatVersion")}, } "#); Ok(()) diff --git a/gix/tests/gix/repository/remote.rs b/gix/tests/gix/repository/remote.rs index bb3c2a8fbb2..f3a6fe117dc 100644 --- a/gix/tests/gix/repository/remote.rs +++ b/gix/tests/gix/repository/remote.rs @@ -464,7 +464,7 @@ mod find_remote { The rewritten fetch url ":://gitoxide" failed to parse Caused by: - 0: URL can not be parsed as valid URL, "input"=":://gitoxide" + 0: URL can not be parsed as valid URL, input=":://gitoxide" 1: relative URL without a base "#); assert_eq!( @@ -537,7 +537,7 @@ mod find_remote { The rewritten push url ":://repo" failed to parse Caused by: - 0: URL can not be parsed as valid URL, "input"=":://repo" + 0: URL can not be parsed as valid URL, input=":://repo" 1: relative URL without a base "#); assert_eq!( @@ -558,7 +558,7 @@ mod find_remote { The rewritten push url ":://repo" failed to parse Caused by: - 0: URL can not be parsed as valid URL, "input"=":://repo" + 0: URL can not be parsed as valid URL, input=":://repo" 1: relative URL without a base "#); @@ -567,7 +567,7 @@ mod find_remote { The rewritten push url ":://repo" failed to parse Caused by: - 0: URL can not be parsed as valid URL, "input"=":://repo" + 0: URL can not be parsed as valid URL, input=":://repo" 1: relative URL without a base "#); diff --git a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs index a08bb614923..457ed4cbabc 100644 --- a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs @@ -64,12 +64,12 @@ fn prefix() -> Result { assert!( source.downcast_any_ref::<gix_error::Message>().is_some(), "the owned lookup error retains concrete causes" - ); + ); } insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&format_args!("{}", normalize_repo_path(&format!("{err:#?}"), &repo)), &[]), "ambiguous prefixes retain the lookup failure for each malformed candidate", @" Short id bad0 is ambiguous. Candidates are: - \tbad0853 lookup error: Could not read loose object, \"path\"=\"$GIT_DIR/objects/ba/d0853730d9d114ac789f0ce89039d224bf66c9\" - \tbad0bd4 lookup error: Could not read loose object, \"path\"=\"$GIT_DIR/objects/ba/d0bd4672dee1b4d3b8088534ed5a0362bc8d59\" + \tbad0853 lookup error: Could not read loose object, path=\"$GIT_DIR/objects/ba/d0853730d9d114ac789f0ce89039d224bf66c9\" + \tbad0bd4 lookup error: Could not read loose object, path=\"$GIT_DIR/objects/ba/d0bd4672dee1b4d3b8088534ed5a0362bc8d59\" "); }; insta::assert_debug_snapshot!(error_snapshots, "ambiguous object prefixes list the matching candidates", @r#" @@ -87,7 +87,7 @@ fn fully_failed_disambiguation_still_yields_an_ambiguity_error() -> Result { let err = parse_spec("0000000000^{tag}", &repo).expect_err("none of the candidates can peel to a tag"); insta::assert_debug_snapshot!(err, "candidate origins distinguish failures that reach the same object", @r#" - delegate.peel_until(ObjectKind(Tag)) failed, "input"="{tag}" + delegate.peel_until(ObjectKind(Tag)) failed, input="{tag}" Caused by: 0: Short id 0000000000 is ambiguous. Candidates are: diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index 4d4d56303c1..68771cac6ce 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -189,7 +189,7 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - .rev_parse("README.md") .expect_err("missing revspec must fail before callers can inspect the error chain"); insta::assert_debug_snapshot!(err, "rev-parse preserves the reference lookup classification", @r#" - couldn't parse revision, "input"="README.md" + couldn't parse revision, input="README.md" Caused by: 0: Reference README.md could not be found @@ -400,7 +400,7 @@ fn missing_objects_are_classified_without_a_missing_reference() -> Result { } insta::assert_debug_snapshot!(error_snapshots, "missing objects are classified without a missing reference", @r#" [ - delegate.peel_until(ValidObject) failed, "input"="{object}" + delegate.peel_until(ValidObject) failed, input="{object}" Caused by: 0: An object with id Oid(1) could not be found, @@ -412,7 +412,7 @@ fn missing_objects_are_classified_without_a_missing_reference() -> Result { Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Could not peel reference to an object: object could not be found, "object_id"="Oid(1)", "reference"="refs/heads/missing-object", + 1: Could not peel reference to an object: object could not be found, object_id="Oid(1)", reference="refs/heads/missing-object", ] "#); Ok(()) @@ -463,12 +463,12 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { "Now we enforce the object to exist and be valid, as ultimately it wants to match with a certain type" ); insta::assert_snapshot!(normalize_repo_path(&format!("{err:#?}"), &repo), @r#" - delegate.peel_until(ValidObject) failed, "input"="{object}" + delegate.peel_until(ValidObject) failed, input="{object}" Caused by: - 0: Could not read loose object, "path"="$GIT_DIR/objects/e3/2851d29feb48953c6f40b2e06d630a3c49608a" + 0: Could not read loose object, path="$GIT_DIR/objects/e3/2851d29feb48953c6f40b2e06d630a3c49608a" 1: The object header contained an unknown object kind. - 2: Unknown object kind, "input"="bad" + 2: Unknown object kind, input="bad" "#); } @@ -480,10 +480,10 @@ fn bad_objects_are_valid_until_they_are_actually_read_from_the_odb() { ); let err = parse_spec("cafea^{object}", &repo).unwrap_err(); insta::assert_snapshot!(normalize_repo_path(&format!("{err:#?}"), &repo), @r#" - delegate.peel_until(ValidObject) failed, "input"="{object}" + delegate.peel_until(ValidObject) failed, input="{object}" Caused by: - 0: Could not read loose object, "path"="$GIT_DIR/objects/ca/fea31147e840161a1860c50af999917ae1536b" + 0: Could not read loose object, path="$GIT_DIR/objects/ca/fea31147e840161a1860c50af999917ae1536b" 1: Could not decode zip stream 2: Invalid input data "#); diff --git a/gix/tests/gix/revision/spec/from_bytes/traverse.rs b/gix/tests/gix/revision/spec/from_bytes/traverse.rs index f52b0a5ab89..7221db0a475 100644 --- a/gix/tests/gix/revision/spec/from_bytes/traverse.rs +++ b/gix/tests/gix/revision/spec/from_bytes/traverse.rs @@ -39,7 +39,7 @@ fn freestanding_negation_yields_descriptive_error() -> Result { "#); let err = parse_spec("^!", &repo).unwrap_err(); insta::assert_debug_snapshot!(err, @r#" - couldn't parse revision, "input"="!" + couldn't parse revision, input="!" Caused by: 0: Reference ! could not be found diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index aac0b5aa03a..a4a2ec76ff6 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -45,7 +45,7 @@ mod open { .open() .expect_err("invalid core.worktree configuration must propagate"); insta::assert_debug_snapshot!(err, "the nested missing-path error must not hide the configuration failure", @r#" - The path at the 'core.worktree' configuration could not be interpolated, "input"="" + The path at the 'core.worktree' configuration could not be interpolated, input="" Caused by: 0: path is missing @@ -429,7 +429,7 @@ mod open { The gitdir file at "<repository>/m1/.git" contains an invalid gitdir target Caused by: - 0: Format should be 'gitdir: <path>', but got, "input"="bogus\n" + 0: Format should be 'gitdir: <path>', but got, input="bogus\n" "#); assert!(err.is_validation()); @@ -442,7 +442,7 @@ mod open { The gitdir file at "<repository>/m1/.git" contains an invalid gitdir target Caused by: - 0: Format should be 'gitdir: <path>', but got, "input"="bogus\n" + 0: Format should be 'gitdir: <path>', but got, input="bogus\n" "#); assert!(err.is_validation()); @@ -873,7 +873,7 @@ mod advisory { .next() .expect("one submodule"); let err = sm.update().expect_err("commands from `.gitmodules` are forbidden"); - insta::assert_debug_snapshot!(err, "update commands from gitmodules are rejected after init", @r#"The 'update' field of submodule 'sub' tried to set a command to be shared, "input"="touch pwned""#); + insta::assert_debug_snapshot!(err, "update commands from gitmodules are rejected after init", @r#"The 'update' field of submodule 'sub' tried to set a command to be shared, input="touch pwned""#); assert_eq!( err.metadata().find_map(|metadata| metadata.get("input")), Some(&gix_error::MetadataValue::Bytes("touch pwned".into())) diff --git a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure index d7f531a9ae6..9477e92963f 100644 --- a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure +++ b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure @@ -1,4 +1,4 @@ -Error: couldn't parse revision, "input"="foobar" +Error: couldn't parse revision, input="foobar" Caused by: 0: Reference foobar could not be found diff --git a/tests/snapshots/porcelain/init/fail b/tests/snapshots/porcelain/init/fail index 854b2eb729e..4f21bab6107 100644 --- a/tests/snapshots/porcelain/init/fail +++ b/tests/snapshots/porcelain/init/fail @@ -1,4 +1,4 @@ Error: Repository initialization failed Caused by: - 0: Refusing to initialize an existing directory, "input"=".git" \ No newline at end of file + 0: Refusing to initialize an existing directory, input=".git" \ No newline at end of file diff --git a/tests/snapshots/porcelain/init/fail-with-multi-element-directory b/tests/snapshots/porcelain/init/fail-with-multi-element-directory index 57d16c59f2e..e7e9d9e8f21 100644 --- a/tests/snapshots/porcelain/init/fail-with-multi-element-directory +++ b/tests/snapshots/porcelain/init/fail-with-multi-element-directory @@ -1,4 +1,4 @@ Error: Repository initialization failed Caused by: - 0: Refusing to initialize an existing directory, "input"="foo/bar/.git" \ No newline at end of file + 0: Refusing to initialize an existing directory, input="foo/bar/.git" \ No newline at end of file From d2b1a85518c18b65475d3594d500407ddedca96e Mon Sep 17 00:00:00 2001 From: Codex GPT-6 <codex@openai.com> Date: Wed, 7 Oct 2026 21:40:35 +0200 Subject: [PATCH 19/28] fixup! change(gix-error)!: wrap `Metadata` and simplify diagnostic keys The `test-fast (ubuntu-latest)` CI job fails in `continuing_a_conflict_commits_the_complete_resolved_index`: its assertion searches for the old quoted `"exit_status"=` metadata key and finds zero occurrences after simple diagnostic keys became unquoted. Update the expected key to `exit_status=`. This preserves the check that command status is rendered exactly once and lets the existing regression continue validating the complete staged conflict resolution. Validation: the regression failed before the change and passes with `GIX_TEST_IGNORE_ARCHIVES=1 cargo test -p gix-tix --features sha1 --lib continuing_a_conflict_commits_the_complete_resolved_index`. `cargo fmt -p gix-tix -- --check` and `git diff --check` pass. --- gix-tix/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gix-tix/src/lib.rs b/gix-tix/src/lib.rs index 7969ff14e4c..5d065ce5215 100644 --- a/gix-tix/src/lib.rs +++ b/gix-tix/src/lib.rs @@ -14931,7 +14931,7 @@ mod tests { ); let rendered = diagnostic.to_string(); assert_eq!( - rendered.matches("\"exit_status\"=").count(), + rendered.matches("exit_status=").count(), 1, "status appears only in metadata" ); From 50b9a46ebadbe58e4cae04414022b8388c51465e Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 6 Oct 2026 08:31:37 +0200 Subject: [PATCH 20/28] fix(gix-error): preserve explicit causes in standard error source chains <!-- agent --> Returning a child frame's bare diagnostic from `Error::source()` lost any explicit causes attached to that frame. Standard error reporters, including `anyhow`, could truncate nested context chains before reaching the underlying error. Keep nonleaf frames reachable as source boundaries while exposing raw leaf diagnostics for downcasting. Format each boundary as a single diagnostic to avoid duplicating subtrees in external reports, and preserve native-source precedence without another allocation. Add regression coverage for standard and `anyhow` traversal and rendering, erasure and boxing round trips, and preservation of typed diagnostics, classification, and metadata. Document linear source traversal and the full-tree inspection alternatives. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- gix-error/src/error.rs | 57 ++++++---- gix-error/src/exn/impls.rs | 121 ++++++++++++++------ gix-error/src/lib.rs | 7 ++ gix-error/tests/error/convert.rs | 189 +++++++++++++++++++++++++++++++ 4 files changed, 317 insertions(+), 57 deletions(-) diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index 0c221fb574b..c4c5b97b808 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -242,7 +242,7 @@ impl<E: std::error::Error + Send + Sync + 'static> crate::Exn<E> { /// As with [`crate::Error::classify()`], unknown errors are omitted, classifications aren't deduplicated, and each /// item retains the classified error for downcasting and origin inspection. pub fn classify(&self) -> Classifications<'_> { - Classifications(Errors::new(Node::Frame(self.frame()))) + Classifications(Errors::new(Node::Frame(self.frame().source_frame()))) } classification_predicates!(); @@ -314,14 +314,11 @@ pub struct Classification<'a> { /// assert!(gix_error::classify(&error).is_not_found()); /// ``` pub fn classify<'a>(err: &'a (dyn std::error::Error + 'static)) -> Classifications<'a> { - Classifications(Errors::new(err.downcast_ref::<crate::Error>().map_or( - Node::Source { - error: err, - location: None, - source_owner: None, - }, - crate::Error::iter_root, - ))) + Classifications(Errors::new(Node::boundary(err).unwrap_or(Node::Source { + error: err, + location: None, + source_owner: None, + }))) } /// A lazy iterator over classified causes. Its predicates consume the remaining iterator. @@ -555,7 +552,7 @@ fn node_can_retry_lenient(node: Node<'_>) -> bool { #[derive(Clone, Copy)] enum Node<'a> { - Frame(&'a crate::exn::Frame), + Frame(&'a crate::exn::impls::FrameSource), Source { error: &'a (dyn std::error::Error + 'static), location: Option<&'static std::panic::Location<'static>>, @@ -571,6 +568,14 @@ enum Node<'a> { } impl<'a> Node<'a> { + fn boundary(error: &'a (dyn std::error::Error + 'static)) -> Option<Self> { + if let Some(error) = error.downcast_ref::<crate::Error>() { + Some(error.iter_root()) + } else { + error.downcast_ref::<crate::exn::impls::FrameSource>().map(Node::Frame) + } + } + fn display(self) -> DisplaySource<'a> { let (error, location) = match self { Node::Frame(frame) => ( @@ -656,14 +661,18 @@ impl<'a> Errors<'a> { error: &'a (dyn std::error::Error + 'static), location: Option<&'static std::panic::Location<'static>>, ) { - if let Some(error) = error.downcast_ref::<crate::Error>() { - self.pending.push_back(error.iter_root()); + if let Some(node) = Node::boundary(error) { + self.pending.push_back(node); } else if let Some(source) = native_source(error) { - self.pending.push_back(Node::Source { - error: source, - location: location.filter(|_| is_transparent_marker(error)), - source_owner: Some(error), - }); + self.pending + .push_back(source.downcast_ref::<crate::exn::impls::FrameSource>().map_or( + Node::Source { + error: source, + location: location.filter(|_| is_transparent_marker(error)), + source_owner: Some(error), + }, + Node::Frame, + )); } } @@ -671,7 +680,8 @@ impl<'a> Errors<'a> { match node { Node::Frame(frame) => { self.source(frame.error(), Some(frame.location())); - self.pending.extend(frame.children().iter().map(Node::Frame)); + self.pending + .extend(frame.children().iter().map(|frame| Node::Frame(frame.source_frame()))); } Node::Source { error, location, .. } => self.source(error, location), #[cfg(all(feature = "auto-chain-error", not(feature = "tree-error")))] @@ -728,11 +738,11 @@ impl<'a> Iterator for Errors<'a> { impl crate::exn::Frame { pub(crate) fn probable_cause_inner(&self) -> Option<&(dyn std::error::Error + 'static)> { - Node::Frame(self).probable_cause() + Node::Frame(self.source_frame()).probable_cause() } pub(crate) fn iter_errors_with_locations(&self) -> impl Iterator<Item = DisplaySource<'_>> + '_ { - Errors::new(Node::Frame(self)) + Errors::new(Node::Frame(self.source_frame())) .map(Node::display) .filter(|source| !is_transparent_marker(source.error)) } @@ -763,7 +773,7 @@ mod _impl { } pub(super) fn iter_root(&self) -> super::Node<'_> { - super::Node::Frame(self.inner.frame()) + super::Node::Frame(self.inner.frame().source_frame()) } } @@ -817,10 +827,7 @@ mod _impl { impl std::error::Error for Error { /// Return the first source of an [Exn] error, or the source of a boxed error. fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match &self.inner { - Inner::ExnAsError(frame) | Inner::Exn(frame) => super::native_source(frame.error()) - .or_else(|| frame.children().first().map(|frame| frame.error() as _)), - } + std::error::Error::source(self.inner.frame().source_frame()) } } diff --git a/gix-error/src/exn/impls.rs b/gix-error/src/exn/impls.rs index c26d57973e2..23f8128fb68 100644 --- a/gix-error/src/exn/impls.rs +++ b/gix-error/src/exn/impls.rs @@ -115,9 +115,11 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { #[track_caller] pub fn new(error: E) -> Self { let frame = Frame { - error: Box::new(error), - location: Location::caller(), - children: Vec::new(), + source: FrameSource { + error: Box::new(error), + location: Location::caller(), + children: Vec::new(), + }, }; Self { @@ -130,7 +132,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { pub(super) fn with_cause(cause: impl Error + Send + Sync + 'static, error: E) -> Self { let cause = into_frame(cause); let mut exn = Exn::new(error); - exn.frame.children.push(*cause); + exn.frame.source.children.push(*cause); exn } @@ -145,7 +147,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { let mut new_exn = Exn::new(err); for exn in children { let exn = exn.into(); - new_exn.frame.children.push(*exn.frame); + new_exn.frame.source.children.push(*exn.frame); } new_exn } @@ -154,7 +156,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { #[track_caller] pub fn raise<T: Error + Send + Sync + 'static>(self, err: T) -> Exn<T> { let mut new_exn = Exn::new(err); - new_exn.frame.children.push(*self.frame); + new_exn.frame.source.children.push(*self.frame); new_exn } @@ -162,7 +164,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { #[track_caller] pub fn chain<T: Error + Send + Sync + 'static>(mut self, err: impl Into<Exn<T>>) -> Exn<E> { let err = err.into(); - self.frame.children.push(*err.frame); + self.frame.source.children.push(*err.frame); self } @@ -176,7 +178,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { { for err in errors { let err = err.into(); - self.frame.children.push(*err.frame); + self.frame.source.children.push(*err.frame); } self } @@ -186,7 +188,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { /// Native [`Error::source()`] values remain owned by their error and aren't drainable frames. This is useful if one /// wants to re-organise explicitly raised errors and the error layout is well known. pub fn drain_children(&mut self) -> impl Iterator<Item = Exn> + '_ { - self.frame.children.drain(..).map(Exn::from) + self.frame.source.children.drain(..).map(Exn::from) } /// Erase the type of this instance and turn it into a bare `Exn`. @@ -198,6 +200,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { /// Return the current exception. pub fn error(&self) -> &E { self.frame + .source .error .downcast_ref() .expect("the owned frame always matches the compile-time error type") @@ -208,7 +211,7 @@ impl<E: Error + Send + Sync + 'static> Exn<E> { /// This is useful to retain the allocation, as internally it's also stored in a box, /// when comparing it to [`Self::into_inner()`]. pub fn into_box(self) -> Box<E> { - match self.frame.error.downcast() { + match self.frame.source.error.downcast() { Ok(err) => err, Err(_) => unreachable!("The type in the frame is always the type of this instance"), } @@ -474,20 +477,46 @@ impl fmt::Display for Frame { /// A frame in the exception tree. pub struct Frame { - /// The error that occurred at this frame. + source: FrameSource, +} + +/// The owning frame contents, with single-diagnostic formatting for standard source-chain consumers. +/// Keeping this separate from `Frame` preserves its existing full-tree formatting without another allocation. +pub(crate) struct FrameSource { error: Box<dyn Error + Send + Sync + 'static>, - /// The source code location where this exception frame was created. location: &'static Location<'static>, - /// Explicitly raised child exception frames. children: Vec<Frame>, } -impl Frame { - /// Return the error as a reference to [`Error`]. - /// - /// If the error was [erased](crate::Exn::erased), this is the original error, - /// so it can still be downcast to its actual type. - pub fn error(&self) -> &(dyn Error + Send + Sync + 'static) { +impl fmt::Display for FrameSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Do not forward alternate formatting: a nested public Error could otherwise print its entire subtree. + write!(f, "{}", self.error()) + } +} + +impl fmt::Debug for FrameSource { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.error(), f) + } +} + +impl Error for FrameSource { + fn source(&self) -> Option<&(dyn Error + 'static)> { + crate::error::native_source(self.error()).or_else(|| { + self.children.first().map(|frame| { + if frame.children().is_empty() { + frame.error() as &(dyn Error + 'static) + } else { + &frame.source as &(dyn Error + 'static) + } + }) + }) + } +} + +impl FrameSource { + pub(crate) fn error(&self) -> &(dyn Error + Send + Sync + 'static) { let mut error = &*self.error; loop { if let Some(erased) = error.downcast_ref::<Untyped>() { @@ -500,9 +529,27 @@ impl Frame { } } + pub(crate) fn location(&self) -> &'static Location<'static> { + self.location + } + + pub(crate) fn children(&self) -> &[Frame] { + &self.children + } +} + +impl Frame { + /// Return the error as a reference to [`Error`]. + /// + /// If the error was [erased](crate::Exn::erased), this is the original error, + /// so it can still be downcast to its actual type. + pub fn error(&self) -> &(dyn Error + Send + Sync + 'static) { + self.source.error() + } + /// Return the source code location where this exception frame was created. pub fn location(&self) -> &'static Location<'static> { - self.location + self.source.location() } /// Return explicitly raised child frames. @@ -510,7 +557,11 @@ impl Frame { /// Native [`Error::source()`] values are borrowed from [`Self::error()`] and traversed lazily, so they aren't owned /// `Frame` children. pub fn children(&self) -> &[Frame] { - &self.children + self.source.children() + } + + pub(crate) fn source_frame(&self) -> &FrameSource { + &self.source } } @@ -555,7 +606,7 @@ impl<'a> ErrorNode<'a> { /// source chain, providing formatting context even though no location was captured for the source itself. pub(crate) fn location(self) -> &'static Location<'static> { match self { - ErrorNode::Frame(frame) => frame.location, + ErrorNode::Frame(frame) => frame.location(), ErrorNode::Source { location, .. } | ErrorNode::FlatSource { location, .. } => location, } } @@ -594,7 +645,7 @@ impl<'a> ErrorNode<'a> { children.push(ErrorNode::Source { error, location }); } if let ErrorNode::Frame(frame) = self { - children.extend(frame.children.iter().map(ErrorNode::Frame)); + children.extend(frame.children().iter().map(ErrorNode::Frame)); } let mut diagnostics = Vec::new(); for child in children { @@ -706,8 +757,8 @@ impl From<Frame> for Exn { impl Exn { pub(crate) fn from_boxed_frame(mut frame: Box<Frame>) -> Self { - if !frame.error.is::<Untyped>() { - frame.error = Box::new(Untyped(frame.error)); + if !frame.source.error.is::<Untyped>() { + frame.source.error = Box::new(Untyped(frame.source.error)); } Exn { frame, @@ -724,21 +775,24 @@ impl Exn { while let Some(node) = next { next = node.source.map(|source| *source); let frame = (!node.err.is_native_source()).then(|| Frame { - error: node.err.into_owned_error(), - location: node.location, - children: Vec::new(), + source: FrameSource { + error: node.err.into_owned_error(), + location: node.location, + children: Vec::new(), + }, }); frames.push((frame, node.logical_parent)); } // Native sources remain owned by their explicit frame; only those frames are rebuilt. while let Some((frame, parent)) = frames.pop() { let Some(mut frame) = frame else { continue }; - frame.children.reverse(); + frame.source.children.reverse(); match parent { Some(parent) => frames[parent] .0 .as_mut() .expect("an explicit frame has an explicit parent") + .source .children .push(frame), None => return frame.into(), @@ -835,9 +889,12 @@ fn flatten_error_nodes(root: Frame) -> Vec<OwnedErrorNode> { Pending::Frame { frame: Frame { - error, - location, - children, + source: + FrameSource { + error, + location, + children, + }, }, logical_parent, } => { diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 5f09ac81e60..059bce5b919 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -587,6 +587,13 @@ pub use exn::{ /// Use [`Error::downcast_any_ref()`] or [`Error::iter_errors()`] to inspect the original types, including sources /// within nested [`Error`] values. This also applies when the `auto-chain-error` feature is enabled. /// +/// In tree mode, standard `source()` traversal prefers the stored error's native source; otherwise it follows the +/// first explicitly raised child. Nonleaf explicit children are exposed through owning source boundaries so traversal +/// retains their descendants. These boundaries display only their current diagnostic, including with alternate Display, +/// while explicit leaves and native sources retain their raw concrete payloads. Raw standard-source downcasts can thus +/// encounter wrappers; use [`Error::downcast_any_ref()`] or [`Error::iter_errors()`] for typed inspection of the complete +/// tree. Standard traversal follows one path, not every branch; use [`Exn::into_chain()`] for a flattened source chain. +/// /// # The `auto-chain-error` feature /// /// If it's enabled, this type is merely a wrapper around [`ChainedError`](types::ChainedError). This happens automatically diff --git a/gix-error/tests/error/convert.rs b/gix-error/tests/error/convert.rs index fa8afd7aff1..9e45b5a9bf8 100644 --- a/gix-error/tests/error/convert.rs +++ b/gix-error/tests/error/convert.rs @@ -503,3 +503,192 @@ fn exception_conversions_preserve_original_caller_locations() { check(make_exception); check(|| make_exception().erased()); } + +#[test] +fn standard_sources_retain_explicit_contexts_and_the_typed_actual_diagnostic() { + use gix_error::{Class, ClassificationMarker}; + + type Actual = crate::ErrorWithSource<ClassificationMarker>; + let make_exception = || { + crate::ErrorWithSource("actual content", ClassificationMarker::CONFLICT) + .raise_typed() + .raise(message("expected content")) + .raise(message("edit failed")) + .raise(message("operation failed").with("operation", "compare")) + }; + let expected = [ + "operation failed, operation=\"compare\"", + "edit failed", + "expected content", + "actual content", + "Conflict", + ]; + for error in [ + make_exception().into_error(), + make_exception().erased().into_error(), + Error::from_boxed(Box::new(make_exception().into_error())), + make_exception().into_error().into_exn().into_error(), + ] { + let root: &dyn std::error::Error = &error; + let sources = std::iter::successors(Some(root), |error| error.source()).collect::<Vec<_>>(); + assert_eq!( + sources + .iter() + .map(|error| without_source_location(error.to_string())) + .collect::<Vec<_>>(), + expected, + "standard traversal retains every explicit context, the actual diagnostic, and its native marker" + ); + for (source, expected) in sources[1..].iter().zip(&expected[1..]) { + assert_eq!( + format!("{source:#}"), + *expected, + "alternate source Display formats only the current diagnostic, not its descendants" + ); + } + let actual = error + .downcast_any_ref::<Actual>() + .expect("the typed actual diagnostic survives"); + let classification = error.classify().next().expect("the conflict remains classified"); + assert_eq!( + classification.class(), + Class::Conflict, + "the intrinsic recovery class is unchanged" + ); + assert!( + std::ptr::eq( + classification + .error() + .downcast_ref::<Actual>() + .expect("the typed classification subject"), + actual + ), + "classification identifies the original actual diagnostic, not a source wrapper" + ); + assert!( + std::ptr::eq( + error + .probable_cause() + .downcast_ref::<Actual>() + .expect("the typed probable cause"), + actual + ), + "source interoperability does not change probable-cause identity" + ); + assert_eq!(error.metadata().count(), 1, "context metadata is retained exactly once"); + #[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] + { + assert!(sources[3].is::<Actual>(), "explicit leaves remain raw concrete sources"); + let borrowed = gix_error::classify(sources[1]) + .next() + .expect("a borrowed source boundary remains classified"); + assert!( + std::ptr::eq( + borrowed + .error() + .downcast_ref::<Actual>() + .expect("the borrowed typed subject"), + actual + ), + "classifying a standard source boundary preserves the original subject" + ); + } + } +} + +#[cfg(feature = "anyhow")] +#[test] +fn anyhow_wrapping_public_errors_retains_the_deepest_actual_diagnostic() { + let make_public = || { + message("actual content") + .and_raise(message("expected content")) + .and_raise(message("edit failed")) + .and_raise(message("operation failed")) + }; + for public in [make_public(), Error::from_error(make_public())] { + let error = anyhow::Error::new(public); + assert!( + error.downcast_ref::<Error>().is_some(), + "anyhow retains the public error boundary" + ); + assert_eq!( + error + .chain() + .map(|error| without_source_location(error.to_string())) + .collect::<Vec<_>>(), + ["operation failed", "edit failed", "expected content", "actual content"], + "anyhow traverses the public error through every explicit context to the deepest diagnostic" + ); + for report in [format!("{error:?}"), format!("{error:#}")] { + for diagnostic in ["operation failed", "edit failed", "expected content", "actual content"] { + assert_eq!( + report.matches(diagnostic).count(), + 1, + "standard reports emit each diagnostic once: {report}" + ); + } + } + } +} + +#[cfg(any(feature = "tree-error", not(feature = "auto-chain-error")))] +#[test] +fn tree_sources_preserve_native_precedence_and_raw_native_leaf_access() { + let error = crate::ErrorWithSource("native root", not_found("native leaf")) + .raise_typed() + .chain(validation("explicit sibling")) + .raise(message("outer context")) + .into_error(); + insta::assert_debug_snapshot!(error, "fairly straightforward when visualised like this", @" + outer context + + Caused by: + 0: native root + ├─0: native leaf + └─1: explicit sibling + "); + let boundary = std::error::Error::source(&error).expect("the explicit nonleaf source is retained"); + assert_eq!( + boundary.to_string(), + "native root", + "a source boundary displays only its diagnostic" + ); + assert_eq!( + format!("{boundary:#}"), + "native root", + "alternate Display does not expand the subtree" + ); + let leaf = boundary + .source() + .expect("native sources take precedence over explicit children"); + assert_eq!( + leaf.downcast_ref::<gix_error::Message>() + .expect("the native leaf retains its concrete type") + .message, + "native leaf", + "standard traversal still chooses the native source" + ); + assert!( + leaf.source().is_none(), + "standard traversal does not append explicit siblings to the native chain" + ); + assert!( + error.is_not_found() && error.is_validation(), + "typed inspection still visits both branches" + ); + assert_eq!( + gix_error::classify(boundary) + .map(|item| item.error().to_string()) + .collect::<Vec<_>>(), + ["native leaf", "explicit sibling"], + "classification expands the complete source boundary without losing concrete subjects" + ); +} + +fn without_source_location(diagnostic: String) -> String { + diagnostic + .split(", at ") + .next() + .expect("every source has a diagnostic") + .to_owned() +} From 3194ded91d6dc03b62572cba1fd76f30bf3cbc8e Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 6 Oct 2026 11:32:17 +0200 Subject: [PATCH 21/28] feat(gix-error): make caller-location printing opt-in <!-- agent --> `auto-chain-error` also printed source locations in user-facing reports, including consumers which only wanted a standard error chain. Make printing independently configurable with `error-print-location`, forwarded by `gix`. The feature gates the shared location writer; caller locations, metadata, classification, and the existing alternate-format rules remain intact. Enable printing explicitly in workspace binary dependencies and the filter test-helper feature. `gix-error` opts in for its own tests so existing location snapshots retain their coverage. An isolated Cargo consumer checks real `main()` stderr for both crates, printing on and off, tree and chain modes, and `tree-error` precedence without workspace test feature unification. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- Cargo.toml | 6 ++---- gix-error/Cargo.toml | 5 ++++- gix-error/src/error.rs | 5 +++-- gix-error/src/lib.rs | 13 +++++++++---- gix-error/src/test.rs | 2 +- gix-filter/Cargo.toml | 2 +- gix/Cargo.toml | 6 +++++- tests/it/Cargo.toml | 2 +- tests/tools/Cargo.toml | 2 +- 9 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f32a05543f5..f8b3b470556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ test = false doctest = false [features] -default = ["max", "auto-chain-error"] +default = ["max"] #! ### Build Configuration #! These combine common choices of building blocks to represent typical builds. @@ -90,8 +90,6 @@ lean-async = ["hashes", "fast", "tracing", "pretty-cli", "tix", "gitoxide-core-t #! ### Building Blocks #! Typical combinations of features of our dependencies, some of which are referred to in the `gitoxide` crate's code for conditional compilation. -## Flatten error trees into chains with source locations in error reports. -auto-chain-error = ["gix/auto-chain-error"] ## Provide support for all known hashes. If this isn't enabled, select an individual hash algorithms. hashes = ["sha1", "sha256"] @@ -172,7 +170,7 @@ gitoxide-core-async-client = ["gitoxide-core/async-client", "futures-lite"] [dependencies] gitoxide-core = { version = "^0.62.0", path = "gitoxide-core" } gix-features = { version = "^0.50.0", path = "gix-features" } -gix = { version = "^0.88.0", path = "gix", default-features = false } +gix = { version = "^0.88.0", path = "gix", default-features = false, features = ["error-print-location"] } gix-tix = { version = "^0.4.0", path = "gix-tix", default-features = false, optional = true } clap = { version = "4.5.42", features = ["derive", "cargo"] } diff --git a/gix-error/Cargo.toml b/gix-error/Cargo.toml index dd716bd7cdb..e864ef1cbe0 100644 --- a/gix-error/Cargo.toml +++ b/gix-error/Cargo.toml @@ -12,6 +12,9 @@ include = ["/src/**/*", "/LICENSE-*"] rust-version = "1.88" [features] +## Print captured caller locations in diagnostics. Alternate formatting continues to omit locations. +## Locations are always captured and available for inspection, even without this feature. +error-print-location = [] ## Accept [`bstr::BString`] and [`bstr::BStr`] as byte metadata. ## Byte metadata always uses `Vec<u8>` with the same diagnostic formatting. bstr = ["dep:bstr"] @@ -39,7 +42,7 @@ document-features = { version = "0.2.0", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } -gix-error = { path = ".", features = ["anyhow"] } +gix-error = { path = ".", features = ["anyhow", "error-print-location"] } insta = "1.46.3" [package.metadata.docs.rs] diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index c4c5b97b808..1871db943df 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -107,8 +107,9 @@ macro_rules! classification_predicates { /// [`crate::Error::iter_errors_with_locations()`] provide the same lightweight view for the tree-backed and flattened-chain /// representations. /// -/// Its normal [`Display`](std::fmt::Display) output appends the location when one is available. Alternate formatting -/// (`{source:#}`) forwards alternate formatting to the underlying error and always omits the location. +/// Its normal [`Display`](std::fmt::Display) output appends the location when one is available and the +/// `error-print-location` feature is enabled. Alternate formatting (`{source:#}`) forwards alternate formatting to +/// the underlying error and always omits the location. #[derive(Clone, Copy, Debug)] pub struct DisplaySource<'a> { error: &'a (dyn std::error::Error + 'static), diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 059bce5b919..8adffc02ee2 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -210,7 +210,8 @@ //! Accepted errors must convert into `Box<dyn std::error::Error + Send + Sync + 'static>`. //! //! When a test returns an error, Rust's test harness prints [`TestError`]'s [`Debug`](std::fmt::Debug) output, -//! including the complete diagnostic tree or chain and captured caller locations. +//! including the complete diagnostic tree or chain. Captured caller locations are printed when the +//! `error-print-location` feature is enabled; alternate formatting omits them. //! //! ```rust,test_harness //! use gix_error::{message, ResultExt, TestResult}; @@ -602,8 +603,9 @@ pub use exn::{ /// When both the `tree-error` and `auto-chain-error` features are enabled, the `tree-error` /// behavior takes precedence and this type uses the tree-based representation. /// -/// With `auto-chain-error`, [`Debug`](std::fmt::Debug) reports the complete diagnostic chain and caller locations, -/// so returning [`Result`] from `main()` retains the underlying causes. Alternate Debug (`{error:#?}`) omits locations. +/// With `auto-chain-error`, [`Debug`](std::fmt::Debug) reports the complete diagnostic chain, +/// so returning [`Result`] from `main()` retains the underlying causes. Caller locations are printed only with the +/// `error-print-location` feature, including for errors returned from `main()`. Alternate Debug (`{error:#?}`) omits them. /// Normal [`Display`](std::fmt::Display) shows the root diagnostic; alternate Display (`{error:#}`) joins the /// complete chain with `: ` and omits locations, suitable for single-line error messages. pub struct Error { @@ -709,5 +711,8 @@ pub use concrete::metadata::{ }; pub(crate) fn write_location(f: &mut std::fmt::Formatter<'_>, location: &std::panic::Location) -> std::fmt::Result { - write!(f, ", at {}:{}", location.file(), location.line()) + if cfg!(feature = "error-print-location") { + write!(f, ", at {}:{}", location.file(), location.line())?; + } + Ok(()) } diff --git a/gix-error/src/test.rs b/gix-error/src/test.rs index 6934fde4682..b944dc29c81 100644 --- a/gix-error/src/test.rs +++ b/gix-error/src/test.rs @@ -6,7 +6,7 @@ use crate::Error; /// errors via [`From`] without conflicting with the standard library's identity conversion. /// Its [`Debug`](std::fmt::Debug) output includes the complete diagnostic tree or chain, omitting classification markers /// unless only markers are available. Custom I/O wrappers show their kind, with their payloads reported separately. -/// Captured caller locations are included unless alternate formatting is used. +/// Captured caller locations are included with the `error-print-location` feature unless alternate formatting is used. pub struct TestError(Error); /// A result type for test functions whose errors are reported through [`TestError`]'s complete diagnostics. diff --git a/gix-filter/Cargo.toml b/gix-filter/Cargo.toml index fca4d8d55f5..a79620b025f 100644 --- a/gix-filter/Cargo.toml +++ b/gix-filter/Cargo.toml @@ -37,7 +37,7 @@ sha256 = ["gix-hash/sha256"] ## Enable thread-safety, make data structures `Send`. parallel = ["gix-attributes/parallel"] ## Internal feature for building binaries referenced by integration tests through CARGO_BIN_EXE_*. -test-helper = [] +test-helper = ["gix-error/error-print-location"] [dependencies] gix-hash = { version = "^0.27.0", path = "../gix-hash" } diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 5615bbe310a..ec804fd22f1 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -293,7 +293,11 @@ max-performance = ["max-performance-safe"] #! #! The catch-all of feature toggles. -## All [`gix::Error`](crate::Error) values produce error chains with source locations. +## Print captured caller locations in error diagnostics, including errors returned from `main()`. +## Alternate formatting continues to omit locations. Locations are always available for inspection. +error-print-location = ["gix-error/error-print-location"] + +## All [`gix::Error`](crate::Error) trees are flattened into chains for simple error-source chaining. auto-chain-error = ["gix-error/auto-chain-error"] ## Allow [`gix::Exn`](crate::Exn) to convert directly into `anyhow::Error`. diff --git a/tests/it/Cargo.toml b/tests/it/Cargo.toml index 88cd6a374f7..b15ef66e4b2 100644 --- a/tests/it/Cargo.toml +++ b/tests/it/Cargo.toml @@ -16,6 +16,6 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.60", features = ["derive"] } -gix = { version = "^0.88.0", path = "../../gix", default-features = false, features = ["attributes", "blame", "blob-diff", "parallel", "revision", "sha1"] } +gix = { version = "^0.88.0", path = "../../gix", default-features = false, features = ["attributes", "blame", "blob-diff", "error-print-location", "parallel", "revision", "sha1"] } gix-testtools = { path = "../tools", default-features = false, features = ["sha1"] } regex = { version = "1.12.3", default-features = false, features = ["std"] } diff --git a/tests/tools/Cargo.toml b/tests/tools/Cargo.toml index fea3806ae7d..ada86b4ce42 100644 --- a/tests/tools/Cargo.toml +++ b/tests/tools/Cargo.toml @@ -62,7 +62,7 @@ xz = ["dep:xz2"] sbom = ["dep:serde_json"] [dependencies] -gix-error = { version = "^0.4.0", path = "../../gix-error" } +gix-error = { version = "^0.4.0", path = "../../gix-error", features = ["error-print-location"] } gix-hash = { version = "^0.27.0", path = "../../gix-hash" } gix-lock = { version = "^25.0.0", path = "../../gix-lock" } gix-path = { version = "^0.13.0", path = "../../gix-path" } From 4e4685620ec1c3aa83acae06a758a45204399279 Mon Sep 17 00:00:00 2001 From: Codex <codex@openai.com> Date: Tue, 6 Oct 2026 11:36:49 +0200 Subject: [PATCH 22/28] fix(gix-tix): retain caller locations in standalone diagnostics `gix` now makes caller-location printing opt-in independently of error representation. Standalone `tix` should continue showing captured locations when `main()` returns an error, including custom builds with default features disabled. Enable `gix/error-print-location` on its existing dependency. Document the diagnostic contract in `spec.md` and add a binary regression check that ordinary Debug includes the caller file while alternate Debug continues to omit it. Validation: the standalone `tix` binary builds with `sha1`; its diagnostic regression test passes; formatting and diff whitespace checks pass. --- gix-tix/Cargo.toml | 2 +- gix-tix/spec.md | 2 ++ gix-tix/src/main.rs | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/gix-tix/Cargo.toml b/gix-tix/Cargo.toml index 69a4624b75d..9a3df1e5a53 100644 --- a/gix-tix/Cargo.toml +++ b/gix-tix/Cargo.toml @@ -32,7 +32,7 @@ sha256 = ["gix/sha256"] clap = { version = "4.5.42", features = ["derive"] } crossterm = { version = "0.29.0", features = ["osc52"] } directories = "6.0.0" -gix = { version = "^0.88.0", path = "../gix", default-features = false, features = ["blob-diff", "mailmap", "merge", "notes", "parallel", "progress-tree", "revision", "command", "status", "tracing", "worktree-mutation"] } +gix = { version = "^0.88.0", path = "../gix", default-features = false, features = ["blob-diff", "error-print-location", "mailmap", "merge", "notes", "parallel", "progress-tree", "revision", "command", "status", "tracing", "worktree-mutation"] } gix-trace = { version = "^0.2.0", path = "../gix-trace", features = ["forest"] } gix-worktree = { version = "^0.57.0", path = "../gix-worktree", default-features = false, features = ["remove"] } notify = "8.2.0" diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 8820b4fd54a..31f1d145470 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -13,6 +13,8 @@ without trading responsiveness for metadata that is not visible. - `tix [REVISION]...` shows commits reachable from the supplied revisions, or from `HEAD` when none are supplied. +- Standalone `tix` opts into `gix/error-print-location`, so errors returned from + `main()` include captured caller locations. Alternate formatting omits them. - Standalone `tix` accepts `-t|--trace` up to four times. One occurrence emits forest-formatted info events, two emit forest-formatted debug events, three emit flat debug events, and four emit flat trace events. `gix tix` inherits diff --git a/gix-tix/src/main.rs b/gix-tix/src/main.rs index d1e25a62056..122c534ffdf 100644 --- a/gix-tix/src/main.rs +++ b/gix-tix/src/main.rs @@ -5,3 +5,18 @@ use gix::Result; fn main() -> Result<()> { gix_tix::command::parse().run() } + +#[test] +fn main_errors_include_caller_locations() { + use gix::error::{ErrorExt, message}; + + let error = message("command failed").raise(); + assert!( + format!("{error:?}").contains(file!()), + "standalone binary diagnostics include captured caller locations" + ); + assert!( + !format!("{error:#?}").contains(file!()), + "alternate formatting still omits caller locations" + ); +} From 777080c4937c7e4d4afd99b1be1bfc21f4270756 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 6 Oct 2026 14:55:45 +0200 Subject: [PATCH 23/28] fix(gix-error): shorten caller locations to package source paths <!-- Byron --> Reviewed and refactored carefully, tried the release script as well. Didn't review the release script at all though, just had some docs added. I consider it disposable. <!-- agent --> Enabled location diagnostics could expose entire checkout and Cargo registry paths, obscuring the useful source location with machine-specific directories. Format conventional source paths relative to their containing package directory and remove Cargo registry version suffixes, so a path such as `/home/user/.cargo/registry/src/index.crates.io-hash/gix-url-0.39.0/src/parse.rs` prints as `gix-url/src/parse.rs`. Unrecognized layouts fall back to the filename. Keep captured `std::panic::Location` values and line numbers intact. The shared formatter covers tree, chain, source, test, and `main()` diagnostics; feature defaults and alternate formatting remain unchanged. Document the limitation that locations contain no Cargo package metadata and build owners can use `--remap-path-prefix` for exact package names in arbitrary layouts. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- .github/workflows/release.yml | 72 +++++++++------- AGENTS.md | 1 + DEVELOPMENT.md | 18 ++++ etc/scripts/remap-release-paths.sh | 130 +++++++++++++++++++++++++++++ gix-error/src/error.rs | 2 + gix-error/src/exn/impls.rs | 2 + gix-error/src/lib.rs | 22 +++-- gix-error/src/location.rs | 100 ++++++++++++++++++++++ 8 files changed, 310 insertions(+), 37 deletions(-) create mode 100644 etc/scripts/remap-release-paths.sh create mode 100644 gix-error/src/location.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce4053c4121..dacc0dd6ea0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,22 +9,22 @@ on: # branches: # - fix-releases tags: - # For now, real releases always use `workflow_dispatch`, and running the workflow on tag pushes - # is only done in testing. This is because we usually push too many tags at once for the `push` - # event to be triggered, since there are usually more than 3 crates tagged together. So the - # `push` trigger doesn't usually work. If we allow it, we risk running the workflow twice if - # it is also manually triggered based on the assumption that it would not run. See #1970 for - # details. See also the `run-release-workflow` and `roll-release` recipes in the `justfile`. - # - 'v*' - - 'v*-DO-NOT-USE' # Pattern for tags used to test the workflow (usually done in a fork). + # For now, real releases always use `workflow_dispatch`, and running the workflow on tag pushes + # is only done in testing. This is because we usually push too many tags at once for the `push` + # event to be triggered, since there are usually more than 3 crates tagged together. So the + # `push` trigger doesn't usually work. If we allow it, we risk running the workflow twice if + # it is also manually triggered based on the assumption that it would not run. See #1970 for + # details. See also the `run-release-workflow` and `roll-release` recipes in the `justfile`. + # - 'v*' + - "v*-DO-NOT-USE" # Pattern for tags used to test the workflow (usually done in a fork). workflow_dispatch: permissions: - contents: read # This is set more permissively in jobs that need `write`. + contents: read # This is set more permissively in jobs that need `write`. defaults: run: - shell: bash # Use `bash` even in the Windows jobs. + shell: bash # Use `bash` even in the Windows jobs. jobs: # Create a draft release, initially with no binary assets attached. @@ -32,11 +32,11 @@ jobs: runs-on: ubuntu-slim permissions: - contents: write # Allows the use of `gh release create`. + contents: write # Allows the use of `gh release create`. -# env: -# # Set to force version number, e.g., when no tag exists. -# VERSION: TEST-0.0.0 + # env: + # # Set to force version number, e.g., when no tag exists. + # VERSION: TEST-0.0.0 steps: - name: Checkout repository @@ -85,7 +85,7 @@ jobs: # Build for a particular feature and target, and attach an archive for it. build-release: - needs: [ create-release ] + needs: [create-release] strategy: matrix: @@ -108,7 +108,7 @@ jobs: - i686-pc-windows-msvc - aarch64-pc-windows-msvc # When changing these features, make the same change in build-macos-universal2-release. - feature: [ small, lean, max, max-pure ] + feature: [small, lean, max, max-pure] include: - rust: stable - target: x86_64-unknown-linux-musl @@ -219,13 +219,13 @@ jobs: runs-on: ${{ matrix.os }} permissions: - contents: write # Allows the use of `gh release upload`. + contents: write # Allows the use of `gh release upload`. env: - RUST_BACKTRACE: '1' # Emit backtraces on panics. + RUST_BACKTRACE: "1" # Emit backtraces on panics. CARGO_TERM_COLOR: always - CLICOLOR: '1' - CARGO: cargo # On Linux, this will be changed to `cross` in a later step. + CLICOLOR: "1" + CARGO: cargo # On Linux, this will be changed to `cross` in a later step. FEATURE: ${{ matrix.feature }} VERSION: ${{ needs.create-release.outputs.version }} TARGET: ${{ matrix.target }} @@ -269,7 +269,17 @@ jobs: - name: Build release binary (with extra optimizations) run: | - "$CARGO" build --verbose --profile="$PROFILE" "$TARGET_FLAGS" --no-default-features --features="$FEATURE" + bash etc/scripts/remap-release-paths.sh build "$CARGO" build --verbose --profile="$PROFILE" "$TARGET_FLAGS" --no-default-features --features="$FEATURE" + + - name: Check release binaries for local build paths + run: | + suffix='' + if [[ "$TARGET" == *-windows-* ]]; then + suffix='.exe' + fi + if ! bash etc/scripts/remap-release-paths.sh check "$TARGET_DIR/$PROFILE/gix$suffix" "$TARGET_DIR/$PROFILE/ein$suffix"; then + echo '::warning::Release binary path scan failed; see the output above. Continuing with packaging.' + fi - name: Determine archive basename run: echo "ARCHIVE=gitoxide-$FEATURE-$VERSION-$TARGET" >> "$GITHUB_ENV" @@ -308,15 +318,15 @@ jobs: build-macos-universal2-release: runs-on: macos-latest - needs: [ create-release, build-release ] + needs: [create-release, build-release] strategy: matrix: # These features need to be exactly the same as the features in build-release. - feature: [ small, lean, max, max-pure ] + feature: [small, lean, max, max-pure] permissions: - contents: write # Allows the use of `gh release upload`. + contents: write # Allows the use of `gh release upload`. env: BASH_ENV: ./helpers.sh @@ -375,10 +385,10 @@ jobs: publish-release: runs-on: ubuntu-slim - needs: [ create-release, build-release, build-macos-universal2-release ] + needs: [create-release, build-release, build-macos-universal2-release] permissions: - contents: write # Allows use of `gh release` for `upload`, `delete-asset`, and `edit`. + contents: write # Allows use of `gh release` for `upload`, `delete-asset`, and `edit`. env: REPOSITORY: ${{ github.repository }} @@ -444,11 +454,11 @@ jobs: announce-release: runs-on: ubuntu-slim - needs: [ create-release, publish-release ] + needs: [create-release, publish-release] permissions: - contents: write # Needed to distinguish unpublished (still draft) from missing releases. - discussions: write # For adding a comment in the announcement discussion. + contents: write # Needed to distinguish unpublished (still draft) from missing releases. + discussions: write # For adding a comment in the announcement discussion. env: REPOSITORY: ${{ github.repository }} @@ -520,7 +530,7 @@ jobs: installation: strategy: matrix: - build: [ win-msvc, win-gnu, win32-msvc, win32-gnu ] + build: [win-msvc, win-gnu, win32-msvc, win32-gnu] include: - build: win-msvc os: windows-latest @@ -559,7 +569,7 @@ jobs: msystem: MINGW${{ startsWith(matrix.target, 'i686-') && '32' || '64' }} pacboy: cc:p path-type: inherit - - name: 'Installation from crates.io: gitoxide' + - name: "Installation from crates.io: gitoxide" run: | cargo +"$RUST" install --target "$TARGET" --no-default-features \ --features max-pure --target-dir install-artifacts --debug --force gitoxide diff --git a/AGENTS.md b/AGENTS.md index e7b19c995b7..c8e4d4c71ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,7 @@ Follow "purposeful conventional commits" style: ### Code Style +- Do not create persistent Python scripts in this repository. Use Bash for repository automation and script-based tests unless the user explicitly requests otherwise. - Follow existing patterns in the codebase - Skip stylistic rewrites that increase SLOC after formatting; keep simpler existing forms instead of applying style rules unconditionally. - Start new Rust modules as `foo.rs`. Create a module directory only when it contains multiple module files; then use `foo/mod.rs` rather than a sibling `foo.rs` file. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index cad48429a83..d0ecaeba745 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -10,6 +10,24 @@ Run `just` to browse commands grouped by purpose, with everyday development task first. Use `just --groups` to list the groups, or filter the overview with, for example, `just --list --group 'Dependencies and SBOMs'`. +## Release source paths + +The GitHub release workflow builds `gix` and `ein` through +`etc/scripts/remap-release-paths.sh`. It appends rustc's `--remap-path-prefix` +flags for all target crates, including dependencies, using stable virtual roots: +`/gitoxide` for the checkout, `/cargo` for Cargo sources, `/rust` for the selected +Rust toolchain, and `/generated` for build output. Home, Rustup, and temporary +paths are also remapped. Native host paths, canonical aliases, Windows separator +variants, and `cross` container mounts are covered. Normal +local development builds are unchanged. + +To opt into the same policy locally, run these commands from the workspace root: + +```sh +bash etc/scripts/remap-release-paths.sh build cargo build --release --locked -p gitoxide --bins +bash etc/scripts/remap-release-paths.sh check target/release/gix target/release/ein +``` + ## Practices * **test-first development** diff --git a/etc/scripts/remap-release-paths.sh b/etc/scripts/remap-release-paths.sh new file mode 100644 index 00000000000..b01ed5f499b --- /dev/null +++ b/etc/scripts/remap-release-paths.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# Remap local Rust source paths to stable virtual roots for release builds. +# Run from the workspace root; requires Bash, cargo, rustc, and jq: +# bash etc/scripts/remap-release-paths.sh build cargo build --release --locked --bins +# bash etc/scripts/remap-release-paths.sh check target/release/gix target/release/ein +# `build` also accepts cross; `check` returns nonzero for known local path leaks +# or scan errors (warning-only in release CI). This is best effort; see DEVELOPMENT.md. + +set -euo pipefail + +release_path_aliases() { + local path="$1" destination="$2" canonical + if command -v cygpath >/dev/null 2>&1; then + path="$(cygpath -am -- "$path")" || return + fi + printf '%s\t%s\n' "$path" "$destination" + printf '%s\t%s\n' "${path//\//\\}" "$destination" + if [[ -d "$path" ]]; then + canonical="$(cd -- "$path" && pwd -P)" || return + if command -v cygpath >/dev/null 2>&1; then + canonical="$(cygpath -am -- "$canonical")" || return + fi + printf '%s\t%s\n' "$canonical" "$destination" + printf '%s\t%s\n' "${canonical//\//\\}" "$destination" + fi +} + +release_local_roots() { + local home="${USERPROFILE:-$HOME}" target_dir sysroot + target_dir="$(cargo metadata --locked --offline --no-deps --format-version=1 | jq -br .target_directory)" || return + sysroot="$(rustc --print sysroot)" || return + sysroot="${sysroot//$'\r'/}" + release_path_aliases "$home" /build-home || return + release_path_aliases "${RUSTUP_HOME:-$home/.rustup}" /rustup || return + release_path_aliases "${CARGO_HOME:-$home/.cargo}" /cargo || return + release_path_aliases "$PWD" /gitoxide || return + if [[ -n "${GITHUB_WORKSPACE:-}" ]]; then + release_path_aliases "$GITHUB_WORKSPACE" /gitoxide || return + fi + release_path_aliases "$sysroot" /rust || return + release_path_aliases "$target_dir" /generated +} + +release_remap_flags() { + local roots="$1" source destination + { + if [[ -n "$roots" ]]; then + printf '%s\n' "$roots" + fi + release_path_aliases "${TMPDIR:-${TEMP:-/tmp}}" /build-tmp || return + # cross 0.2.5 container mounts; newer cross versions may retain host paths. + printf '%s\n' $'/project\t/gitoxide' $'/cargo\t/cargo' $'/rust\t/rust' $'/target\t/generated' $'target\t/generated' + } | awk -F '\t' '{ print length($1) "\t" $0 }' | sort -n | cut -f2- | + while IFS=$'\t' read -r source destination; do + # rustc uses the last matching rule, so nested roots must follow parents. + printf '%s\n' "--remap-path-prefix=$source=$destination" + done +} + +release_build() { + local roots="$1" flags flag remaps + shift + if [[ "${CARGO_ENCODED_RUSTFLAGS+x}" ]]; then + flags="$CARGO_ENCODED_RUSTFLAGS" + else + # Cargo splits RUSTFLAGS on whitespace; shell quotes are not interpreted. + flags="$(printf '%s' "${RUSTFLAGS:-}" | awk '{ for (i = 1; i <= NF; i++) { printf "%s%s", sep, $i; sep = "\037" } }')" || return + fi + remaps="$(release_remap_flags "$roots")" || return + while IFS= read -r flag; do + if [[ -n "$flags" ]]; then + flags+=$'\x1f' + fi + flags+="$flag" + done <<< "$remaps" + # Keep the parent environment unchanged, including flags used to install cross. + # Git Bash must not rewrite virtual roots when launching native Cargo on Windows. + MSYS2_ENV_CONV_EXCL="${MSYS2_ENV_CONV_EXCL:+$MSYS2_ENV_CONV_EXCL;}CARGO_ENCODED_RUSTFLAGS" \ + CARGO_ENCODED_RUSTFLAGS="$flags" "$@" +} + +release_check() { + local roots="$1" binary source destination prefix grep_status status=0 + shift + for binary in "$@"; do + [[ -f "$binary" ]] || { printf 'missing release binary: %s\n' "$binary" >&2; return 1; } + while IFS=$'\t' read -r source destination; do + prefix="${source%/}/" + if [[ "$source" == *\\* ]]; then + prefix="${source%\\}\\" + fi + if LC_ALL=C grep -aFq -- "$prefix" "$binary"; then + printf '%s: embedded local build path prefix: %s\n' "$binary" "$prefix" >&2 + status=1 + else + grep_status=$? + if [[ "$grep_status" -ne 1 ]]; then + printf '%s: could not scan release binary\n' "$binary" >&2 + return "$grep_status" + fi + fi + done < <( + printf '%s\n' "$roots" + printf '%s\n' $'/project\t/gitoxide' + # `/target/` alone is also a legitimate Git ignore-pattern literal in ein. + printf '%s\t/generated\n' "/target/${TARGET:-release-github}" /target/debug /target/release /target/release-github /target/build + ) + done + return "$status" +} + +release_paths_main() { + local operation="${1:-}" roots + if [[ "$#" -lt 2 ]] || [[ "$operation" != build && "$operation" != check ]]; then + printf 'usage: %s build COMMAND [ARGS...] | check BINARY...\n' "$0" >&2 + return 2 + fi + shift + roots="$(release_local_roots)" || return + if [[ "$operation" == build ]]; then + release_build "$roots" "$@" + else + release_check "$roots" "$@" + fi +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + release_paths_main "$@" +fi diff --git a/gix-error/src/error.rs b/gix-error/src/error.rs index 1871db943df..3e40e2b3701 100644 --- a/gix-error/src/error.rs +++ b/gix-error/src/error.rs @@ -123,6 +123,8 @@ impl<'a> DisplaySource<'a> { } /// Return the captured or inherited caller location, or `None` for an ordinary native error source. + /// + /// The file path is the compiler-provided path, before diagnostic formatting shortens it. pub fn location(&self) -> Option<&'static std::panic::Location<'static>> { self.location } diff --git a/gix-error/src/exn/impls.rs b/gix-error/src/exn/impls.rs index 23f8128fb68..a0d4bceb770 100644 --- a/gix-error/src/exn/impls.rs +++ b/gix-error/src/exn/impls.rs @@ -548,6 +548,8 @@ impl Frame { } /// Return the source code location where this exception frame was created. + /// + /// The file path is the compiler-provided path, before diagnostic formatting shortens it. pub fn location(&self) -> &'static Location<'static> { self.source.location() } diff --git a/gix-error/src/lib.rs b/gix-error/src/lib.rs index 8adffc02ee2..dee30094526 100644 --- a/gix-error/src/lib.rs +++ b/gix-error/src/lib.rs @@ -60,6 +60,20 @@ //! metadata, and caller locations; `?`, `.into()`, and [`Exn::into_error()`] also convert exceptions to [`Error`]. //! Use [`Error::into_exn()`] to recover an exception tree for internal processing, including rearranging child frames. //! +//! # Caller locations +//! +//! The `error-print-location` feature enables caller locations in diagnostics. Paths with conventional +//! `src`, `tests`, `examples`, or `benches` directories are shortened to the containing package directory +//! and source path; Cargo registry package versions are omitted. For example, +//! `/home/user/.cargo/registry/src/index.crates.io-hash/gix-url-0.39.0/src/parse.rs:349` +//! is displayed as `gix-url/src/parse.rs:349`. Package-relative source paths are retained; +//! paths whose package root cannot be inferred from these directory conventions fall back to the filename. +//! +//! This only changes formatting: inspected locations retain the compiler-provided file, line, and column. +//! A location contains no Cargo package metadata, so a directory that differs from the package name cannot +//! be renamed automatically. Build owners can use rustc's `--remap-path-prefix` to control captured paths +//! for arbitrary source layouts. Alternate formatting continues to omit locations. +//! //! # Standard Error Types //! //! Use these types for diagnostic context when recovery does not depend on a specific condition or structured payload. @@ -710,9 +724,5 @@ pub use concrete::metadata::{ permission_denied, resource_exhaustion, retryable, unauthenticated, unsupported, validation, }; -pub(crate) fn write_location(f: &mut std::fmt::Formatter<'_>, location: &std::panic::Location) -> std::fmt::Result { - if cfg!(feature = "error-print-location") { - write!(f, ", at {}:{}", location.file(), location.line())?; - } - Ok(()) -} +mod location; +pub(crate) use location::write as write_location; diff --git a/gix-error/src/location.rs b/gix-error/src/location.rs new file mode 100644 index 00000000000..3f65dbefd8d --- /dev/null +++ b/gix-error/src/location.rs @@ -0,0 +1,100 @@ +use std::{fmt, panic::Location}; + +pub(crate) fn write(f: &mut fmt::Formatter<'_>, location: &Location<'_>) -> fmt::Result { + if cfg!(feature = "error-print-location") { + let (package, source) = local_path(location.file()); + write!(f, ", at {package}{source}:{}", location.line())?; + } + Ok(()) +} + +/// Location has no Cargo package metadata. Recognize conventional source directories; +/// exact package names for arbitrary layouts require the build owner to use --remap-path-prefix. +fn local_path(file: &str) -> (&str, &str) { + let registry_path = file + .rsplit_once("/registry/src/") + .or_else(|| file.rsplit_once(r"\registry\src\")) + .and_then(|(_, registry)| registry.split_once(['/', '\\'])) + .map(|(_, package)| package); + let file = registry_path.unwrap_or(file); + let mut package = ""; + let mut offset = 0; + for component in file.split(['/', '\\']) { + if offset == 0 && matches!(component, "src" | "tests" | "examples" | "benches") { + return ("", file); + } + if matches!(component, "src" | "tests" | "examples" | "benches") + && !matches!(package, "" | "." | ".." | "registry") + { + if registry_path.is_some() { + package = without_version(package); + } + return (package, &file[offset - 1..]); + } + package = component; + offset += component.len() + 1; + } + ("", file.rsplit(['/', '\\']).next().unwrap_or(file)) +} + +fn without_version(package: &str) -> &str { + package + .match_indices('-') + .find_map(|(offset, _)| { + let version = package[offset + 1..].split(['-', '+']).next()?; + let mut numbers = version.split('.'); + (numbers.clone().count() == 3 + && numbers.all(|number| !number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit()))) + .then_some(&package[..offset]) + }) + .unwrap_or(package) +} + +#[cfg(test)] +mod tests { + #[test] + fn paths_are_local_to_conventional_package_roots() { + for (input, expected) in [ + ("gix-error/src/lib.rs", "gix-error/src/lib.rs"), + ("/home/user/gitoxide/gix-error/src/lib.rs", "gix-error/src/lib.rs"), + ( + "/home/user/gitoxide/gix-error/tests/error/main.rs", + "gix-error/tests/error/main.rs", + ), + ("/checkout/crate/examples/main.rs", "crate/examples/main.rs"), + ("/checkout/crate/benches/main.rs", "crate/benches/main.rs"), + ("/checkout/crate/src/nested/src/main.rs", "crate/src/nested/src/main.rs"), + ( + "/home/user/.cargo/registry/src/index.crates.io-hash/gix-url-0.39.0/src/parse.rs", + "gix-url/src/parse.rs", + ), + ( + "/home/user/src/cargo/registry/src/index.crates.io-hash/gix-url-0.39.0/src/parse.rs", + "gix-url/src/parse.rs", + ), + ( + "/cargo/registry/src/index.crates.io-hash/crate-2-name-1.2.3-alpha.1+build/src/lib.rs", + "crate-2-name/src/lib.rs", + ), + ( + r"C:\Users\user\.cargo\registry\src\index.crates.io-hash\gix-url-0.39.0\src\parse.rs", + r"gix-url\src\parse.rs", + ), + ( + r"C:\checkout\gix-error\tests\error\main.rs", + r"gix-error\tests\error\main.rs", + ), + ("/checkout/crate-1.2.3/src/main.rs", "crate-1.2.3/src/main.rs"), + ("src/main.rs", "src/main.rs"), + ("../src/main.rs", "main.rs"), + ("/src/main.rs", "main.rs"), + ("/custom/layout/main.rs", "main.rs"), + (r"C:\custom\layout\main.rs", "main.rs"), + ("main.rs", "main.rs"), + ("", ""), + ] { + let (package, source) = super::local_path(input); + assert_eq!(format!("{package}{source}"), expected, "source path: {input}"); + } + } +} From d942fe872bd5aac026d89067b1583a167e4ed00e Mon Sep 17 00:00:00 2001 From: Codex GPT-6 <codex@openai.com> Date: Wed, 7 Oct 2026 22:07:17 +0200 Subject: [PATCH 24/28] fixup! fix(gix-error): shorten caller locations to package source paths The full `test` CI job fails in `just check-mode`: the new `etc/scripts/remap-release-paths.sh` starts with a Bash shebang but was committed with mode `100644`. The repository requires scripts with shebangs to be executable. Set its mode to `100755` without changing its contents. The existing `cargo run -p internal-tools -- check-mode` reproduces the mismatch before staging this change and passes afterward. --- etc/scripts/remap-release-paths.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 etc/scripts/remap-release-paths.sh diff --git a/etc/scripts/remap-release-paths.sh b/etc/scripts/remap-release-paths.sh old mode 100644 new mode 100755 From 49d1880f5fde59f5340dd5432eacadb190c57c0e Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Tue, 29 Sep 2026 21:26:40 +0200 Subject: [PATCH 25/28] fix(gix-discover): derive trust from all detected repository paths <!-- Byron --> There are some concerns over performance, but it seems the work it does is required after all. There are possible optimisations left on the table, as it might do duplicate IO to check ownership. More tests are read-only now as well, it really does like its tempdirs and direct Git invocations. <!-- agent --> Checking ownership of only the discovery candidate or resolved git directory can grant full trust to a linked repository with foreign-owned paths. Add `gix_discover::repository::trust()` to compute the minimum trust of the original candidate, resolved git directory, common directory referenced by `commondir`, and existing worktree. Use it during upward discovery and direct and environment-based `gix` opens, before selecting trust-dependent options or reading repository configuration. Never upgrade an untrusted candidate. Skip a duplicate git-directory check only when its spelling exactly matches the candidate after joining with the current directory; filesystem aliases can still cause repeated checks. Skip a missing worktree so a private git directory remains usable after checkout removal, but propagate missing git or common-directory errors. Preserve `TrustPolicy::Assume` and explicit opening trust overrides, and document the distinction between ownership-derived trust, required thresholds, and assumed trust. Add deterministic ownership regressions through private callbacks, including linked paths, duplicate checks, and missing directories. Replace the ancestor-discovery test's dependency on the source checkout with read-only nested repository fixtures. Git reference: `setup.c::ensure_valid_ownership` at `d38352cd43ab9745686d697872408bc3249a153f` checks gitfile, worktree and gitdir. The common directory is also checked here as a configuration source. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gix-discover/src/repository.rs | 153 +++++++++++++++++ gix-discover/src/upwards/mod.rs | 161 +++++++++++++++--- gix-discover/src/upwards/types.rs | 24 ++- gix-discover/tests/discover/upwards/mod.rs | 48 ++---- .../fixtures/generated-archives/.gitignore | 9 + .../fixtures/make_linked_ownership_repo.sh | 8 + .../tests/fixtures/make_nested_repos.sh | 8 + .../tests/fixtures/make_ownership_repos.sh | 7 + gix/src/open/options.rs | 12 +- gix/src/open/repository.rs | 34 +++- 10 files changed, 397 insertions(+), 67 deletions(-) create mode 100755 gix-discover/tests/fixtures/make_linked_ownership_repo.sh create mode 100755 gix-discover/tests/fixtures/make_nested_repos.sh create mode 100755 gix-discover/tests/fixtures/make_ownership_repos.sh diff --git a/gix-discover/src/repository.rs b/gix-discover/src/repository.rs index 2742e9ec6ac..76f6b7fc9b7 100644 --- a/gix-discover/src/repository.rs +++ b/gix-discover/src/repository.rs @@ -1,5 +1,158 @@ use std::path::PathBuf; +use gix_error::{Result, ResultExt, message}; +use gix_sec::Trust; + +/// Determine the minimum ownership trust of `candidate`, `git_dir`, its common directory, and existing `work_dir`. +/// +/// Relative paths are accessed from `current_dir`. `candidate` identifies the original path before resolving a +/// possible gitfile. Its optional, previously determined ownership trust can only be lowered by the other paths. +/// If its spelling exactly matches `git_dir` after joining with `current_dir`, ownership is checked only once. +/// No normalization or filesystem alias checks are performed to establish equality. +/// A missing `work_dir` is skipped, but missing `git_dir` or common directories are errors. +pub fn trust( + git_dir: &std::path::Path, + work_dir: Option<&std::path::Path>, + current_dir: &std::path::Path, + candidate: (&std::path::Path, Option<Trust>), +) -> Result<Trust> { + trust_with(git_dir, work_dir, current_dir, candidate, Trust::from_path_ownership) +} + +pub(crate) fn trust_with( + git_dir: &std::path::Path, + work_dir: Option<&std::path::Path>, + current_dir: &std::path::Path, + (candidate, candidate_trust): (&std::path::Path, Option<Trust>), + mut trust_from_path: impl FnMut(&std::path::Path) -> std::io::Result<Trust>, +) -> Result<Trust> { + let git_dir = current_dir.join(git_dir); + let candidate = current_dir.join(candidate); + let common_dir = crate::path::from_plain_file(&git_dir.join("commondir")) + .transpose() + .or_raise(|| message("Could not resolve the common repository directory for trust check"))? + .map(|common_dir| git_dir.join(common_dir)); + let mut trust = match candidate_trust { + Some(trust) => trust, + None => trust_from_path(&candidate) + .or_raise(|| message!("Could not determine trust level for path '{}'.", candidate.display()))?, + }; + for (path, optional_worktree) in [ + ( + (git_dir.as_os_str() != candidate.as_os_str()).then_some(git_dir.as_path()), + false, + ), + (common_dir.as_deref(), false), + (work_dir, true), + ] { + let Some(path) = path else { continue }; + let path = current_dir.join(path); + let ownership = match trust_from_path(&path) { + // A private git directory remains usable after its checkout was removed. + Err(err) if optional_worktree && err.kind() == std::io::ErrorKind::NotFound => continue, + ownership => ownership, + }; + trust = trust + .min(ownership.or_raise(|| message!("Could not determine trust level for path '{}'.", path.display()))?); + } + Ok(trust) +} + +#[cfg(test)] +mod trust_tests { + use super::trust_with; + use gix_sec::Trust; + + #[test] + fn known_candidate_trust_skips_only_identical_git_directory_spellings() -> gix_error::TestResult { + let root = gix_testtools::scripted_fixture_read_only("make_ownership_repos.sh")?; + let cwd = std::env::current_dir()?; + for name in ["worktree/.git", "bare"] { + let git_dir = root.join(name); + for candidate in [git_dir.clone(), cwd.join(&git_dir), git_dir.join(".")] { + let mut checked_paths = Vec::new(); + let trust = trust_with(&git_dir, None, &cwd, (&candidate, Some(Trust::Reduced)), |path| { + checked_paths.push(path.to_path_buf()); + Ok(Trust::Full) + })?; + let expected_paths = if cwd.join(&candidate).as_os_str() == cwd.join(&git_dir).as_os_str() { + Vec::new() + } else { + vec![cwd.join(&git_dir)] + }; + assert_eq!( + checked_paths, expected_paths, + "only an exact candidate spelling can avoid checking the git directory" + ); + assert_eq!(trust, Trust::Reduced, "reuse must not upgrade the candidate's trust"); + } + } + Ok(()) + } + + #[test] + fn resolved_paths_constrain_trust_without_upgrading_the_candidate() -> gix_error::TestResult { + let root = gix_testtools::scripted_fixture_read_only("make_linked_ownership_repo.sh")?; + let cwd = std::env::current_dir()?; + let candidate = root.join("linked/.git"); + let git_dir = root.join("main/.git/worktrees/linked"); + let common_dir = root.join("main/.git"); + let worktree = root.join("linked"); + for foreign_path in [&git_dir, &common_dir, &worktree] { + let foreign_path = foreign_path.canonicalize()?; + let trust = trust_with( + &git_dir, + Some(&worktree), + &cwd, + (&candidate, Some(Trust::Full)), + |path| { + Ok(if path.canonicalize()? == foreign_path { + Trust::Reduced + } else { + Trust::Full + }) + }, + )?; + assert_eq!(trust, Trust::Reduced, "every resolved repository path constrains trust"); + } + assert_eq!( + trust_with( + &git_dir, + Some(&worktree), + &cwd, + (&candidate, Some(Trust::Reduced)), + |_| Ok(Trust::Full) + )?, + Trust::Reduced, + "a trusted git directory must not upgrade an untrusted gitfile" + ); + let missing_worktree = root.join("missing-checkout"); + assert!( + super::trust(&git_dir, Some(&missing_worktree), &cwd, (&candidate, Some(Trust::Full))).is_ok(), + "a missing checkout must not prevent opening the repository" + ); + let common_dir = common_dir.canonicalize()?; + assert!( + trust_with( + &git_dir, + Some(&missing_worktree), + &cwd, + (&candidate, Some(Trust::Full)), + |path| { + if path.canonicalize()? == common_dir { + Err(std::io::ErrorKind::NotFound.into()) + } else { + Trust::from_path_ownership(path) + } + } + ) + .is_err(), + "only a missing checkout may be skipped; repository paths remain mandatory" + ); + Ok(()) + } +} + /// A repository path which either points to a work tree or the `.git` repository itself. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Path { diff --git a/gix-discover/src/upwards/mod.rs b/gix-discover/src/upwards/mod.rs index 9245095dd3c..bd4d6052307 100644 --- a/gix-discover/src/upwards/mod.rs +++ b/gix-discover/src/upwards/mod.rs @@ -197,14 +197,23 @@ pub(crate) mod function { } } - /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide - /// an associated Trust level by looking at the git directory's ownership, and control discovery using `options`. + /// Find the location of the git repository directly in `directory` or in any of its parent directories, + /// controlling discovery using `options`, and return its path and associated trust level. + /// + /// [`Options::trust`] determines whether trust is [derived from ownership of all relevant repository paths][TrustPolicy::Required] + /// or [supplied by the caller without ownership checks][TrustPolicy::Assume]. The returned trust is not + /// necessarily derived only from the returned path; see [`TrustPolicy`] for details. /// /// Fail if no valid-looking git repository could be found. /// Downcast to [`Error`] to distinguish a missing repository or a search limit from an untrusted candidate. /// Filesystem and other operational failures retain their original causes. + pub fn discover_opts(directory: &Path, options: Options<'_>) -> Result<(crate::repository::Path, gix_sec::Trust)> { + discover_opts_with_trust(directory, options, Trust::from_path_ownership) + } + + /// Note that `trust_from_path` is really only needed for testability. #[cfg_attr(not(unix), allow(unused_variables))] - pub fn discover_opts( + fn discover_opts_with_trust( directory: &Path, Options { trust, @@ -214,6 +223,7 @@ pub(crate) mod function { current_dir, dot_git_only, }: Options<'_>, + mut trust_from_path: impl FnMut(&Path) -> std::io::Result<Trust>, ) -> Result<(crate::repository::Path, gix_sec::Trust)> { // Normalize the path so that `Path::parent()` _actually_ gives // us the parent directory. (`Path::parent` just strips off the last @@ -262,20 +272,39 @@ pub(crate) mod function { let initial_device = device_id(&dir_metadata); let resolved = OnceCell::<Option<PathBuf>>::new(); let resolved = || resolved.get_or_init(|| resolved_directory_for_parent_traversal(directory, cwd.as_ref())); - let filter_by_trust = |dir: &Path| -> Result<std::result::Result<Trust, (Trust, Trust)>> { - match trust { - TrustPolicy::Required(required) => { - let trust = Trust::from_path_ownership(dir) - .or_raise(|| message!("Could not determine trust level for path \"{}\".", dir.display()))?; - Ok(if trust >= required { - Ok(trust) - } else { - Err((required, trust)) - }) + let mut filter_by_trust = + |dir: &Path, kind: &crate::repository::Kind| -> Result<std::result::Result<Trust, (Trust, Trust)>> { + match trust { + TrustPolicy::Required(required) => { + use crate::repository::Kind; + let git_dir = match kind { + Kind::Submodule { git_dir } + | Kind::WorkTree { + linked_git_dir: Some(git_dir), + } => git_dir.as_path(), + _ => dir, + }; + let work_dir = match kind { + Kind::WorkTree { .. } | Kind::Submodule { .. } => dir.parent(), + Kind::WorkTreeGitDir { work_dir } => Some(work_dir.as_path()), + _ => None, + }; + let trust = crate::repository::trust_with( + git_dir, + work_dir, + cwd.as_ref(), + (dir, None), + &mut trust_from_path, + )?; + Ok(if trust >= required { + Ok(trust) + } else { + Err((required, trust)) + }) + } + TrustPolicy::Assume(trust) => Ok(Ok(trust)), } - TrustPolicy::Assume(trust) => Ok(Ok(trust)), - } - }; + }; // A preceding symlink makes `..` ascend from its target rather than its lexical parent. // Resolve any input containing `..` before probing because only the filesystem can distinguish these cases. @@ -321,7 +350,7 @@ pub(crate) mod function { } if let Some((kind, appended_dot_git)) = search.probe_repository(cwd.as_ref(), dot_git_only) { - match filter_by_trust(&search.current)? { + match filter_by_trust(&search.current, &kind)? { Err((required, trust)) => { break 'outer Err(Error::NoTrustedGitRepository { path: search.logical, @@ -386,11 +415,105 @@ pub(crate) mod function { } } - /// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide - /// the trust level derived from Path ownership. + /// Find the location of the git repository directly in `directory` or in any of its parent directories, + /// and return its path and ownership-derived trust level. + /// + /// Uses default options with [`TrustPolicy::Required`] set to [`Trust::Reduced`]. The returned trust is the + /// minimum ownership-derived trust of all paths described by that policy, not just the returned path, + /// and can be [`Trust::Full`]. See [`crate::upwards_opts()`] to customize discovery and its trust policy. /// /// Fail if no valid-looking git repository could be found. pub fn discover(directory: &Path) -> Result<(crate::repository::Path, gix_sec::Trust)> { discover_opts(directory, Default::default()) } + + #[cfg(test)] + mod tests { + use super::{Options, Trust, TrustPolicy, discover_opts_with_trust}; + + #[test] + fn ownership_checks_skip_identical_candidate_and_git_directory() -> gix_error::TestResult { + let root = gix_testtools::scripted_fixture_read_only("make_ownership_repos.sh")?; + let root = std::env::current_dir()?.join(root); + for (name, bare) in [("worktree", false), ("bare", true)] { + let repository = root.join(name); + let git_dir = if bare { + repository.clone() + } else { + repository.join(".git") + }; + let mut expected_paths = vec![git_dir]; + if !bare { + expected_paths.push(repository.clone()); + } + let mut checked_paths = Vec::new(); + let (_, trust) = discover_opts_with_trust(&repository, Options::default(), |path| { + checked_paths.push(path.to_path_buf()); + Ok(Trust::Reduced) + })?; + assert_eq!( + checked_paths, expected_paths, + "{name} discovery checks each distinct repository path exactly once" + ); + assert_eq!( + trust, + Trust::Reduced, + "skipping a duplicate must preserve ownership-derived trust" + ); + } + Ok(()) + } + + #[test] + fn ownership_of_every_linked_repository_path_limits_trust() -> gix_testtools::Result { + let root = gix_testtools::scripted_fixture_read_only("make_linked_ownership_repo.sh")?; + let main = root.join("main"); + let worktree = root.join("linked"); + + for foreign_path in [ + worktree.clone(), + worktree.join(".git"), + main.join(".git/worktrees/linked"), + main.join(".git"), + ] { + let foreign_path = foreign_path.canonicalize()?; + let mut ownership = |path: &std::path::Path| { + Ok(if path.canonicalize()? == foreign_path { + Trust::Reduced + } else { + Trust::Full + }) + }; + let (_, trust) = discover_opts_with_trust(&worktree, Options::default(), &mut ownership)?; + assert_eq!( + trust, + Trust::Reduced, + "untrusted {} must constrain discovery", + foreign_path.display() + ); + assert!( + discover_opts_with_trust( + &worktree, + Options { + trust: TrustPolicy::Required(Trust::Full), + ..Options::default() + }, + &mut ownership, + ) + .is_err(), + "requiring full trust rejects every untrusted link in the repository paths" + ); + } + let (_, trust) = discover_opts_with_trust( + &worktree, + Options { + trust: TrustPolicy::Assume(Trust::Full), + ..Options::default() + }, + |_| panic!("explicit trust overrides must not inspect ownership"), + )?; + assert_eq!(trust, Trust::Full, "explicit trust overrides remain authoritative"); + Ok(()) + } + } } diff --git a/gix-discover/src/upwards/types.rs b/gix-discover/src/upwards/types.rs index bd56bf6ec26..8206bc6c1ff 100644 --- a/gix-discover/src/upwards/types.rs +++ b/gix-discover/src/upwards/types.rs @@ -33,7 +33,7 @@ pub enum Error { candidate: PathBuf, /// The required trust level. required: gix_sec::Trust, - /// The candidate's actual trust level. + /// The minimum ownership-derived trust of the paths checked by [`TrustPolicy::Required`]. trust: gix_sec::Trust, }, } @@ -78,12 +78,25 @@ impl std::error::Error for Error { } } -/// How to obtain the trust level for a discovered repository. +/// How to obtain the trust level returned alongside a discovered [`repository::Path`][crate::repository::Path]. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum TrustPolicy { - /// Determine trust from repository ownership and require it to be at least the given level. + /// Derive trust from ownership via [`gix_sec::Trust::from_path_ownership()`] and require it to be at least + /// the given level. + /// + /// The returned trust is the minimum across the repository candidate itself (a gitfile or git directory), + /// its target git directory (if gitfile), the common directory referenced by `commondir` if present, and the worktree + /// known from repository detection if it exists. A missing worktree is skipped, but missing git or common + /// directories are errors. A trusted target cannot upgrade an untrusted gitfile. + /// + /// This trust describes all checked paths, not just the returned [`repository::Path`][crate::repository::Path], + /// which does not retain the original gitfile. The given level is only an acceptance threshold, not the + /// returned value: `Required(Reduced)` can return `Full` when all checked paths are owned by the current user. + /// If the minimum falls below the threshold, discovery fails with [`Error::NoTrustedGitRepository`]. Required(gix_sec::Trust), - /// Trust computation is skipped and the given trust level is assumed. + /// Return the given trust level unchanged, without computing trust from ownership. + /// + /// This is a caller-supplied assertion of trust, not an ownership-derived result. Assume(gix_sec::Trust), } @@ -96,7 +109,8 @@ impl Default for TrustPolicy { /// Options to help guide the [discovery][crate::upwards()] of repositories, along with their options /// when instantiated. pub struct Options<'a> { - /// When discovering a repository, determine how trust should be obtained. + /// Determine how discovery obtains its returned trust level; see [`TrustPolicy`] for the checked paths + /// and the distinction between an acceptance threshold and assumed trust. /// /// This defaults to [`Required(Reduced)`][TrustPolicy::Required] as our default settings are geared towards avoiding abuse. /// Set it to `Required(Full)` to only see repositories that [are owned by the current user][gix_sec::Trust::from_path_ownership()], diff --git a/gix-discover/tests/discover/upwards/mod.rs b/gix-discover/tests/discover/upwards/mod.rs index e73d27d8997..f40d050c880 100644 --- a/gix-discover/tests/discover/upwards/mod.rs +++ b/gix-discover/tests/discover/upwards/mod.rs @@ -436,41 +436,29 @@ fn relative_symlinks_use_the_configured_current_dir() -> Result { #[test] fn from_dir_with_dot_dot() -> Result { - // This would be neater if we could just change the actual working directory, - // but Rust tests run in parallel by default so we'd interfere with other tests. - // Instead ensure it finds the gitoxide repo instead of a test repo if we crawl - // up far enough. (This tests that `discover::existing` canonicalizes paths before - // exploring ancestors.) - let working_dir = repo_path()?; - let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../.."); - let (path, trust) = gix_discover::upwards(&dir)?; - assert_ne!( - path.as_ref().canonicalize()?, - working_dir.canonicalize()?, - "a relative path that climbs above the test repo should yield the parent-gitoxide repo" - ); - // If the parent repo is actually a main worktree, we can make more assertions. If it is not, - // it will use an absolute paths and we have to bail. - if path.as_ref() == std::path::Path::new("..") { + let outer = gix_testtools::scripted_fixture_read_only("make_nested_repos.sh")?; + let outer = std::env::current_dir()?.join(outer); + let inner = outer.join("inner"); + let relative = std::path::Path::new("some/very/deeply/nested/subdir/../../../../../.."); + for (input, expected) in [ + (relative.to_owned(), PathBuf::from("..")), + (inner.join(relative), outer.clone()), + ] { + let (path, trust) = gix_discover::upwards_opts( + &input, + gix_discover::upwards::Options { + current_dir: Some(&inner), + ..Default::default() + }, + )?; assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None }); assert_eq!( path.as_ref(), - std::path::Path::new(".."), - "there is only the minimal amount of relative path components to see this worktree" - ); - } else { - assert!( - path.as_ref().is_absolute(), - "worktree paths are absolute and the parent repo is one" + expected, + "normalizing dot-dot components discovers the outer repository with minimal path components" ); - assert!(matches!( - path.kind(), - Kind::WorkTree { - linked_git_dir: Some(_) - } - )); + assert_eq!(trust, expected_trust(), "the repository has the fixture's ownership"); } - assert_eq!(trust, expected_trust()); Ok(()) } diff --git a/gix-discover/tests/fixtures/generated-archives/.gitignore b/gix-discover/tests/fixtures/generated-archives/.gitignore index 7e849bb8847..c12a97b3787 100644 --- a/gix-discover/tests/fixtures/generated-archives/.gitignore +++ b/gix-discover/tests/fixtures/generated-archives/.gitignore @@ -3,3 +3,12 @@ # the host's working directory. Re-running the script on the test host produces # the correct paths for that machine. /make_basic_repo.tar + +# Only the nested repository layout matters; regenerate it with the host's Git templates. +/make_nested_repos.tar + +# Only the bare/worktree layout matters; regenerate it with the host's Git templates. +/make_ownership_repos.tar + +# Linked-worktree metadata contains absolute paths into the temporary fixture directory. +/make_linked_ownership_repo.tar diff --git a/gix-discover/tests/fixtures/make_linked_ownership_repo.sh b/gix-discover/tests/fixtures/make_linked_ownership_repo.sh new file mode 100755 index 00000000000..b8e4ae8d09b --- /dev/null +++ b/gix-discover/tests/fixtures/make_linked_ownership_repo.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Each of the checkout, gitfile, private git directory, and common git directory must limit trust. +# Run in the final fixture directory: Git writes absolute paths into linked-worktree metadata. +git init -q main +git -C main commit -q --allow-empty -m initial +git -C main worktree add -q --detach ../linked diff --git a/gix-discover/tests/fixtures/make_nested_repos.sh b/gix-discover/tests/fixtures/make_nested_repos.sh new file mode 100755 index 00000000000..638db4f5b27 --- /dev/null +++ b/gix-discover/tests/fixtures/make_nested_repos.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# Dot-dot normalization must skip the inner repository and discover the outer one. +# Both repositories are fixtures, not the potentially foreign-owned source checkout. +git init -q . +git init -q inner +mkdir -p inner/some/very/deeply/nested/subdir diff --git a/gix-discover/tests/fixtures/make_ownership_repos.sh b/gix-discover/tests/fixtures/make_ownership_repos.sh new file mode 100755 index 00000000000..f00961768dd --- /dev/null +++ b/gix-discover/tests/fixtures/make_ownership_repos.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +# In ordinary and bare repositories, the discovery candidate is also the git directory. +# Ownership tests must check that path once, plus the checkout for a non-bare repository. +git init -q worktree +git init -q --bare bare diff --git a/gix/src/open/options.rs b/gix/src/open/options.rs index abd050907cc..0280fbbad2a 100644 --- a/gix/src/open/options.rs +++ b/gix/src/open/options.rs @@ -111,15 +111,17 @@ impl Options { /// not be fully trusted, leading to limitations in how configuration files /// are interpreted. /// - /// If not called explicitly, it will be determined by looking at its - /// ownership via [`gix_sec::Trust::from_path_ownership()`]. + /// If not called explicitly, trust is determined via [`gix_sec::Trust::from_path_ownership()`] for the + /// candidate path before resolving a possible gitfile, then lowered as needed by ownership checks of the + /// resolved git directory, common directory, and existing worktree. Setting trust explicitly bypasses + /// these initial ownership checks when opening, but not when discovering a repository. /// /// # Security Warning /// /// Use with extreme care and only if it's absolutely known that the repository - /// is always controlled by the desired user. Using this capability _only_ saves - /// a permission check and only so if the [`open()`][Self::open()] method is used, - /// as opposed to discovery. + /// is always controlled by the desired user. This only bypasses the ownership checks described above + /// when the [`open()`][Self::open()] method is used, as opposed to discovery. Worktree ownership and + /// `safe.directory` are still considered later during opening. pub fn with(mut self, trust: gix_sec::Trust) -> Self { self.git_dir_trust = trust.into(); self diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 8e85627258a..e3d1b31cbb1 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -63,7 +63,10 @@ impl ThreadSafeRepository { /// `options` for fine-grained control. /// Empty `core.worktree` values include their bytes as `input` [metadata](gix_error::Error::metadata()). /// - /// Note that you should use [`crate::discover()`] if security should be adjusted by ownership. + /// Unless trust is set explicitly with [`Options::with()`], its initial level comes from ownership of the + /// candidate path (`path` or `path/.git`) before resolving a possible gitfile. Ownership checks of the resolved + /// git directory, common directory, and existing worktree can then only lower this level. + /// Use [`crate::discover()`] if options such as permissions should also be selected according to ownership. /// /// ### Differences to `git2::Repository::open_ext()` /// @@ -101,11 +104,22 @@ impl ThreadSafeRepository { // To be altered later based on `core.precomposeUnicode`. let cwd = gix_fs::current_dir(false).or_error()?; - let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, kind, &cwd) + let candidate_trust = options + .git_dir_trust + .is_none() + .then(|| gix_sec::Trust::from_path_ownership(&path)) + .transpose() + .or_error()?; + let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path.clone(), kind, &cwd) .expect("we have sanitized path with is_git()") .into_repository_and_work_tree_directories(); - if options.git_dir_trust.is_none() { - options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir).or_error()?.into(); + if let Some(candidate_trust) = candidate_trust { + options.git_dir_trust = Some(gix_discover::repository::trust( + &git_dir, + worktree_dir.as_deref(), + &cwd, + (&path, Some(candidate_trust)), + )?); } options.current_dir = Some(cwd); ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None) @@ -114,8 +128,10 @@ impl ThreadSafeRepository { /// Try to open a git repository in `fallback_directory` (can be worktree or `.git` directory) only if there is no override /// of the `gitdir` using git environment variables. /// - /// Use the `trust_map` to apply options depending in the trust level for `directory` or the directory it's overridden with. - /// The `.git` directory whether given or computed is used for trust checks. + /// Use `trust_map` to select options according to the ownership-derived trust level. Initial trust comes from + /// the candidate path (`GIT_DIR` if set, otherwise `fallback_directory`) before resolving a possible gitfile. + /// Ownership checks of the resolved git directory, common directory, and existing worktree can only lower + /// that level, so a trusted target cannot upgrade an untrusted gitfile. /// /// Note that this will read various `GIT_*` environment variables to check for overrides, and is probably most useful when implementing /// custom hooks. @@ -145,12 +161,14 @@ impl ThreadSafeRepository { // To be altered later based on `core.precomposeUnicode`. let cwd = gix_fs::current_dir(false).or_error()?; - let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path, path_kind, &cwd) + let candidate_trust = gix_sec::Trust::from_path_ownership(&path).or_error()?; + let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path.clone(), path_kind, &cwd) .expect("we have sanitized path with is_git()") .into_repository_and_work_tree_directories(); let worktree_dir = worktree_dir.or(overrides.worktree_dir); - let git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir).or_error()?; + let git_dir_trust = + gix_discover::repository::trust(&git_dir, worktree_dir.as_deref(), &cwd, (&path, Some(candidate_trust)))?; let mut options = trust_map.into_value_by_level(git_dir_trust); options.git_dir_trust = git_dir_trust.into(); options.current_dir = Some(cwd); From 2eeb6048a60f8534fa54a6c1a7ba6e700c131580 Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Wed, 30 Sep 2026 10:05:57 +0200 Subject: [PATCH 26/28] fix(gix-path)!: return path conversion errors instead of panicking <!-- Byron --> Also brought TestResult in there for nicer error messages, presumably, and if anything then at least it's now consistent. <!-- agent --> Git paths can contain arbitrary bytes on Unix, while native Windows paths can contain unpaired surrogates that cannot be represented as UTF-8. The canonical conversion functions previously hid these failures behind panicking wrappers, even when their callers already returned errors. Make `into_bstr`, `from_bstr`, `from_bstring`, `from_byte_slice`, and `to_native_path_on_windows` fallible and remove their redundant `try_*` counterparts. Keep borrowed inputs borrowed and owned inputs owned, and retain the concrete encoding error as a source. Also return an error from `normalize_saturating` when normalization exhausts a relative or empty CWD. Adapt the workspace in the same breaking change, including public APIs for reference and reflog paths, worktree IDs, configuration, pathspecs, pattern sources, and command preparation. Convert `Prepare` to `Command` with `TryFrom`; inspect ASCII shell syntax through native encoded bytes and preserve native commands without a Git-path round trip. Keep reference names as bytes in missing-reference diagnostics. Propagate conversion failures before affected command execution, filter exchanges, and watcher updates. Debug logging records failed diagnostic conversions without changing pathspec behavior. Add regression coverage for arbitrary Unix bytes, Windows encoding failures, ownership, lazy configuration selection, and dependent operations. Assisted-by: GPT 6.0 Astra Co-authored-by: GPT 6.0 Astra <codex@openai.com> --- gitoxide-core/src/pack/receive.rs | 3 +- gitoxide-core/src/query/engine/command.rs | 4 +- .../src/repository/attributes/query.rs | 9 +- .../attributes/validate_baseline.rs | 4 +- gitoxide-core/src/repository/blame.rs | 2 +- gitoxide-core/src/repository/clean.rs | 6 +- gitoxide-core/src/repository/clone.rs | 2 +- gitoxide-core/src/repository/diff.rs | 4 +- gitoxide-core/src/repository/editor.rs | 2 +- gitoxide-core/src/repository/exclude.rs | 2 +- gitoxide-core/src/repository/index/mod.rs | 2 +- gitoxide-core/src/repository/status.rs | 18 +- gix-actor/tests/actor/identity.rs | 6 +- gix-actor/tests/actor/signature.rs | 23 +- gix-archive/src/write.rs | 4 +- gix-archive/tests/archive.rs | 30 +- gix-attributes/benches/lookup.rs | 32 ++- .../fuzz/fuzz_targets/fuzz_search.rs | 18 +- gix-attributes/src/lib.rs | 3 +- gix-attributes/src/search/attributes.rs | 7 +- gix-attributes/tests/attributes/search.rs | 26 +- gix-attributes/tests/attributes/source.rs | 16 +- gix-bitmap/tests/bitmap.rs | 35 +-- gix-blame/tests/blame.rs | 44 ++- gix-command/fuzz/fuzz_targets/prepare.rs | 2 +- gix-command/src/lib.rs | 6 +- gix-command/src/parse.rs | 2 +- gix-command/src/prepare.rs | 40 +-- gix-command/src/tests.rs | 2 +- gix-command/tests/command/command_line.rs | 8 +- gix-command/tests/command/context.rs | 35 ++- gix-command/tests/command/main.rs | 4 +- gix-command/tests/command/prepare.rs | 264 ++++++++++++------ gix-command/tests/command/spawn.rs | 48 ++-- gix-commitgraph/tests/commitgraph.rs | 4 +- gix-config-value/src/path.rs | 11 +- gix-config-value/tests/value/boolean.rs | 19 +- gix-config-value/tests/value/color.rs | 4 +- gix-config-value/tests/value/integer.rs | 8 +- gix-config-value/tests/value/path.rs | 36 ++- gix-config/src/file/includes/mod.rs | 12 +- gix-config/tests/config/file/access/mutate.rs | 50 ++-- .../config/file/access/raw/raw_multi_value.rs | 22 +- .../tests/config/file/access/raw/raw_value.rs | 26 +- .../file/access/raw/set_existing_raw_value.rs | 4 +- .../config/file/access/raw/set_raw_value.rs | 6 +- .../tests/config/file/access/read_only.rs | 56 ++-- gix-config/tests/config/file/impls.rs | 4 +- gix-config/tests/config/file/init/comfort.rs | 10 +- gix-config/tests/config/file/init/from_env.rs | 16 +- .../includes/conditional/gitdir/mod.rs | 128 ++++----- .../includes/conditional/hasconfig.rs | 12 +- .../from_paths/includes/conditional/mod.rs | 4 +- .../includes/conditional/onbranch.rs | 16 +- .../init/from_paths/includes/unconditional.rs | 12 +- .../tests/config/file/init/from_paths/mod.rs | 8 +- gix-config/tests/config/file/init/from_str.rs | 6 +- gix-config/tests/config/file/mod.rs | 4 +- .../tests/config/file/mutable/multi_value.rs | 30 +- .../tests/config/file/mutable/section.rs | 60 ++-- gix-config/tests/config/file/mutable/value.rs | 26 +- .../tests/config/file/resolve_includes.rs | 4 +- gix-config/tests/config/file/write.rs | 27 +- gix-config/tests/config/main.rs | 2 +- gix-config/tests/config/parse/from_bytes.rs | 6 +- gix-config/tests/config/parse/section.rs | 6 +- gix-credentials/src/helper/cascade.rs | 6 +- gix-credentials/src/program/mod.rs | 46 +-- gix-credentials/tests/credentials.rs | 2 +- gix-credentials/tests/helper/cascade.rs | 111 ++++---- gix-credentials/tests/helper/context.rs | 34 +-- gix-credentials/tests/helper/invoke.rs | 34 +-- gix-credentials/tests/helper/mod.rs | 2 +- .../tests/program/from_custom_definition.rs | 67 +++-- gix-credentials/tests/program/main.rs | 8 +- gix-credentials/tests/protocol/context.rs | 20 +- gix-date/tests/time/baseline.rs | 25 +- gix-date/tests/time/format.rs | 18 +- gix-date/tests/time/main.rs | 8 +- gix-date/tests/time/parse/mod.rs | 4 +- gix-date/tests/time/parse/relative.rs | 16 +- gix-diff/src/blob/pipeline.rs | 72 +++-- gix-diff/src/blob/platform.rs | 8 +- gix-diff/tests/diff/blob/pipeline.rs | 18 +- gix-diff/tests/diff/blob/platform.rs | 40 ++- gix-diff/tests/diff/blob/slider.rs | 24 +- gix-diff/tests/diff/blob/textconv.rs | 2 +- gix-diff/tests/diff/blob/unified_diff.rs | 18 +- gix-diff/tests/diff/index.rs | 51 ++-- gix-diff/tests/diff/rewrites/tracker.rs | 34 +-- gix-diff/tests/diff/tree.rs | 12 +- gix-diff/tests/diff/tree_with_rewrites.rs | 56 ++-- gix-dir/src/walk/classify.rs | 2 +- gix-dir/src/walk/function.rs | 17 +- gix-dir/src/walk/readdir.rs | 6 +- gix-dir/tests/dir/walk.rs | 199 +++++++------ gix-dir/tests/dir_cwd.rs | 2 +- gix-discover/src/parse.rs | 2 +- gix-discover/src/path.rs | 9 +- gix-discover/src/repository.rs | 4 +- gix-discover/src/upwards/mod.rs | 4 +- gix-discover/src/upwards/types.rs | 20 +- gix-discover/tests/discover/is_git.rs | 24 +- gix-discover/tests/discover/main.rs | 2 - gix-discover/tests/discover/parse.rs | 4 +- gix-discover/tests/discover/path.rs | 18 +- .../tests/discover/upwards/ceiling_dirs.rs | 34 +-- gix-discover/tests/discover/upwards/mod.rs | 92 +++--- gix-discover/tests/isolated.rs | 37 ++- gix-features/tests/parallel/in_order_iter.rs | 12 +- gix-features/tests/pipe.rs | 50 ++-- gix-filter/src/driver/init.rs | 4 +- gix-filter/src/pipeline/convert.rs | 2 +- gix-filter/tests/filter/driver.rs | 39 ++- gix-filter/tests/filter/eol/convert_to_git.rs | 48 ++-- .../tests/filter/eol/convert_to_worktree.rs | 14 +- gix-filter/tests/filter/ident.rs | 22 +- .../tests/filter/pipeline/convert_to_git.rs | 8 +- .../filter/pipeline/convert_to_worktree.rs | 14 +- gix-filter/tests/filter/pipeline/mod.rs | 4 +- gix-filter/tests/filter/worktree.rs | 8 +- gix-fs/src/stack.rs | 2 +- gix-fs/tests/current_dir.rs | 2 +- gix-fs/tests/fs/capabilities.rs | 22 +- gix-fs/tests/fs/dir/create.rs | 43 ++- gix-fs/tests/fs/dir/remove.rs | 24 +- gix-fs/tests/fs/file.rs | 6 +- gix-fs/tests/fs/main.rs | 2 +- gix-fs/tests/fs/read_dir.rs | 4 +- gix-fs/tests/fs/snapshot.rs | 4 +- gix-fs/tests/fs/stack.rs | 21 +- gix-fs/tests/fs/walkdir.rs | 8 +- gix-glob/src/search/pattern.rs | 42 ++- gix-glob/tests/glob/pattern/matching.rs | 2 +- gix-glob/tests/glob/search/pattern.rs | 27 +- gix-glob/tests/pattern_list.rs | 2 + gix-hash/tests/hash/change_id.rs | 4 +- gix-hash/tests/hash/object_id.rs | 33 ++- gix-hash/tests/hash/prefix.rs | 12 +- gix-ignore/fuzz/fuzz_targets/ignore.rs | 4 +- gix-ignore/src/search.rs | 7 +- gix-ignore/tests/ignore/search.rs | 6 +- gix-index/tests/index/file/access.rs | 4 +- gix-index/tests/index/file/init.rs | 18 +- gix-index/tests/index/file/write.rs | 12 +- gix-index/tests/index/fs.rs | 4 +- gix-index/tests/index/fuzzed.rs | 12 +- gix-index/tests/index/init.rs | 12 +- gix-lock/src/acquire.rs | 6 +- gix-lock/tests/lock/file.rs | 20 +- gix-lock/tests/lock/marker.rs | 6 +- gix-mailmap/tests/mailmap/snapshot.rs | 10 +- gix-merge/src/blob/pipeline.rs | 6 +- gix-merge/src/blob/platform/merge.rs | 10 +- gix-merge/tests/merge/blob/builtin_driver.rs | 4 +- gix-merge/tests/merge/blob/pipeline.rs | 12 +- gix-merge/tests/merge/blob/platform.rs | 36 ++- gix-merge/tests/merge/tree/cartesian.rs | 4 +- gix-merge/tests/merge/tree/mod.rs | 6 +- gix-negotiate/tests/baseline/mod.rs | 8 +- gix-negotiate/tests/negotiate.rs | 2 +- gix-note/tests/note.rs | 46 +-- gix-object/src/signature/mod.rs | 4 +- gix-object/src/signature/sign.rs | 11 +- gix-object/src/signature/verify.rs | 16 +- gix-object/tests/object/commit/from_bytes.rs | 46 +-- gix-object/tests/object/commit/iter.rs | 40 ++- gix-object/tests/object/commit/mod.rs | 6 +- gix-object/tests/object/commit/signature.rs | 10 +- gix-object/tests/object/encode.rs | 12 +- gix-object/tests/object/main.rs | 18 +- gix-object/tests/object/signature_format.rs | 2 +- gix-object/tests/object/tag/mod.rs | 42 ++- gix-object/tests/object/tag/signature.rs | 10 +- gix-object/tests/object/tree/editor.rs | 56 ++-- gix-object/tests/object/tree/entries.rs | 3 +- gix-object/tests/object/tree/from_bytes.rs | 9 +- gix-object/tests/object/tree/iter.rs | 13 +- gix-odb/src/alternate/parse.rs | 2 +- gix-odb/src/store_impls/dynamic/load_index.rs | 2 +- gix-odb/tests/odb/alternate.rs | 11 +- gix-odb/tests/odb/error.rs | 27 +- gix-odb/tests/odb/memory.rs | 26 +- gix-odb/tests/odb/regression.rs | 8 +- gix-odb/tests/odb/sink.rs | 3 +- gix-odb/tests/odb/store/dynamic.rs | 27 +- gix-odb/tests/odb/store/linked.rs | 10 +- gix-odb/tests/odb/store/loose.rs | 47 ++-- gix-pack/src/cache/delta/traverse/resolve.rs | 70 +++-- gix-pack/src/cache/delta/tree.rs | 18 +- gix-pack/src/data/entry/decode.rs | 12 +- gix-pack/src/multi_index/chunk.rs | 2 +- gix-pack/tests/pack/bundle.rs | 22 +- gix-pack/tests/pack/data/file.rs | 7 +- gix-pack/tests/pack/data/fuzzed.rs | 124 ++++---- gix-pack/tests/pack/data/header.rs | 3 +- gix-pack/tests/pack/data/input.rs | 4 +- .../pack/data/output/count_and_entries.rs | 10 +- gix-pack/tests/pack/index.rs | 21 +- gix-pack/tests/pack/iter.rs | 12 +- gix-pack/tests/pack/malformed.rs | 36 +-- gix-pack/tests/pack/multi_index/access.rs | 13 +- gix-pack/tests/pack/multi_index/fuzzed.rs | 19 +- gix-pack/tests/pack/multi_index/verify.rs | 3 +- gix-pack/tests/pack/multi_index/write.rs | 4 +- gix-packetline/tests/packetline/decode.rs | 30 +- gix-packetline/tests/packetline/encode.rs | 8 +- gix-packetline/tests/packetline/read/mod.rs | 19 +- .../tests/packetline/read/sideband.rs | 12 +- gix-packetline/tests/packetline/write.rs | 6 +- gix-path/src/convert.rs | 113 +++----- gix-path/src/env/git/tests.rs | 14 +- gix-path/src/env/mod.rs | 4 +- gix-path/src/env/tests.rs | 15 +- gix-path/src/lib.rs | 53 +--- gix-path/src/relative_path.rs | 20 +- gix-path/tests/path/convert/mod.rs | 158 ++++++++++- gix-path/tests/path/convert/normalize.rs | 2 +- gix-path/tests/path/env.rs | 16 +- gix-path/tests/path/realpath.rs | 20 +- gix-pathspec/src/pattern.rs | 8 +- gix-pathspec/src/search/mod.rs | 18 +- gix-pathspec/tests/defaults.rs | 10 +- gix-pathspec/tests/normalize/mod.rs | 14 +- gix-pathspec/tests/parse/valid.rs | 28 +- gix-pathspec/tests/search/mod.rs | 68 ++--- gix-prompt/tests/options/mod.rs | 14 +- gix-prompt/tests/prompt.rs | 8 +- gix-protocol/src/ls_refs.rs | 24 +- gix-protocol/tests/protocol/fetch/response.rs | 38 +-- gix-protocol/tests/protocol/fetch/v1.rs | 13 +- gix-protocol/tests/protocol/fetch/v2.rs | 12 +- gix-protocol/tests/protocol/handshake.rs | 23 +- gix-protocol/tests/protocol/mod.rs | 2 +- gix-quote/tests/quote.rs | 14 +- gix-ref/src/fullname.rs | 7 +- gix-ref/src/name.rs | 3 +- gix-ref/src/namespace.rs | 3 +- gix-ref/src/store/file/find.rs | 80 +++--- gix-ref/src/store/file/log/iter.rs | 12 +- gix-ref/src/store/file/loose/iter.rs | 2 +- .../src/store/file/loose/reference/logiter.rs | 16 +- gix-ref/src/store/file/loose/reflog.rs | 50 ++-- .../loose/reflog/create_or_update/tests.rs | 14 +- gix-ref/src/store/file/overlay_iter.rs | 44 +-- gix-ref/src/store/file/raw_ext.rs | 12 +- gix-ref/src/store/file/transaction/commit.rs | 4 +- gix-ref/src/store/file/transaction/prepare.rs | 4 +- gix-ref/src/store/packed/decode/tests.rs | 16 +- gix-ref/src/store/packed/find.rs | 2 +- gix-ref/src/store/packed/transaction.rs | 64 ++--- gix-ref/tests/refs/equality.rs | 6 +- gix-ref/tests/refs/error.rs | 29 +- gix-ref/tests/refs/file/log.rs | 22 +- gix-ref/tests/refs/file/reference.rs | 29 +- gix-ref/tests/refs/file/store/access.rs | 5 +- gix-ref/tests/refs/file/store/find.rs | 23 +- gix-ref/tests/refs/file/store/iter.rs | 47 ++-- gix-ref/tests/refs/file/store/mod.rs | 5 +- gix-ref/tests/refs/file/store/reflog.rs | 12 +- .../create_or_update/collisions.rs | 10 +- .../create_or_update/mod.rs | 48 ++-- .../transaction/prepare_and_commit/delete.rs | 60 ++-- gix-ref/tests/refs/file/worktree.rs | 16 +- gix-ref/tests/refs/fullname.rs | 27 +- gix-ref/tests/refs/main.rs | 3 +- gix-ref/tests/refs/packed/find.rs | 15 +- gix-ref/tests/refs/packed/iter.rs | 15 +- gix-ref/tests/refs/packed/open.rs | 5 +- gix-ref/tests/refs/transaction.rs | 16 +- gix-ref/tests/transaction_fd_limit.rs | 2 +- gix-refspec/tests/refspec/parse/mod.rs | 19 +- gix-revision/tests/revision/describe/mod.rs | 42 +-- gix-revision/tests/revision/merge_base.rs | 12 +- gix-status/src/fscache.rs | 2 +- gix-status/src/index_as_worktree/function.rs | 4 +- .../src/index_as_worktree_with_renames/mod.rs | 6 +- gix-status/src/stack.rs | 3 +- gix-status/tests/status/index_as_worktree.rs | 16 +- gix-status/tests/status/stack.rs | 10 +- gix-submodule/src/access.rs | 2 +- gix-submodule/tests/file/baseline.rs | 4 +- gix-submodule/tests/file/mod.rs | 54 ++-- gix-tempfile/src/signal.rs | 2 +- gix-tempfile/tests/all.rs | 2 +- gix-tempfile/tests/registry.rs | 2 +- gix-tempfile/tests/tempfile/handle.rs | 30 +- gix-tix/spec.md | 6 +- gix-tix/src/edit/create.rs | 4 +- gix-tix/src/edit/discard.rs | 12 +- gix-tix/src/edit/mod.rs | 4 +- gix-tix/src/edit/replay_refs/mod.rs | 4 +- gix-tix/src/edit/reword.rs | 13 +- gix-tix/src/edit/time_travel.rs | 7 +- gix-tix/src/history.rs | 33 ++- gix-tix/src/lib.rs | 96 ++++--- gix-tix/src/ref_tree.rs | 2 +- gix-tix/src/worktrunk/mod.rs | 11 +- gix-trace/tests/forest.rs | 96 +++---- gix-trace/tests/forest_init.rs | 2 +- gix-transport/src/client/blocking_io/file.rs | 32 ++- .../client/blocking_io/http/curl/remote.rs | 6 +- .../src/client/blocking_io/ssh/mod.rs | 5 +- .../src/client/blocking_io/ssh/tests.rs | 6 +- gix-transport/src/client/git/mod.rs | 48 ++-- gix-transport/tests/async-transport.rs | 2 +- .../tests/blocking-transport-http-reqwest.rs | 16 +- .../tests/blocking-transport-http.rs | 2 +- gix-transport/tests/blocking-transport.rs | 2 +- .../tests/client/blocking_io/http/mod.rs | 56 ++-- gix-transport/tests/client/capabilities.rs | 15 +- gix-transport/tests/client/git.rs | 26 +- .../traverse/commit/simple/adjusted_dates.rs | 12 +- .../traverse/commit/simple/different_date.rs | 4 +- .../simple/different_date_intermixed.rs | 4 +- .../tests/traverse/commit/simple/hide.rs | 16 +- .../tests/traverse/commit/simple/same_date.rs | 14 +- gix-traverse/tests/traverse/commit/topo.rs | 25 +- gix-traverse/tests/traverse/tree.rs | 23 +- gix-url/src/expand_path.rs | 2 +- gix-url/src/impls.rs | 8 +- gix-url/src/lib.rs | 6 +- gix-url/src/simple_url.rs | 6 +- gix-url/tests/url/access.rs | 58 ++-- gix-url/tests/url/baseline.rs | 15 +- gix-url/tests/url/expand_path.rs | 4 +- gix-url/tests/url/parse/file.rs | 48 ++-- gix-url/tests/url/parse/http.rs | 52 ++-- gix-url/tests/url/parse/mod.rs | 14 +- gix-url/tests/url/parse/remote_helper.rs | 30 +- gix-url/tests/url/parse/ssh.rs | 78 +++--- gix-worktree-state/src/checkout/entry.rs | 20 +- .../tests/worktree-state/checkout.rs | 71 +++-- .../tests/worktree-state/main.rs | 2 +- gix-worktree-stream/src/lib.rs | 9 +- gix-worktree-stream/tests/stream.rs | 23 +- gix-worktree/src/add.rs | 4 +- gix-worktree/src/remove.rs | 8 +- gix-worktree/src/stack/delegate.rs | 4 +- gix-worktree/src/stack/mod.rs | 10 +- gix-worktree/src/stack/platform.rs | 8 +- gix-worktree/src/stack/state/attributes.rs | 12 +- gix-worktree/src/stack/state/ignore.rs | 16 +- gix-worktree/tests/worktree/add.rs | 22 +- gix-worktree/tests/worktree/remove.rs | 18 +- .../tests/worktree/stack/attributes.rs | 4 +- .../tests/worktree/stack/create_directory.rs | 20 +- gix-worktree/tests/worktree/stack/ignore.rs | 13 +- gix-zlib/tests/zlib/decompress.rs | 11 +- gix/src/commit/mod.rs | 14 +- gix/src/commit/sign.rs | 15 +- gix/src/config/cache/access.rs | 12 +- gix/src/config/snapshot/access.rs | 11 +- gix/src/config/snapshot/credential_helpers.rs | 2 +- gix/src/filter.rs | 6 +- gix/src/open/repository.rs | 8 +- gix/src/reference/log.rs | 2 +- gix/src/remote/connect.rs | 19 +- .../remote/connection/fetch/receive_pack.rs | 2 +- .../connection/fetch/update_refs/tests.rs | 38 +-- gix/src/remote/url/scheme_permission.rs | 8 +- gix/src/repository/config/mod.rs | 26 +- gix/src/repository/dirwalk.rs | 4 +- gix/src/repository/location.rs | 11 +- gix/src/repository/remote.rs | 5 +- gix/src/repository/shallow.rs | 18 +- gix/src/repository/worktree.rs | 12 +- .../revision/spec/parse/delegate/navigate.rs | 10 +- .../revision/spec/parse/delegate/revision.rs | 2 +- gix/src/revision/spec/parse/mod.rs | 6 +- gix/src/submodule/mod.rs | 17 +- gix/src/worktree/mod.rs | 21 +- gix/src/worktree/proxy.rs | 4 +- gix/src/worktree/remove.rs | 25 +- gix/tests/gix-init.rs | 44 +-- gix/tests/gix/clone.rs | 129 ++++----- gix/tests/gix/commit.rs | 34 +-- gix/tests/gix/config/file_mut.rs | 20 +- gix/tests/gix/config/tree.rs | 174 ++++++------ gix/tests/gix/diff.rs | 4 +- gix/tests/gix/error.rs | 2 +- gix/tests/gix/head.rs | 12 +- gix/tests/gix/id.rs | 20 +- gix/tests/gix/init.rs | 24 +- gix/tests/gix/object/commit.rs | 8 +- gix/tests/gix/object/mod.rs | 4 +- gix/tests/gix/object/tree/diff.rs | 17 +- gix/tests/gix/object/tree/mod.rs | 26 +- gix/tests/gix/reference/mod.rs | 20 +- gix/tests/gix/reference/remote.rs | 12 +- gix/tests/gix/remote/connect.rs | 8 +- gix/tests/gix/remote/fetch.rs | 25 +- gix/tests/gix/remote/ref_map.rs | 4 +- gix/tests/gix/remote/save.rs | 24 +- gix/tests/gix/repository/blame.rs | 6 +- gix/tests/gix/repository/branch.rs | 18 +- .../config_snapshot/credential_helpers.rs | 12 +- .../repository/config/config_snapshot/mod.rs | 40 +-- gix/tests/gix/repository/config/editor.rs | 8 +- gix/tests/gix/repository/config/identity.rs | 24 +- gix/tests/gix/repository/config/mod.rs | 14 +- gix/tests/gix/repository/config/remote.rs | 30 +- gix/tests/gix/repository/excludes.rs | 14 +- gix/tests/gix/repository/filter.rs | 16 +- gix/tests/gix/repository/mailmap.rs | 10 +- gix/tests/gix/repository/merge.rs | 4 +- gix/tests/gix/repository/mod.rs | 26 +- gix/tests/gix/repository/note.rs | 14 +- gix/tests/gix/repository/object.rs | 75 +++-- gix/tests/gix/repository/open.rs | 94 +++---- gix/tests/gix/repository/pathspec.rs | 6 +- gix/tests/gix/repository/reference.rs | 24 +- gix/tests/gix/repository/remote.rs | 59 ++-- gix/tests/gix/repository/revision.rs | 10 +- gix/tests/gix/repository/shallow.rs | 16 +- gix/tests/gix/repository/state.rs | 18 +- gix/tests/gix/repository/submodule.rs | 22 +- gix/tests/gix/repository/worktree.rs | 144 +++++----- .../gix/revision/spec/from_bytes/ambiguous.rs | 28 +- gix/tests/gix/revision/spec/from_bytes/mod.rs | 67 +++-- .../gix/revision/spec/from_bytes/reflog.rs | 12 +- .../gix/revision/spec/from_bytes/regex.rs | 4 +- .../gix/revision/spec/from_bytes/traverse.rs | 12 +- gix/tests/gix/status.rs | 50 ++-- gix/tests/gix/submodule.rs | 50 ++-- gix/tests/interrupt.rs | 24 +- gix/tests/repository.rs | 26 +- gix/tests/submodule_open.rs | 2 +- src/plumbing/main.rs | 16 +- src/shared.rs | 6 +- tests/it/src/args.rs | 2 +- tests/it/src/commands/copy_royal.rs | 2 +- .../invalid-branch-name-failure | 2 +- tests/tools/src/lib.rs | 12 +- tests/tools/src/repository/gix.rs | 2 +- tests/tools/src/repository/mod.rs | 22 +- tests/tools/src/tests.rs | 190 ++++++------- tests/tools/tests/env.rs | 12 +- tests/tools/tests/sbom.rs | 6 +- tests/tools/tests/tools/main.rs | 4 +- tests/tools/tests/tools/repository.rs | 19 +- tests/tools/tests/tools/rust_fixture.rs | 8 +- .../tests/tools/scripted_fixture_with_post.rs | 12 +- tests/tools/tests/umask.rs | 12 +- 444 files changed, 4726 insertions(+), 4556 deletions(-) create mode 100644 gix-glob/tests/pattern_list.rs diff --git a/gitoxide-core/src/pack/receive.rs b/gitoxide-core/src/pack/receive.rs index ab8d73e15e8..5ed7dbf4f79 100644 --- a/gitoxide-core/src/pack/receive.rs +++ b/gitoxide-core/src/pack/receive.rs @@ -255,7 +255,8 @@ fn write_raw_refs(refs: &[Ref], directory: PathBuf) -> std::io::Result<()> { let assure_dir_exists = |path: &BString| { gix::validate::reference::name(path.as_bstr()) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - let path = directory.join(gix::path::from_byte_slice(path)); + let path = directory + .join(gix::path::from_byte_slice(path).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?); std::fs::create_dir_all(path.parent().expect("multi-component path")).map(|_| path) }; for r in refs { diff --git a/gitoxide-core/src/query/engine/command.rs b/gitoxide-core/src/query/engine/command.rs index b2afb34424d..dfbbdf524d3 100644 --- a/gitoxide-core/src/query/engine/command.rs +++ b/gitoxide-core/src/query/engine/command.rs @@ -25,9 +25,9 @@ impl query::Engine { let is_excluded = spec.is_excluded(); let relpath = if spec.signature.contains(gix::pathspec::MagicSignature::TOP) { let root = self.repo.workdir().unwrap_or_else(|| self.repo.git_dir()); - let path = root.join(gix::path::from_bstr(spec.path()).as_ref()); + let path = root.join(gix::path::from_bstr(spec.path())?.as_ref()); self.repo - .normalize_path(gix::path::into_bstr(path).as_ref())? + .normalize_path(gix::path::into_bstr(path)?.as_ref())? .into_owned() } else { self.repo.normalize_path(spec.path())?.into_owned() diff --git a/gitoxide-core/src/repository/attributes/query.rs b/gitoxide-core/src/repository/attributes/query.rs index 945a745d676..862857d5de7 100644 --- a/gitoxide-core/src/repository/attributes/query.rs +++ b/gitoxide-core/src/repository/attributes/query.rs @@ -41,7 +41,7 @@ pub(crate) mod function { match input { PathsOrPatterns::Paths(paths) => { for path in paths { - let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))? .metadata() .ok() .map(|m| is_dir_to_mode(m.is_dir())); @@ -88,11 +88,14 @@ pub(crate) mod function { let workdir = repo.workdir(); for pattern in pathspec.search().patterns() { let path = pattern.path(); + let is_dir = match workdir { + Some(wd) => wd.join(gix::path::from_bstr(path)?).is_dir(), + None => false, + }; let entry = cache.at_entry( path, Some(is_dir_to_mode( - workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir()) - || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), + is_dir || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR), )), )?; if !entry.matching_attributes(&mut matches) { diff --git a/gitoxide-core/src/repository/attributes/validate_baseline.rs b/gitoxide-core/src/repository/attributes/validate_baseline.rs index 114b500396b..dbfd197e939 100644 --- a/gitoxide-core/src/repository/attributes/validate_baseline.rs +++ b/gitoxide-core/src/repository/attributes/validate_baseline.rs @@ -79,7 +79,7 @@ pub(crate) mod function { let mut progress = progress.add_child("attributes"); move || -> Result<()> { let mut child = - std::process::Command::from(gix::command::prepare(gix::path::env::exe_invocation())) + std::process::Command::try_from(gix::command::prepare(gix::path::env::exe_invocation()))? .args(["check-attr", "--stdin", "-a"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) @@ -132,7 +132,7 @@ pub(crate) mod function { let mut progress = progress.add_child("excludes"); move || -> Result<()> { let mut child = - std::process::Command::from(gix::command::prepare(gix::path::env::exe_invocation())) + std::process::Command::try_from(gix::command::prepare(gix::path::env::exe_invocation()))? .args(["check-ignore", "--stdin", "-nv", "--no-index"]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) diff --git a/gitoxide-core/src/repository/blame.rs b/gitoxide-core/src/repository/blame.rs index 178829b4c86..ce9c2797619 100644 --- a/gitoxide-core/src/repository/blame.rs +++ b/gitoxide-core/src/repository/blame.rs @@ -65,7 +65,7 @@ fn start_for_blame<'a>( let Some(workdir) = repo.workdir() else { return Ok(Start::Commit(first_suspect)); }; - let path = workdir.join(gix::path::from_bstr(file)); + let path = workdir.join(gix::path::from_bstr(file)?); let metadata = match std::fs::symlink_metadata(&path) { Ok(metadata) => metadata, Err(err) if gix::fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => { diff --git a/gitoxide-core/src/repository/clean.rs b/gitoxide-core/src/repository/clean.rs index fee7b59ace8..e1cf72891be 100644 --- a/gitoxide-core/src/repository/clean.rs +++ b/gitoxide-core/src/repository/clean.rs @@ -167,7 +167,7 @@ pub(crate) mod function { }; if entry.disk_kind.is_none() { entry.disk_kind = workdir - .join(gix::path::from_bstr(entry.rela_path.as_bstr())) + .join(gix::path::from_bstr(entry.rela_path.as_bstr())?) .symlink_metadata() .ok() .map(|e| e.file_type().into()); @@ -186,7 +186,7 @@ pub(crate) mod function { } if disk_kind == gix::dir::entry::Kind::Directory - && gix::discover::is_git(&workdir.join(gix::path::from_bstr(entry.rela_path.as_bstr()))).is_ok() + && gix::discover::is_git(&workdir.join(gix::path::from_bstr(entry.rela_path.as_bstr())?)).is_ok() { if debug { writeln!( @@ -228,7 +228,7 @@ pub(crate) mod function { } let is_ignored = matches!(entry.status, gix::dir::entry::Status::Ignored(_)); - let entry_path = gix::path::from_bstr(entry.rela_path); + let entry_path = gix::path::from_bstr(entry.rela_path)?; let display_path = gix::path::relativize_with_prefix(&entry_path, prefix); if disk_kind == gix::dir::entry::Kind::Directory { saw_ignored_directory |= is_ignored; diff --git a/gitoxide-core/src/repository/clone.rs b/gitoxide-core/src/repository/clone.rs index 2087cea9590..d7f158bc732 100644 --- a/gitoxide-core/src/repository/clone.rs +++ b/gitoxide-core/src/repository/clone.rs @@ -53,7 +53,7 @@ pub(crate) mod function { let url: gix::Url = url.as_ref().try_into()?; let directory = directory.map_or_else( || { - let path = gix::path::from_bstr(Cow::Borrowed(url.path.as_ref())); + let path = gix::path::from_bstr(Cow::Borrowed(url.path.as_ref()))?; if !bare && path.extension() == Some(OsStr::new("git")) { path.file_stem().map(Into::into) } else { diff --git a/gitoxide-core/src/repository/diff.rs b/gitoxide-core/src/repository/diff.rs index ceae29ad963..57db038e83e 100644 --- a/gitoxide-core/src/repository/diff.rs +++ b/gitoxide-core/src/repository/diff.rs @@ -130,9 +130,7 @@ fn resolve_revspec( let not_found = err.downcast_any_ref::<gix::refs::file::find::NotFound>(); if let Some(gix::refs::file::find::NotFound { name }) = not_found { let root = repo.workdir().map(ToOwned::to_owned); - let name = gix::path::os_string_into_bstring(name.into())?; - - Ok((ObjectId::null(gix::hash::Kind::Sha1), root, name)) + Ok((ObjectId::null(gix::hash::Kind::Sha1), root, name.clone())) } else { Err(err) } diff --git a/gitoxide-core/src/repository/editor.rs b/gitoxide-core/src/repository/editor.rs index 6307db22ff6..dec2541a534 100644 --- a/gitoxide-core/src/repository/editor.rs +++ b/gitoxide-core/src/repository/editor.rs @@ -10,7 +10,7 @@ pub fn function(repo: gix::Repository, paths: Vec<PathBuf>) -> Result<()> { .or_raise(|| message("Could not prepare editor"))? .ok_or_raise(|| unsupported("No editor is configured and the terminal is not capable of running one"))?; let editor_display = editor.command.to_string_lossy().into_owned(); - let mut command: std::process::Command = editor.args(paths).into(); + let mut command: std::process::Command = editor.args(paths).try_into()?; // Program metadata already names a directly launched editor. For shell commands, it only names // the shell, so retain the configured editor command in the message instead of losing that detail. let editor_display = if editor_display == command.get_program().to_string_lossy() { diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs index 8fc6b0ad024..29995961b80 100644 --- a/gitoxide-core/src/repository/exclude.rs +++ b/gitoxide-core/src/repository/exclude.rs @@ -52,7 +52,7 @@ pub fn query( PathsOrPatterns::Patterns(paths) => Box::new(paths.into_iter()), }; for path in paths { - let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref())) + let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))? .metadata() .ok() .map(|m| is_dir_to_mode(m.is_dir())) diff --git a/gitoxide-core/src/repository/index/mod.rs b/gitoxide-core/src/repository/index/mod.rs index f8264f92791..f60267df25b 100644 --- a/gitoxide-core/src/repository/index/mod.rs +++ b/gitoxide-core/src/repository/index/mod.rs @@ -61,7 +61,7 @@ pub fn from_list( .validation(); bail!(err) } - let path = gix::path::into_bstr(path); + let path = gix::path::into_bstr(path)?; index.dangerously_push_entry( gix::index::entry::Stat::default(), gix::hash::ObjectId::empty_blob(object_hash), diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs index 221b8310e50..f786fe7a406 100644 --- a/gitoxide-core/src/repository/status.rs +++ b/gitoxide-core/src/repository/status.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{io, path::Path}; use gix::{ Result, @@ -133,7 +133,7 @@ pub fn show( gix::diff::index::Change::Rewrite { ref source_location, .. } => { - let source_location = gix::path::from_bstr(source_location.as_ref()); + let source_location = gix::path::from_bstr(source_location.as_ref())?; let source_location = gix::path::relativize_with_prefix(&source_location, prefix); writeln!( out, @@ -141,7 +141,7 @@ pub fn show( status = "R", source_rela_path = source_location.display(), dest_rela_path = - gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(), + gix::path::relativize_with_prefix(&gix::path::from_bstr(location)?, prefix).display(), ) .or_error()?; continue; @@ -150,7 +150,7 @@ pub fn show( writeln!( out, "{status: >2} {rela_path}", - rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(location), prefix).display(), + rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(location)?, prefix).display(), ) .or_error()?; } @@ -169,8 +169,8 @@ pub fn show( out, "{status: >3} {rela_path}{slash}", status = "?", - rela_path = - gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display(), + rela_path = gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path)?, prefix) + .display(), slash = if entry.disk_kind.unwrap_or(gix::dir::entry::Kind::File).is_dir() { "/" } else { @@ -189,9 +189,9 @@ pub fn show( "{status: >3} {source_rela_path} → {dest_rela_path}", status = "R", source_rela_path = - gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path()), prefix).display(), + gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path())?, prefix).display(), dest_rela_path = gix::path::relativize_with_prefix( - &gix::path::from_bstr(dirwalk_entry.rela_path.as_bstr()), + &gix::path::from_bstr(dirwalk_entry.rela_path.as_bstr())?, prefix ) .display(), @@ -239,7 +239,7 @@ fn print_index_entry_status( EntryStatus::IntentToAdd => "A", }; - let rela_path = gix::path::from_bstr(rela_path); + let rela_path = gix::path::from_bstr(rela_path).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let display_path = gix::path::relativize_with_prefix(&rela_path, prefix); writeln!(out, "{status: >3} {}", display_path.display()) } diff --git a/gix-actor/tests/actor/identity.rs b/gix-actor/tests/actor/identity.rs index c258bf2815c..3f2178acf40 100644 --- a/gix-actor/tests/actor/identity.rs +++ b/gix-actor/tests/actor/identity.rs @@ -2,7 +2,7 @@ use bstr::ByteSlice; use gix_actor::Identity; #[test] -fn round_trip() -> gix_testtools::Result { +fn round_trip() -> gix_testtools::TestResult { static DEFAULTS: &[&[u8]] = &[ b"Sebastian Thiel <byronimo@gmail.com>", b"Sebastian Thiel < byronimo@gmail.com>", @@ -12,7 +12,7 @@ fn round_trip() -> gix_testtools::Result { b".. whitespace \t is explicitly allowed - unicode aware trimming must be done elsewhere <byronimo@gmail.com>" ]; for input in DEFAULTS { - let signature: Identity = gix_actor::IdentityRef::from_bytes(input).unwrap().into(); + let signature: Identity = gix_actor::IdentityRef::from_bytes(input)?.into(); let mut output = Vec::new(); signature.write_to(&mut output)?; assert_eq!(output.as_bstr(), input.as_bstr()); @@ -21,7 +21,7 @@ fn round_trip() -> gix_testtools::Result { } #[test] -fn lenient_parsing() -> gix_testtools::Result { +fn lenient_parsing() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); for (input, expected_email) in [ ( diff --git a/gix-actor/tests/actor/signature.rs b/gix-actor/tests/actor/signature.rs index f697f40a81f..24254ffb99c 100644 --- a/gix-actor/tests/actor/signature.rs +++ b/gix-actor/tests/actor/signature.rs @@ -62,7 +62,7 @@ fn trim() { } #[test] -fn round_trip() -> Result<(), Box<dyn std::error::Error>> { +fn round_trip() -> gix_testtools::TestResult { static DEFAULTS: &[&[u8]] = &[ b"Sebastian Thiel <byronimo@gmail.com> 1 -0030", b"Sebastian Thiel <byronimo@gmail.com> -1500 -0030", @@ -71,7 +71,7 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> { ]; for input in DEFAULTS { - let signature: Signature = gix_actor::SignatureRef::from_bytes(input).unwrap().into(); + let signature: Signature = gix_actor::SignatureRef::from_bytes(input)?.into(); let mut output = Vec::new(); signature.write_to(&mut output)?; assert_eq!(output.as_bstr(), input.as_bstr()); @@ -80,9 +80,9 @@ fn round_trip() -> Result<(), Box<dyn std::error::Error>> { } #[test] -fn signature_ref_round_trips_with_seconds_in_offset() -> Result<(), Box<dyn std::error::Error>> { +fn signature_ref_round_trips_with_seconds_in_offset() -> gix_testtools::TestResult { let input = b"Sebastian Thiel <byronimo@gmail.com> 1313584730 +051800"; // Seen in the wild - let signature: SignatureRef = gix_actor::SignatureRef::from_bytes(input).unwrap(); + let signature: SignatureRef = gix_actor::SignatureRef::from_bytes(input)?; let mut output = Vec::new(); signature.write_to(&mut output)?; assert_eq!(output.as_bstr(), input.as_bstr()); @@ -90,9 +90,8 @@ fn signature_ref_round_trips_with_seconds_in_offset() -> Result<(), Box<dyn std: } #[test] -fn parse_timestamp_with_trailing_digits() { - let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com> 1312735823 +051800") - .expect("deal with trailing zeroes in timestamp by discarding it"); +fn parse_timestamp_with_trailing_digits() -> gix_testtools::TestResult { + let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com> 1312735823 +051800")?; assert_eq!( signature, SignatureRef { @@ -102,8 +101,7 @@ fn parse_timestamp_with_trailing_digits() { } ); - let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com> 1312735823 +0518") - .expect("this naturally works as the timestamp does not have trailing zeroes"); + let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com> 1312735823 +0518")?; assert_eq!( signature, SignatureRef { @@ -112,12 +110,12 @@ fn parse_timestamp_with_trailing_digits() { time: "1312735823 +0518", } ); + Ok(()) } #[test] -fn parse_missing_timestamp() { - let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com>") - .expect("deal with missing timestamp in signature by zeroing it"); +fn parse_missing_timestamp() -> gix_testtools::TestResult { + let signature = gix_actor::SignatureRef::from_bytes(b"first last <name@example.com>")?; assert_eq!( signature, SignatureRef { @@ -126,4 +124,5 @@ fn parse_missing_timestamp() { time: "" } ); + Ok(()) } diff --git a/gix-archive/src/write.rs b/gix-archive/src/write.rs index 06edb8a111c..dab4e5f8a08 100644 --- a/gix-archive/src/write.rs +++ b/gix-archive/src/write.rs @@ -307,12 +307,12 @@ fn append_tar_entry<W: std::io::Write>( buf.clear(); std::io::copy(&mut entry, buf).or_raise(|| message("Could not read entry data"))?; - let path = gix_path::from_bstr(add_prefix(entry.relative_path(), opts.tree_prefix.as_ref())); + let path = gix_path::from_bstr(add_prefix(entry.relative_path(), opts.tree_prefix.as_ref()))?; header.set_size(buf.len() as u64); if entry.mode.is_link() { use bstr::ByteSlice; - let target = gix_path::from_bstr(buf.as_bstr()); + let target = gix_path::from_bstr(buf.as_bstr())?; header.set_entry_type(tar::EntryType::Symlink); header.set_size(0); ar.append_link(&mut header, path, target) diff --git a/gix-archive/tests/archive.rs b/gix-archive/tests/archive.rs index d2cc21a9a17..0f12d86c439 100644 --- a/gix-archive/tests/archive.rs +++ b/gix-archive/tests/archive.rs @@ -52,14 +52,14 @@ mod from_tree { use gix_attributes::glob::pattern::Case; use gix_object::tree::EntryKind; - use gix_testtools::bstr::ByteSlice; + use gix_testtools::{TestResult, bstr::ByteSlice}; use gix_worktree::stack::state::attributes::Source; use crate::hex_to_id; #[test] - fn basic_usage_internal() -> gix_testtools::Result { - basic_usage(gix_archive::Format::InternalTransientNonPersistable, |buf| { + fn basic_usage_internal() -> TestResult { + Ok(basic_usage(Format::InternalTransientNonPersistable, |buf| { #[cfg(target_pointer_width = "64")] let expected_buffer_length = match gix_testtools::object_hash() { gix_hash::Kind::Sha1 => 551, @@ -137,13 +137,13 @@ mod from_tree { ] ); Ok(()) - }) + })?) } #[test] #[cfg(feature = "tar")] - fn basic_usage_tar() -> gix_testtools::Result { - basic_usage(gix_archive::Format::Tar, |buf| { + fn basic_usage_tar() -> TestResult { + Ok(basic_usage(Format::Tar, |buf| { use tar::EntryType; let mut ar = tar::Archive::new(buf.as_slice()); let mut out = Vec::new(); @@ -187,14 +187,14 @@ mod from_tree { .collect::<Vec<_>>() ); Ok(()) - }) + })?) } #[test] #[cfg(feature = "tar_gz")] - fn basic_usage_tar_gz() -> gix_testtools::Result { - basic_usage( - gix_archive::Format::TarGz { + fn basic_usage_tar_gz() -> TestResult { + Ok(basic_usage( + Format::TarGz { compression_level: Some(9), }, |buf| { @@ -205,14 +205,14 @@ mod from_tree { ); Ok(()) }, - ) + )?) } #[test] #[cfg(feature = "zip")] - fn basic_usage_zip() -> gix_testtools::Result { - basic_usage( - gix_archive::Format::Zip { + fn basic_usage_zip() -> TestResult { + Ok(basic_usage( + Format::Zip { compression_level: Some(9), }, |buf| { @@ -270,7 +270,7 @@ mod from_tree { assert!(found_link, "symlink entry should be found"); Ok(()) }, - ) + )?) } fn basic_usage( diff --git a/gix-attributes/benches/lookup.rs b/gix-attributes/benches/lookup.rs index 7b40e1c787e..ca2226ff0f8 100644 --- a/gix-attributes/benches/lookup.rs +++ b/gix-attributes/benches/lookup.rs @@ -33,13 +33,15 @@ fn single_file(c: &mut Criterion) { for num_entries in [1, 10, 100, 1_000] { let mut search = Search::default(); let mut collection = MetadataCollection::default(); - search.add_patterns_buffer( - &attributes(num_entries, 0), - ".gitattributes".into(), - None, - &mut collection, - true, - ); + search + .add_patterns_buffer( + &attributes(num_entries, 0), + ".gitattributes".into(), + None, + &mut collection, + true, + ) + .expect("benchmark pattern sources are valid UTF-8 paths"); assert_eq!(lookup(&search, &collection, "file.rs"), num_entries); group.throughput(Throughput::Elements(num_entries as u64)); @@ -73,13 +75,15 @@ fn five_file_hierarchy(c: &mut Criterion) { } else { Path::new(directory).join(".gitattributes") }; - search.add_patterns_buffer( - &attributes(num_entries, file_index), - source, - Some(Path::new("")), - &mut collection, - true, - ); + search + .add_patterns_buffer( + &attributes(num_entries, file_index), + source, + Some(Path::new("")), + &mut collection, + true, + ) + .expect("benchmark pattern sources are valid UTF-8 paths"); } let num_entries = ENTRIES.into_iter().sum::<usize>(); diff --git a/gix-attributes/fuzz/fuzz_targets/fuzz_search.rs b/gix-attributes/fuzz/fuzz_targets/fuzz_search.rs index 21827b4a5ae..fe7443b515e 100644 --- a/gix-attributes/fuzz/fuzz_targets/fuzz_search.rs +++ b/gix-attributes/fuzz/fuzz_targets/fuzz_search.rs @@ -1,6 +1,6 @@ #![no_main] -use gix_error::Result; +use gix_error::{Result, ResultExt}; use libfuzzer_sys::fuzz_target; use std::hint::black_box; @@ -34,13 +34,15 @@ fn fuzz(Ctx { pattern, case }: Ctx) -> Result<()> { let mut search = Search::default(); let mut collection = MetadataCollection::default(); - search.add_patterns_buffer( - format!("{pattern} attr").as_bytes(), - Default::default(), - None, - &mut collection, - true, - ); + search + .add_patterns_buffer( + format!("{pattern} attr").as_bytes(), + Default::default(), + None, + &mut collection, + true, + ) + .or_error()?; let mut out = Outcome::default(); out.initialize(&collection); _ = black_box(search.pattern_matching_relative_path("relative/path".into(), case, None, &mut out)); diff --git a/gix-attributes/src/lib.rs b/gix-attributes/src/lib.rs index a4d9112c30b..a03af92aa5b 100644 --- a/gix-attributes/src/lib.rs +++ b/gix-attributes/src/lib.rs @@ -14,7 +14,7 @@ //! None, //! &mut collection, //! true, -//! ); +//! )?; //! //! let mut out = Outcome::default(); //! out.initialize_with_selection(&collection, ["text", "eol"]); @@ -25,6 +25,7 @@ //! .map(|m| m.assignment.to_string()) //! .collect::<Vec<_>>(); //! assert_eq!(assignments, vec!["text", "eol=lf"]); +//! # Ok::<(), std::io::Error>(()) //! ``` //! //! ## Feature Flags diff --git a/gix-attributes/src/search/attributes.rs b/gix-attributes/src/search/attributes.rs index b3155d4d722..dfa67c9a970 100644 --- a/gix-attributes/src/search/attributes.rs +++ b/gix-attributes/src/search/attributes.rs @@ -32,7 +32,7 @@ impl Search { None, collection, true, /* allow macros */ - ); + )?; for path in files.into_iter() { group.add_patterns_file(path.into(), true, None, buf, collection, true /* allow macros */)?; @@ -81,15 +81,16 @@ impl Search { root: Option<&Path>, collection: &mut MetadataCollection, allow_macros: bool, - ) { + ) -> std::io::Result<()> { self.patterns - .push(pattern::List::from_bytes(bytes, source, root, Attributes)); + .push(pattern::List::from_bytes(bytes, source, root, Attributes)?); let last = self.patterns.last_mut().expect("just added"); if !allow_macros { last.patterns .retain(|p| !matches!(p.value, Value::MacroAssignments { .. })); } collection.update_from_list(last); + Ok(()) } /// Pop the last attribute patterns list from our queue. diff --git a/gix-attributes/tests/attributes/search.rs b/gix-attributes/tests/attributes/search.rs index b74d29bd2d8..023bd151a2d 100644 --- a/gix-attributes/tests/attributes/search.rs +++ b/gix-attributes/tests/attributes/search.rs @@ -42,13 +42,15 @@ mod specials { fn search_case(pattern: &str, path: &str, rela_containing_dir: Option<&str>, case: Case) -> bool { let mut search = Search::default(); let mut collection = MetadataCollection::default(); - search.add_patterns_buffer( - format!("{pattern} test").as_bytes(), - rela_containing_dir.map_or_else(|| Path::new("<memory>").into(), |d| Path::new(d).join("filename")), - rela_containing_dir.map(|_| Path::new("")), - &mut collection, - true, - ); + search + .add_patterns_buffer( + format!("{pattern} test").as_bytes(), + rela_containing_dir.map_or_else(|| Path::new("<memory>").into(), |d| Path::new(d).join("filename")), + rela_containing_dir.map(|_| Path::new("")), + &mut collection, + true, + ) + .expect("UTF-8 pattern source has a valid base"); let mut out = Outcome::default(); out.initialize(&collection); search.pattern_matching_relative_path(path.into(), case, None, &mut out) @@ -63,7 +65,7 @@ mod specials { } #[test] -fn baseline() -> gix_error::TestResult { +fn baseline() -> gix_testtools::TestResult { let mut buf = Vec::new(); // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this. @@ -131,7 +133,7 @@ fn assert_references(out: &Outcome) { } #[test] -fn all_attributes_are_listed_in_declaration_order() -> gix_error::TestResult { +fn all_attributes_are_listed_in_declaration_order() -> gix_testtools::TestResult { let (mut group, mut collection, base, input) = baseline::user_attributes("lookup-order")?; let mut buf = Vec::new(); @@ -228,7 +230,7 @@ fn all_attributes_are_listed_in_declaration_order() -> gix_error::TestResult { } #[test] -fn given_attributes_are_made_available_in_given_order() -> gix_error::TestResult { +fn given_attributes_are_made_available_in_given_order() -> gix_testtools::TestResult { let (mut group, mut collection, base, input) = baseline::user_attributes_named_baseline("lookup-order", "baseline.selected")?; @@ -270,13 +272,13 @@ fn given_attributes_are_made_available_in_given_order() -> gix_error::TestResult } #[test] -fn macro_attributes_expand_only_when_macro_is_set() -> gix_error::TestResult { +fn macro_attributes_expand_only_when_macro_is_set() -> gix_testtools::TestResult { assert_baseline("macro-expansion")?; Ok(()) } #[test] -fn attribute_tokenisation_matches_git() -> gix_error::TestResult { +fn attribute_tokenisation_matches_git() -> gix_testtools::TestResult { assert_baseline("tokenisation")?; Ok(()) } diff --git a/gix-attributes/tests/attributes/source.rs b/gix-attributes/tests/attributes/source.rs index ccc28ce8124..26459fa65d3 100644 --- a/gix-attributes/tests/attributes/source.rs +++ b/gix-attributes/tests/attributes/source.rs @@ -1,23 +1,23 @@ use gix_attributes::Source; #[test] -fn system_attributes_with_mingw64() -> gix_testtools::Result { - system_attributes("mingw64", false) +fn system_attributes_with_mingw64() -> gix_testtools::TestResult { + system_attributes("mingw64", false).map_err(Into::into) } #[test] -fn system_attributes_with_ucrt64() -> gix_testtools::Result { - system_attributes("ucrt64", false) +fn system_attributes_with_ucrt64() -> gix_testtools::TestResult { + system_attributes("ucrt64", false).map_err(Into::into) } #[test] -fn system_attributes_with_mixed_prefixes_and_mingw64_active() -> gix_testtools::Result { - system_attributes("mingw64", true) +fn system_attributes_with_mixed_prefixes_and_mingw64_active() -> gix_testtools::TestResult { + system_attributes("mingw64", true).map_err(Into::into) } #[test] -fn system_attributes_with_mixed_prefixes_and_ucrt64_active() -> gix_testtools::Result { - system_attributes("ucrt64", true) +fn system_attributes_with_mixed_prefixes_and_ucrt64_active() -> gix_testtools::TestResult { + system_attributes("ucrt64", true).map_err(Into::into) } fn system_attributes(runtime: &str, mixed: bool) -> gix_testtools::Result { diff --git a/gix-bitmap/tests/bitmap.rs b/gix-bitmap/tests/bitmap.rs index 1f83d77d718..e4c1cefaa3d 100644 --- a/gix-bitmap/tests/bitmap.rs +++ b/gix-bitmap/tests/bitmap.rs @@ -1,18 +1,18 @@ mod fuzzed { #[test] - fn ewah_artifacts_run_fuzzer() { + fn ewah_artifacts_run_fuzzer() -> gix_testtools::TestResult { for path in artifact_paths("ewah") { - let data = std::fs::read(path).expect("artifact is readable"); + let data = std::fs::read(path)?; let _ = gix_bitmap::ewah::decode(&data); } + Ok(()) } #[test] - fn runaway_run_length_is_rejected() { + fn runaway_run_length_is_rejected() -> gix_testtools::TestResult { let (bitmap, rest) = gix_bitmap::ewah::decode(include_bytes!( "../fuzz/artifacts/ewah/slow-unit-ac817962d1a6c123d4d1f73860f5b779423ed171" - )) - .expect("fixture must decode"); + ))?; assert!(rest.is_empty(), "fixture should be fully consumed"); assert_eq!( @@ -20,28 +20,23 @@ mod fuzzed { None, "impossible run lengths must be rejected instead of iterating unboundedly" ); + Ok(()) } #[test] - fn non_zero_padding_bits_in_last_literal_word_are_rejected() { + fn non_zero_padding_bits_in_last_literal_word_are_rejected() -> gix_testtools::TestResult { let bitmap = gix_bitmap::ewah::Vec::from_bits(&[false]).expect("small test fixtures must fit into u32"); let mut data = Vec::new(); - bitmap - .write_to(&mut data) - .expect("writing a valid test fixture to bytes must succeed"); + bitmap.write_to(&mut data)?; let header_size = 4 + 4; // Skip the fixed-size header and the RLW word, then flip bit 33 in the first literal word // so the bitmap stays logically empty while the serialized padding becomes invalid. let literal_word_offset = header_size + 8; - let mut literal_word = u64::from_be_bytes( - data[literal_word_offset..literal_word_offset + 8] - .try_into() - .expect("literal word"), - ); + let mut literal_word = u64::from_be_bytes(data[literal_word_offset..literal_word_offset + 8].try_into()?); literal_word |= 1u64 << 33; data[literal_word_offset..literal_word_offset + 8].copy_from_slice(&literal_word.to_be_bytes()); - let (bitmap, rest) = gix_bitmap::ewah::decode(&data).expect("fixture must decode"); + let (bitmap, rest) = gix_bitmap::ewah::decode(&data)?; assert!(rest.is_empty(), "fixture should be fully consumed"); assert_eq!( @@ -49,10 +44,11 @@ mod fuzzed { None, "set bits outside the declared bit length must be rejected" ); + Ok(()) } #[test] - fn literal_only_bitmaps_preserve_all_set_bits() { + fn literal_only_bitmaps_preserve_all_set_bits() -> gix_testtools::TestResult { for bits in [ vec![], vec![false], @@ -70,10 +66,8 @@ mod fuzzed { .filter_map(|(idx, bit)| bit.then_some(idx)) .collect(); - bitmap - .write_to(&mut encoded) - .expect("writing a valid test fixture to bytes must succeed"); - let (bitmap, rest) = gix_bitmap::ewah::decode(&encoded).expect("serialized test fixture must decode"); + bitmap.write_to(&mut encoded)?; + let (bitmap, rest) = gix_bitmap::ewah::decode(&encoded)?; let mut actual = Vec::new(); assert!(rest.is_empty(), "serialized test fixture should be fully consumed"); @@ -95,6 +89,7 @@ mod fuzzed { "iteration should report exactly the set bits from the source bitmap" ); } + Ok(()) } #[test] diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 2db0ca9bf39..78e1e2642d8 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -249,7 +249,7 @@ impl Fixture { macro_rules! mktest { ($name:ident, $case:expr, $number_of_lines:literal) => { #[test] - fn $name() -> gix_testtools::Result { + fn $name() -> gix_testtools::TestResult { let Fixture { odb, mut resource_cache, @@ -374,7 +374,7 @@ fn diff_algorithm_parity() { } #[test] -fn file_that_was_added_in_two_branches() -> gix_testtools::Result { +fn file_that_was_added_in_two_branches() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_two_roots_repo.sh")?; let Fixture { @@ -405,7 +405,7 @@ fn file_that_was_added_in_two_branches() -> gix_testtools::Result { } #[test] -fn since() -> gix_testtools::Result { +fn since() -> gix_testtools::TestResult { let Fixture { odb, mut resource_cache, @@ -423,9 +423,7 @@ fn since() -> gix_testtools::Result { gix_blame::Options { diff_algorithm: gix_diff::blob::Algorithm::Histogram, ranges: BlameRanges::default(), - since: Some( - gix_date::parse("2025-01-31", None).expect("TODO: should be able to to retrieve inner from Exn"), - ), + since: Some(gix_date::parse("2025-01-31", None)?), rewrites: Some(gix_diff::Rewrites::default()), debug_track_path: false, }, @@ -447,7 +445,7 @@ mod blame_ranges { use gix_blame::BlameRanges; #[test] - fn line_range() -> gix_testtools::Result { + fn line_range() -> gix_testtools::TestResult { let Fixture { odb, mut resource_cache, @@ -483,7 +481,7 @@ mod blame_ranges { } #[test] - fn multiple_ranges_using_add_range() -> gix_testtools::Result { + fn multiple_ranges_using_add_range() -> gix_testtools::TestResult { let Fixture { odb, mut resource_cache, @@ -529,7 +527,7 @@ mod blame_ranges { } #[test] - fn multiple_ranges_using_from_ranges() -> gix_testtools::Result { + fn multiple_ranges_using_from_ranges() -> gix_testtools::TestResult { let Fixture { odb, mut resource_cache, @@ -576,7 +574,7 @@ mod rename_tracking { use crate::{Baseline, Fixture}; #[test] - fn source_file_name_is_tracked_per_hunk() -> gix_testtools::Result { + fn source_file_name_is_tracked_per_hunk() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_rename_tracking_repo.sh")?; let Fixture { @@ -613,7 +611,7 @@ mod rename_tracking { } #[test] - fn rename_and_change_in_merge_commit() -> gix_testtools::Result { + fn rename_and_change_in_merge_commit() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_rename_tracking_repo.sh")?; let mut fixture = Fixture::for_worktree_path(worktree_path.to_path_buf())?; @@ -649,12 +647,12 @@ mod untracked_changes { use crate::{Baseline, Fixture}; #[test] - fn untracked_lines() -> gix_testtools::Result { + fn untracked_lines() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_repo.sh")?; let mut fixture = Fixture::for_worktree_path(worktree_path.to_path_buf())?; let source_file_name = "untracked-lines.txt"; - let contents = std::fs::read(worktree_path.join(source_file_name)).expect("file to be present and readable"); + let contents = std::fs::read(worktree_path.join(source_file_name))?; let lines_blamed = fixture .blame_untracked_changes( @@ -681,12 +679,12 @@ mod untracked_changes { } #[test] - fn untracked_file() -> gix_testtools::Result { + fn untracked_file() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_repo.sh")?; let mut fixture = Fixture::for_worktree_path(worktree_path.to_path_buf())?; let source_file_name = "untracked-file.txt"; - let contents = std::fs::read(worktree_path.join(source_file_name)).expect("file to be present and readable"); + let contents = std::fs::read(worktree_path.join(source_file_name))?; let lines_blamed = fixture .blame_untracked_changes( @@ -713,12 +711,12 @@ mod untracked_changes { } #[test] - fn untracked_lines_with_ranges() -> gix_testtools::Result { + fn untracked_lines_with_ranges() -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_repo.sh")?; let mut fixture = Fixture::for_worktree_path(worktree_path.to_path_buf())?; let source_file_name = "untracked-lines.txt"; - let contents = std::fs::read(worktree_path.join(source_file_name)).expect("file to be present and readable"); + let contents = std::fs::read(worktree_path.join(source_file_name))?; let lines_blamed = fixture .blame_untracked_changes( @@ -750,7 +748,7 @@ mod symlinks { use crate::{Baseline, Fixture}; - fn run_test(source_file_name: &str, baseline_name: &str) -> gix_testtools::Result { + fn run_test(source_file_name: &str, baseline_name: &str) -> gix_testtools::TestResult { let worktree_path = gix_testtools::scripted_fixture_read_only("make_blame_symlinks_repo.sh")?; let mut fixture = Fixture::for_worktree_path(worktree_path.to_path_buf())?; @@ -778,27 +776,27 @@ mod symlinks { } #[test] - fn lines_added_to_file_targeted_by_symlink() -> gix_testtools::Result { + fn lines_added_to_file_targeted_by_symlink() -> gix_testtools::TestResult { run_test("symlink", "symlink.baseline") } #[test] - fn symlink_changing_target() -> gix_testtools::Result { + fn symlink_changing_target() -> gix_testtools::TestResult { run_test("symlink-changing-target", "symlink-changing-target.baseline") } #[test] - fn symlink_renamed() -> gix_testtools::Result { + fn symlink_renamed() -> gix_testtools::TestResult { run_test("symlink-after-rename", "symlink-renamed.baseline") } #[test] - fn file_becomes_symlink() -> gix_testtools::Result { + fn file_becomes_symlink() -> gix_testtools::TestResult { run_test("file-then-symlink", "file-becomes-symlink.baseline") } #[test] - fn symlink_becomes_file() -> gix_testtools::Result { + fn symlink_becomes_file() -> gix_testtools::TestResult { run_test("symlink-then-file", "symlink-becomes-file.baseline") } } diff --git a/gix-command/fuzz/fuzz_targets/prepare.rs b/gix-command/fuzz/fuzz_targets/prepare.rs index ed5066ebaf0..d038a0652f0 100644 --- a/gix-command/fuzz/fuzz_targets/prepare.rs +++ b/gix-command/fuzz/fuzz_targets/prepare.rs @@ -12,7 +12,7 @@ fn inspect_prepare(command: &str) { gix_command::prepare(command).with_shell(), gix_command::prepare(command).with_shell().with_quoted_command(), ] { - let command = std::process::Command::from(prep); + let command = std::process::Command::try_from(prep); _ = black_box(format!("{command:?}")); } } diff --git a/gix-command/src/lib.rs b/gix-command/src/lib.rs index bf3c547aa97..7c0e3f6c2ce 100644 --- a/gix-command/src/lib.rs +++ b/gix-command/src/lib.rs @@ -46,7 +46,7 @@ pub mod shebang { .map_or(line.len(), |space_idx| slash_idx + space_idx); let (interpreter, args) = line.split_at(space_idx); Some(Data { - interpreter: gix_path::try_from_byte_slice(interpreter.trim()).ok()?.to_owned(), + interpreter: gix_path::from_byte_slice(interpreter.trim()).ok()?.to_owned(), args: crate::parse::arguments(args.trim().as_bstr()).unwrap_or_default(), }) } @@ -63,6 +63,10 @@ pub mod shebang { /// A structure to keep settings to use when invoking a command via [`spawn()`][Prepare::spawn()], /// after creating it with [`prepare()`]. +/// +/// Convert it to [`std::process::Command`] with [`TryFrom::try_from()`] to finish preparation +/// without spawning. Preparation reports an error when shell quoting or namespace conversion +/// requires an encoding the platform cannot represent. pub struct Prepare { /// The command to invoke, either directly or with a shell depending on `use_shell`. pub command: OsString, diff --git a/gix-command/src/parse.rs b/gix-command/src/parse.rs index 40fe4fad595..40aa0345171 100644 --- a/gix-command/src/parse.rs +++ b/gix-command/src/parse.rs @@ -198,7 +198,7 @@ fn push_unquoted( } fn into_os_string(value: BString) -> ExnResult<OsString, Error> { - gix_path::try_from_bstring(value) + gix_path::from_bstring(value) .map(std::path::PathBuf::into_os_string) .or_raise_typed(|| Error::UnrepresentableOsString) } diff --git a/gix-command/src/prepare.rs b/gix-command/src/prepare.rs index 34719cde4e2..266fbea11c0 100644 --- a/gix-command/src/prepare.rs +++ b/gix-command/src/prepare.rs @@ -6,6 +6,7 @@ use std::{ }; use bstr::ByteSlice; +use gix_error::Result; use crate::{Context, Prepare, extract_interpreter, is_bare_command, split_paths, win_path_lookup}; @@ -15,8 +16,8 @@ impl Prepare { /// scripts, and if found will use `sh` to execute it or whatever is set as /// [`with_shell_program()`](Self::with_shell_program()). /// - /// Commands are inspected as bytes, including non-UTF-8 commands on Unix. If the platform - /// cannot represent a command as bytes, it is invoked directly. + /// Commands are inspected using their native encoded bytes, including commands that cannot + /// be represented as UTF-8. /// /// If a shell is used, then arguments given here with [arg()](Self::arg) or /// [args()](Self::args) will be substituted via `"$@"` if it's not already present in the @@ -30,8 +31,11 @@ impl Prepare { /// If neither this method nor [`with_shell()`](Self::with_shell()) is called, commands are /// always executed verbatim and directly, without the use of a shell. pub fn command_may_be_shell_script(mut self) -> Self { - self.use_shell = gix_path::os_str_into_bstr(&self.command) - .is_ok_and(|cmd| cmd.find_byteset(b"|&;<>()$`\\\"' \t\n*?[#~=%").is_some()); + self.use_shell = self + .command + .as_encoded_bytes() + .find_byteset(b"|&;<>()$`\\\"' \t\n*?[#~=%") + .is_some(); self } @@ -168,15 +172,20 @@ impl Prepare { /// Finalization impl Prepare { /// Spawn the command as configured. + /// + /// Encoding errors during preparation are returned with [`std::io::ErrorKind::InvalidInput`]. pub fn spawn(self) -> std::io::Result<std::process::Child> { - let mut cmd = Command::from(self); + let mut cmd = + Command::try_from(self).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; gix_trace::debug!(cmd = ?cmd); cmd.spawn() } } -impl From<Prepare> for Command { - fn from(mut prep: Prepare) -> Command { +impl TryFrom<Prepare> for Command { + type Error = gix_error::Error; + + fn try_from(mut prep: Prepare) -> Result<Self> { let mut inline_env = Vec::new(); let mut cmd = if prep.use_shell { let split_args = prep @@ -218,11 +227,10 @@ impl From<Prepare> for Command { .to_os_string(); cmd.arg("-c"); if !prep.args.is_empty() { - if !gix_path::os_str_into_bstr(&prep.command).is_ok_and(|cmd| cmd.contains_str("$@")) { - if prep.quote_command - && let Ok(command) = gix_path::os_str_into_bstr(&prep.command) - { - prep.command = gix_path::from_bstring(gix_quote::single(command)).into(); + if !prep.command.as_encoded_bytes().contains_str("$@") { + if prep.quote_command { + let command = gix_path::os_str_into_bstr(&prep.command)?; + prep.command = gix_path::from_bstring(gix_quote::single(command))?.into(); } prep.command.push(r#" "$@""#); } else { @@ -265,7 +273,7 @@ impl From<Prepare> for Command { cmd.env("GIT_NO_REPLACE_OBJECTS", usize::from(value).to_string()); } if let Some(namespace) = ctx.ref_namespace { - cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace)); + cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace)?); } if let Some(value) = ctx.literal_pathspecs { cmd.env("GIT_LITERAL_PATHSPECS", usize::from(value).to_string()); @@ -288,7 +296,7 @@ impl From<Prepare> for Command { } } cmd.envs(inline_env); - cmd + Ok(cmd) } } @@ -360,7 +368,7 @@ mod tests { use super::*; #[test] - fn inherited_path_is_used_for_both_probing_and_execution() -> gix_testtools::Result { + fn inherited_path_is_used_for_both_probing_and_execution() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -413,7 +421,7 @@ mod tests { } #[test] - fn explicit_path_lookup_failure_stays_within_that_path() -> gix_testtools::Result { + fn explicit_path_lookup_failure_stays_within_that_path() -> gix_testtools::TestResult { let joined_paths = std::env::join_paths(["", "not/a/real/path", "also/not/real"])?; let cmd = windows_command("missing.exe".into(), &[], &[("PATH".into(), joined_paths)]); assert_eq!( diff --git a/gix-command/src/tests.rs b/gix-command/src/tests.rs index 8c5f5ad2710..9dd01f58bd6 100644 --- a/gix-command/src/tests.rs +++ b/gix-command/src/tests.rs @@ -1,7 +1,7 @@ use super::*; #[test] -fn internal_win_path_lookup() -> gix_testtools::Result { +fn internal_win_path_lookup() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; let mut paths: Vec<_> = std::fs::read_dir(&root)? .filter_map(Result::ok) diff --git a/gix-command/tests/command/command_line.rs b/gix-command/tests/command/command_line.rs index 0be91766c89..9be96013927 100644 --- a/gix-command/tests/command/command_line.rs +++ b/gix-command/tests/command/command_line.rs @@ -4,7 +4,7 @@ use gix_command::parse::{self, Outcome}; use gix_error::{Result, ResultExt}; #[test] -fn words_are_split_without_expansion() -> gix_testtools::Result { +fn words_are_split_without_expansion() -> gix_testtools::TestResult { assert_eq!( command_line( r#"cmd 'single quoted' "double \"quoted\"" escaped\ word "kept\q" "" # ignored @@ -32,7 +32,7 @@ next"#, } #[test] -fn assignments_are_returned_separately() -> gix_testtools::Result { +fn assignments_are_returned_separately() -> gix_testtools::TestResult { assert_eq!( command_line(r#" FIRST=one SECOND="two words" _THIRD='' command arg"#)?, Outcome { @@ -49,7 +49,7 @@ fn assignments_are_returned_separately() -> gix_testtools::Result { } #[test] -fn invalid_assignment_names_are_arguments() -> gix_testtools::Result { +fn invalid_assignment_names_are_arguments() -> gix_testtools::TestResult { for (input, expected) in [ ("tool-name=value arg", &["tool-name=value", "arg"]), (r#"'FOO'=bar command"#, &["FOO=bar", "command"]), @@ -168,7 +168,7 @@ fn parse_errors_retain_their_classification() { #[test] #[cfg(unix)] -fn non_utf8_input_is_preserved() -> gix_testtools::Result { +fn non_utf8_input_is_preserved() -> gix_testtools::TestResult { use bstr::ByteSlice; use std::os::unix::ffi::OsStringExt; diff --git a/gix-command/tests/command/context.rs b/gix-command/tests/command/context.rs index 73a12d7836d..c45e5815c6f 100644 --- a/gix-command/tests/command/context.rs +++ b/gix-command/tests/command/context.rs @@ -6,69 +6,74 @@ fn winfix(expected: impl Into<String>) -> String { } #[test] -fn git_dir_sets_git_dir_env_and_cwd() { +fn git_dir_sets_git_dir_env_and_cwd() -> gix_testtools::TestResult { let ctx = Context { git_dir: Some(".".into()), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_DIR="." """#)); + Ok(()) } #[test] -fn worktree_dir_sets_env_only() { +fn worktree_dir_sets_env_only() -> gix_testtools::TestResult { let ctx = Context { worktree_dir: Some(".".into()), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_WORK_TREE="." """#)); + Ok(()) } #[test] -fn no_replace_objects_sets_env_only() { +fn no_replace_objects_sets_env_only() -> gix_testtools::TestResult { for value in [false, true] { let expected = usize::from(value); let ctx = Context { no_replace_objects: Some(value), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_NO_REPLACE_OBJECTS="{expected}" """#)) ); } + Ok(()) } #[test] -fn ref_namespace_sets_env_only() { +fn ref_namespace_sets_env_only() -> gix_testtools::TestResult { let ctx = Context { ref_namespace: Some("namespace".into()), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_NAMESPACE="namespace" """#)); + Ok(()) } #[test] -fn literal_pathspecs_sets_env_only() { +fn literal_pathspecs_sets_env_only() -> gix_testtools::TestResult { for value in [false, true] { let expected = usize::from(value); let ctx = Context { literal_pathspecs: Some(value), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_LITERAL_PATHSPECS="{expected}" """#)) ); } + Ok(()) } #[test] -fn glob_pathspecs_sets_env_only() { +fn glob_pathspecs_sets_env_only() -> gix_testtools::TestResult { for (value, expected) in [ (false, r#"GIT_NOGLOB_PATHSPECS="1""#), (true, r#"GIT_GLOB_PATHSPECS="1""#), @@ -77,23 +82,25 @@ fn glob_pathspecs_sets_env_only() { glob_pathspecs: Some(value), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!(format!("{cmd:?}"), winfix(format!(r#"{expected} """#))); } + Ok(()) } #[test] -fn icase_pathspecs_sets_env_only() { +fn icase_pathspecs_sets_env_only() -> gix_testtools::TestResult { for value in [false, true] { let expected = usize::from(value); let ctx = Context { icase_pathspecs: Some(value), ..Default::default() }; - let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + let cmd = std::process::Command::try_from(gix_command::prepare("").with_context(ctx))?; assert_eq!( format!("{cmd:?}"), winfix(format!(r#"GIT_ICASE_PATHSPECS="{expected}" """#)) ); } + Ok(()) } diff --git a/gix-command/tests/command/main.rs b/gix-command/tests/command/main.rs index fef0a322c4c..497a256693f 100644 --- a/gix-command/tests/command/main.rs +++ b/gix-command/tests/command/main.rs @@ -1,9 +1,9 @@ use std::path::Path; -use gix_testtools::Result; +use gix_testtools::TestResult; #[test] -fn extract_interpreter() -> gix_testtools::Result { +fn extract_interpreter() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; assert_eq!( gix_command::extract_interpreter(&root.join("b").join("exe")), diff --git a/gix-command/tests/command/prepare.rs b/gix-command/tests/command/prepare.rs index ec716770d82..2a27b776576 100644 --- a/gix-command/tests/command/prepare.rs +++ b/gix-command/tests/command/prepare.rs @@ -1,6 +1,85 @@ -use crate::Result; +use gix_testtools::TestResult; use std::sync::LazyLock; +#[test] +#[cfg(any(unix, windows))] +fn native_program_paths_are_preserved_without_utf8_conversion() -> TestResult { + #[cfg(unix)] + let program = { + use std::os::unix::ffi::OsStringExt; + std::ffi::OsString::from_vec(b"./native-\xff".to_vec()) + }; + #[cfg(windows)] + let program = { + use std::os::windows::ffi::OsStringExt; + std::ffi::OsString::from_wide(&[b'.' as u16, b'/' as u16, 0xd800]) + }; + // An explicit PATH avoids reading the process environment during Windows preparation. + let command = std::process::Command::try_from(gix_command::prepare(&program).env("PATH", ""))?; + assert_eq!( + command.get_program(), + program, + "direct commands preserve native path encoding" + ); + Ok(()) +} + +#[test] +#[cfg(windows)] +fn native_shell_inspection_preserves_bytes_and_quoting_reports_encoding_errors() -> TestResult { + use std::os::windows::ffi::OsStringExt; + + let native = std::ffi::OsString::from_wide(&[0xd800]); + let mut script = native.clone(); + script.push(" $@"); + let prepare = gix_command::prepare(&script).command_may_be_shell_script(); + assert!( + prepare.use_shell, + "ASCII shell syntax is recognized beside an unpaired surrogate" + ); + let command = std::process::Command::try_from(prepare.with_shell_program("unused-shell").arg("arg"))?; + assert_eq!( + command.get_args().nth(1), + Some(script.as_os_str()), + "an existing $@ is detected without changing the native script" + ); + let err = std::process::Command::try_from( + gix_command::prepare(native) + .with_shell() + .with_shell_program("unused-shell") + .with_quoted_command() + .arg("arg"), + ) + .expect_err("shell quoting needs representable bytes"); + assert!(err.is_validation(), "unrepresentable shell input is a validation error"); + Ok(()) +} + +#[test] +#[cfg(windows)] +fn unrepresentable_namespace_is_reported_before_spawning() -> TestResult { + let prepare = || { + gix_command::prepare("./unused-command") + .env("PATH", "") + .with_context(gix_command::Context { + ref_namespace: Some(vec![0xff].into()), + ..Default::default() + }) + }; + let err = std::process::Command::try_from(prepare()).expect_err("a namespace must fit the process environment"); + assert!( + err.is_validation(), + "invalid namespace bytes retain their validation classification" + ); + let err = prepare().spawn().expect_err("invalid input prevents process creation"); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert!( + err.get_ref().is_some(), + "the encoding failure remains the I/O error's source" + ); + Ok(()) +} + fn default_shell() -> &'static str { static SH: LazyLock<std::ffi::OsString> = LazyLock::new(|| gix_path::env::shell_command().get_program().to_owned()); SH.to_str() @@ -17,7 +96,8 @@ fn quoted(input: &[&str]) -> String { // These assertions cover argument parsing. Windows resolves the program before spawning, so // compare against the same program prepared without any argument splitting. let (program, args) = input.split_first().expect("a command always includes its program"); - let cmd = std::process::Command::from(gix_command::prepare(program)); + let cmd = std::process::Command::try_from(gix_command::prepare(program)) + .expect("command fixture can be represented by the platform"); std::iter::once(format!("{cmd:?}")) .chain(args.iter().map(|s| format!("\"{s}\""))) .collect::<Vec<_>>() @@ -42,46 +122,51 @@ fn quoted_default_shell(input: &[&str]) -> String { } #[test] -fn empty() { - let cmd = std::process::Command::from(gix_command::prepare("")); +fn empty() -> TestResult { + let cmd = std::process::Command::try_from(gix_command::prepare(""))?; assert_eq!(format!("{cmd:?}"), "\"\""); + Ok(()) } #[test] -fn whitespace_only_without_shell() { - let cmd = std::process::Command::from(gix_command::prepare(" ")); +fn whitespace_only_without_shell() -> TestResult { + let cmd = std::process::Command::try_from(gix_command::prepare(" "))?; assert_eq!(format!("{cmd:?}"), quoted(&[" "])); + Ok(()) } #[test] -fn whitespace_only_commands_with_auto_split_fall_back_to_shell() { - let cmd = std::process::Command::from( +fn whitespace_only_commands_with_auto_split_fall_back_to_shell() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(" ").command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!(format!("{cmd:?}"), quoted_default_shell(&["-c", " ", SH_BASENAME])); + Ok(()) } #[test] -fn single_and_multiple_arguments() { - let cmd = std::process::Command::from(gix_command::prepare("ls").arg("first").args(["second", "third"])); +fn single_and_multiple_arguments() -> TestResult { + let cmd = std::process::Command::try_from(gix_command::prepare("ls").arg("first").args(["second", "third"]))?; assert_eq!(format!("{cmd:?}"), quoted(&["ls", "first", "second", "third"])); + Ok(()) } #[test] -fn multiple_arguments_in_one_line_with_auto_split() { - let cmd = std::process::Command::from( +fn multiple_arguments_in_one_line_with_auto_split() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare("echo first second third").command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted(&["echo", "first", "second", "third"]), "we split by hand which works unless one tries to rely on shell-builtins (which we can't detect)" ); + Ok(()) } #[test] -fn shell_assignments_are_applied_during_manual_splitting() { - let cmd = std::process::Command::from( +fn shell_assignments_are_applied_during_manual_splitting() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#" FOO=bar BAR="two words" GIT_DIR=inline command.exe arg"#) .env("FOO", "overridden") .with_context(gix_command::Context { @@ -89,10 +174,10 @@ fn shell_assignments_are_applied_during_manual_splitting() { ..Default::default() }) .command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( cmd.get_program(), - std::process::Command::from(gix_command::prepare("command.exe")).get_program(), + std::process::Command::try_from(gix_command::prepare("command.exe"))?.get_program(), "non-PATH assignments don't prevent manual splitting" ); assert_eq!(cmd.get_args().collect::<Vec<_>>(), ["arg"], "arguments are retained"); @@ -116,20 +201,21 @@ fn shell_assignments_are_applied_during_manual_splitting() { Some(std::ffi::OsStr::new("inline")), "inline assignments have shell precedence over context values" ); + Ok(()) } #[test] #[cfg(windows)] -fn inline_path_assignment_controls_lookup() -> gix_testtools::Result { +fn inline_path_assignment_controls_lookup() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; let joined_paths = root.join("a").to_string_lossy().replace('\\', "/"); let program = format!("{joined_paths}/x.exe"); let input = format!(r#"PATH="{joined_paths}" x.exe arg"#); - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare(input) .env("PATH", "builder path must be overridden") .command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!(cmd.get_program(), std::path::Path::new(&program)); assert_eq!( cmd.get_args().collect::<Vec<_>>(), @@ -148,11 +234,11 @@ fn inline_path_assignment_controls_lookup() -> gix_testtools::Result { #[test] #[cfg(windows)] -fn builder_path_controls_lookup() -> gix_testtools::Result { +fn builder_path_controls_lookup() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; let joined_paths = root.join("b").to_string_lossy().replace('\\', "/"); let script = std::path::PathBuf::from(&joined_paths).join("exe"); - let cmd = std::process::Command::from(gix_command::prepare("exe").env("Path", joined_paths.as_str())); + let cmd = std::process::Command::try_from(gix_command::prepare("exe").env("Path", joined_paths.as_str()))?; assert_eq!(cmd.get_program(), std::path::Path::new("/b/exe")); assert_eq!( cmd.get_args().collect::<Vec<_>>(), @@ -164,13 +250,13 @@ fn builder_path_controls_lookup() -> gix_testtools::Result { #[test] #[cfg(windows)] -fn manually_split_commands_retain_shebang_dispatch() -> gix_testtools::Result { +fn manually_split_commands_retain_shebang_dispatch() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("win_path_lookup.sh")?; let script = root.join("b").join("exe").to_string_lossy().replace('\\', "/"); let input = format!(r#"FOO=bar "{script}" arg"#); - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare(input).command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!(cmd.get_program(), std::path::Path::new("/b/exe")); assert_eq!( cmd.get_args().collect::<Vec<_>>(), @@ -188,38 +274,40 @@ fn manually_split_commands_retain_shebang_dispatch() -> gix_testtools::Result { } #[test] -fn only_unambiguous_shell_assignments_are_applied() { +fn only_unambiguous_shell_assignments_are_applied() -> TestResult { for (input, program, args) in [ ("tool-name=value arg", "tool-name=value", &["arg"][..]), (r#"'FOO'=bar command"#, "FOO=bar", &["command"][..]), ] { - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare(input).command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( cmd.get_program(), - std::process::Command::from(gix_command::prepare(program)).get_program(), + std::process::Command::try_from(gix_command::prepare(program))?.get_program(), "{input:?} is not an assignment prefix" ); assert_eq!(cmd.get_args().collect::<Vec<_>>(), args, "arguments are retained"); assert_eq!(cmd.get_envs().count(), 0, "the environment is unchanged"); } + Ok(()) } #[test] -fn assignment_only_input_is_left_to_the_shell() { - let cmd = std::process::Command::from( +fn assignment_only_input_is_left_to_the_shell() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare("tool=name").command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", "tool=name", SH_BASENAME]) ); + Ok(()) } #[test] #[cfg(unix)] -fn invalid_utf8_commands_are_checked_for_shell_syntax() { +fn invalid_utf8_commands_are_checked_for_shell_syntax() -> TestResult { use std::os::unix::ffi::{OsStrExt, OsStringExt}; assert!( @@ -235,20 +323,21 @@ fn invalid_utf8_commands_are_checked_for_shell_syntax() { "shell syntax is detected without requiring UTF-8" ); - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare(std::ffi::OsString::from_vec(vec![0xff, b' ', 0xfe])) .command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!(cmd.get_program().as_bytes(), [0xff]); assert_eq!( cmd.get_args().map(OsStrExt::as_bytes).collect::<Vec<_>>(), [&[0xfe][..]], "manual splitting preserves invalid UTF-8" ); + Ok(()) } #[test] -fn relative_existing_paths_with_shell_syntax_still_use_the_shell() -> Result { +fn relative_existing_paths_with_shell_syntax_still_use_the_shell() -> TestResult { let temp = gix_testtools::tempfile::Builder::new() .prefix("$HOME") .tempdir_in(".")?; @@ -264,18 +353,20 @@ fn relative_existing_paths_with_shell_syntax_still_use_the_shell() -> Result { } #[test] -fn single_and_multiple_arguments_as_part_of_command() { - let cmd = std::process::Command::from(gix_command::prepare("ls first second third")); +fn single_and_multiple_arguments_as_part_of_command() -> TestResult { + let cmd = std::process::Command::try_from(gix_command::prepare("ls first second third"))?; assert_eq!( format!("{cmd:?}"), quoted(&["ls first second third"]), "without shell, this is an invalid command" ); + Ok(()) } #[test] -fn single_and_multiple_arguments_as_part_of_command_with_shell() { - let cmd = std::process::Command::from(gix_command::prepare("ls first second third").command_may_be_shell_script()); +fn single_and_multiple_arguments_as_part_of_command_with_shell() -> TestResult { + let cmd = + std::process::Command::try_from(gix_command::prepare("ls first second third").command_may_be_shell_script())?; assert_eq!( format!("{cmd:?}"), if cfg!(windows) { @@ -285,15 +376,16 @@ fn single_and_multiple_arguments_as_part_of_command_with_shell() { }, "with shell, this works as it performs word splitting" ); + Ok(()) } #[test] -fn single_and_multiple_arguments_as_part_of_command_with_given_shell() { - let cmd = std::process::Command::from( +fn single_and_multiple_arguments_as_part_of_command_with_given_shell() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare("ls first second third") .command_may_be_shell_script() .with_shell_program("/somepath/to/bash"), - ); + )?; assert_eq!( format!("{cmd:?}"), if cfg!(windows) { @@ -303,15 +395,16 @@ fn single_and_multiple_arguments_as_part_of_command_with_given_shell() { }, "with shell, this works as it performs word splitting on Windows, but on linux (or without splitting) it uses the given shell" ); + Ok(()) } #[test] -fn single_and_complex_arguments_as_part_of_command_with_shell() { - let cmd = std::process::Command::from( +fn single_and_complex_arguments_as_part_of_command_with_shell() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#"ls --foo "a b""#) .arg("additional") .command_may_be_shell_script(), - ); + )?; assert_eq!( format!("{cmd:?}"), if cfg!(windows) { @@ -322,63 +415,68 @@ fn single_and_complex_arguments_as_part_of_command_with_shell() { }, "with shell, this works as it performs word splitting, on windows we can avoid the shell" ); + Ok(()) } #[test] -fn single_and_complex_arguments_with_auto_split() { - let cmd = std::process::Command::from( +fn single_and_complex_arguments_with_auto_split() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted(&["ls", "--foo=a b"]), "splitting can also handle quotes" ); + Ok(()) } #[test] -fn single_and_complex_arguments_without_auto_split() { - let cmd = std::process::Command::from( +fn single_and_complex_arguments_without_auto_split() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#"ls --foo="a b""#).command_may_be_shell_script_disallow_manual_argument_splitting(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"ls --foo=\"a b\""#, SH_BASENAME]) ); + Ok(()) } #[test] -fn single_and_simple_arguments_without_auto_split_with_shell() { - let cmd = std::process::Command::from(gix_command::prepare("ls").arg("--foo=a b").with_shell()); +fn single_and_simple_arguments_without_auto_split_with_shell() -> TestResult { + let cmd = std::process::Command::try_from(gix_command::prepare("ls").arg("--foo=a b").with_shell())?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"ls \"$@\""#, SH_BASENAME, "--foo=a b"]) ); + Ok(()) } #[test] -fn quoted_command_without_argument_splitting() { - let cmd = std::process::Command::from( +fn quoted_command_without_argument_splitting() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare("ls") .arg("--foo=a b") .with_shell() .with_quoted_command(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"'ls' \"$@\""#, SH_BASENAME, "--foo=a b"]), "looks strange thanks to debug printing, but is the right amount of quotes actually" ); + Ok(()) } #[test] -fn quoted_windows_command_without_argument_splitting() { - let cmd = std::process::Command::from( +fn quoted_windows_command_without_argument_splitting() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r"C:\Users\O'Shaughnessy\with space.exe") .arg("--foo='a b'") .with_shell() .with_quoted_command(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&[ @@ -389,58 +487,63 @@ fn quoted_windows_command_without_argument_splitting() { ]), "again, a lot of extra backslashes, but it's correct outside of the debug formatting" ); + Ok(()) } #[test] -fn single_and_complex_arguments_will_not_auto_split_on_special_characters() { - let cmd = std::process::Command::from( +fn single_and_complex_arguments_will_not_auto_split_on_special_characters() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare("ls --foo=~/path").command_may_be_shell_script_allow_manual_argument_splitting(), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", "ls --foo=~/path", SH_BASENAME]), "splitting can also handle quotes" ); + Ok(()) } #[test] -fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() { - let cmd = - std::process::Command::from(gix_command::prepare(r#"~/bin/exe --foo "a b""#).command_may_be_shell_script()); +fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() -> TestResult { + let cmd = std::process::Command::try_from( + gix_command::prepare(r#"~/bin/exe --foo "a b""#).command_may_be_shell_script(), + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"~/bin/exe --foo \"a b\""#, SH_BASENAME]), "this always needs a shell as we need tilde expansion" ); + Ok(()) } #[test] -fn script_with_dollar_at() { - let cmd = std::process::Command::from( +fn script_with_dollar_at() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .arg("store"), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"echo \"$@\" >&2"#, SH_BASENAME, "store"]), "this is how credential helpers have to work as for some reason they don't get '$@' added in Git.\ We deal with it by not doubling the '$@' argument, which seems more flexible." ); + Ok(()) } #[test] #[cfg(unix)] -fn non_utf8_script_with_dollar_at_does_not_duplicate_arguments() { +fn non_utf8_script_with_dollar_at_does_not_duplicate_arguments() -> TestResult { use bstr::ByteSlice; use std::os::unix::ffi::{OsStrExt, OsStringExt}; let script = std::ffi::OsString::from_vec(b"echo \xff \"$@\"".to_vec()); - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare(script.clone()) .command_may_be_shell_script() .arg("argument"), - ); + )?; assert_eq!( cmd.get_args() .nth(1) @@ -450,24 +553,26 @@ fn non_utf8_script_with_dollar_at_does_not_duplicate_arguments() { script.as_bytes().as_bstr(), "the existing byte-encoded $@ is retained without appending another one" ); + Ok(()) } #[test] -fn script_with_dollar_at_has_no_quoting() { - let cmd = std::process::Command::from( +fn script_with_dollar_at_has_no_quoting() -> TestResult { + let cmd = std::process::Command::try_from( gix_command::prepare(r#"echo "$@" >&2"#) .command_may_be_shell_script() .with_quoted_command() .arg("store"), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted_default_shell(&["-c", r#"echo \"$@\" >&2"#, SH_BASENAME, "store"]) ); + Ok(()) } #[test] -fn shell_program_with_no_basename_uses_underscore_placeholder() { +fn shell_program_with_no_basename_uses_underscore_placeholder() -> TestResult { // Defensive fallback for degenerate input that should not occur in // practice. If a caller passes a shell path whose `file_name()` is // `None` (empty string, `/`, etc.), the `command_name` operand falls @@ -475,14 +580,15 @@ fn shell_program_with_no_basename_uses_underscore_placeholder() { // in shell one-liners. Such a "shell" would not produce a runnable // command — the fallback only keeps the construction total in the // face of bad input, without making a false claim about the shell. - let cmd = std::process::Command::from( + let cmd = std::process::Command::try_from( gix_command::prepare("echo hi") .command_may_be_shell_script_disallow_manual_argument_splitting() .with_shell_program(""), - ); + )?; assert_eq!( format!("{cmd:?}"), quoted(&["", "-c", "echo hi", "_"]), "with no basename available, the command_name operand is '_', not a guessed shell name" ); + Ok(()) } diff --git a/gix-command/tests/command/spawn.rs b/gix-command/tests/command/spawn.rs index ed96f4ef86b..d8423e6cd89 100644 --- a/gix-command/tests/command/spawn.rs +++ b/gix-command/tests/command/spawn.rs @@ -1,8 +1,8 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteSlice; #[test] -fn environment_variables_are_passed_one_by_one() -> Result { +fn environment_variables_are_passed_one_by_one() -> TestResult { let out = gix_command::prepare("echo $FIRST $SECOND") .env("FIRST", "first") .env("SECOND", "second") @@ -14,7 +14,7 @@ fn environment_variables_are_passed_one_by_one() -> Result { } #[test] -fn disallow_shell() -> Result { +fn disallow_shell() -> TestResult { let out = gix_command::prepare("PATH= echo hi") .command_may_be_shell_script_disallow_manual_argument_splitting() .spawn()? @@ -24,7 +24,7 @@ fn disallow_shell() -> Result { let mut cmd: std::process::Command = gix_command::prepare("echo hi") .command_may_be_shell_script() .without_shell() - .into(); + .try_into()?; assert!( cmd.env_remove("PATH").spawn().is_err(), "no command named 'echo hi' exists" @@ -33,12 +33,12 @@ fn disallow_shell() -> Result { } #[test] -fn script_with_dollar_at() -> Result { - let out = std::process::Command::from( +fn script_with_dollar_at() -> TestResult { + let out = std::process::Command::try_from( gix_command::prepare(r#"echo "$@""#) .command_may_be_shell_script() .arg("arg"), - ) + )? .spawn()? .wait_with_output()?; assert_eq!( @@ -50,7 +50,7 @@ fn script_with_dollar_at() -> Result { } #[test] -fn direct_command_execution_searches_in_path() -> Result { +fn direct_command_execution_searches_in_path() -> TestResult { assert!( gix_command::prepare(if cfg!(unix) { "ls" } else { "attrib.exe" }) .spawn()? @@ -62,17 +62,17 @@ fn direct_command_execution_searches_in_path() -> Result { #[cfg(unix)] #[test] -fn direct_command_with_absolute_command_path() -> Result { +fn direct_command_with_absolute_command_path() -> TestResult { assert!(gix_command::prepare("/usr/bin/env").spawn()?.wait()?.success()); Ok(()) } mod with_shell { - use crate::Result; + use crate::TestResult; use gix_testtools::bstr::ByteSlice; #[test] - fn command_in_path_with_args() -> Result { + fn command_in_path_with_args() -> TestResult { // `ls` is occasionaly a builtin, as in busybox ash, but it is usually external. assert!( gix_command::prepare(if cfg!(unix) { "ls -l" } else { "attrib.exe /d" }) @@ -86,7 +86,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path() -> Result { + fn shell_builtin_or_command_in_path() -> TestResult { let out = gix_command::prepare("echo") .command_may_be_shell_script() .spawn()? @@ -98,7 +98,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path_with_single_extra_arg() -> Result { + fn shell_builtin_or_command_in_path_with_single_extra_arg() -> TestResult { let out = gix_command::prepare("printf") .command_may_be_shell_script() .arg("1") @@ -111,7 +111,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn shell_builtin_or_command_in_path_with_multiple_extra_args() -> Result { + fn shell_builtin_or_command_in_path_with_multiple_extra_args() -> TestResult { let out = gix_command::prepare("printf") .command_may_be_shell_script() .arg("%s") @@ -124,7 +124,7 @@ mod with_shell { } #[test] - fn force_shell_builtin() -> Result { + fn force_shell_builtin() -> TestResult { let out = gix_command::prepare("echo").with_shell().spawn()?.wait_with_output()?; assert!(out.status.success()); assert_eq!(out.stdout.as_bstr(), "\n"); @@ -132,7 +132,7 @@ mod with_shell { } #[test] - fn force_shell_builtin_with_single_extra_arg() -> Result { + fn force_shell_builtin_with_single_extra_arg() -> TestResult { let out = gix_command::prepare("printf") .with_shell() .arg("1") @@ -144,7 +144,7 @@ mod with_shell { } #[test] - fn force_shell_builtin_with_multiple_extra_args() -> Result { + fn force_shell_builtin_with_multiple_extra_args() -> TestResult { let out = gix_command::prepare("printf") .with_shell() .arg("%s") @@ -157,7 +157,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code() -> Result { + fn sh_shell_specific_script_code() -> TestResult { assert!( gix_command::prepare(":;:;:") .command_may_be_shell_script() @@ -169,7 +169,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code_with_single_extra_arg() -> Result { + fn sh_shell_specific_script_code_with_single_extra_arg() -> TestResult { let out = gix_command::prepare(":;printf") .command_may_be_shell_script() .arg("1") @@ -181,7 +181,7 @@ mod with_shell { } #[test] - fn sh_shell_specific_script_code_with_multiple_extra_args() -> Result { + fn sh_shell_specific_script_code_with_multiple_extra_args() -> TestResult { let out = gix_command::prepare(":;printf") .command_may_be_shell_script() .arg("%s") @@ -195,7 +195,7 @@ mod with_shell { #[cfg(unix)] #[test] - fn dollar_zero_in_minus_c_is_basename_of_default_shell() -> Result { + fn dollar_zero_in_minus_c_is_basename_of_default_shell() -> TestResult { let out = gix_command::prepare(r#"printf %s "$0""#) .command_may_be_shell_script() .spawn()? @@ -211,12 +211,12 @@ mod with_shell { #[cfg(unix)] #[test] - fn dollar_zero_in_minus_c_reflects_with_shell_program() -> Result { - let out = std::process::Command::from( + fn dollar_zero_in_minus_c_reflects_with_shell_program() -> TestResult { + let out = std::process::Command::try_from( gix_command::prepare(r#"printf %s "$0""#) .command_may_be_shell_script() .with_shell_program(gix_testtools::bash_program()), - ) + )? .spawn()? .wait_with_output()?; assert_eq!( diff --git a/gix-commitgraph/tests/commitgraph.rs b/gix-commitgraph/tests/commitgraph.rs index 855192d4b9d..cbe54f9d959 100644 --- a/gix-commitgraph/tests/commitgraph.rs +++ b/gix-commitgraph/tests/commitgraph.rs @@ -12,7 +12,7 @@ use gix_testtools::scripted_fixture_read_only; mod access; #[test] -fn missing_path_is_not_found() -> gix_testtools::Result { +fn missing_path_is_not_found() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let err = gix_commitgraph::at(dir.path().join("missing")) .err() @@ -31,7 +31,7 @@ fn missing_path_is_not_found() -> gix_testtools::Result { } #[test] -fn checksum_mismatches_retain_their_classification() -> gix_testtools::Result { +fn checksum_mismatches_retain_their_classification() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_writable("single_commit.sh")?; let mut data = std::fs::read(repo.path().join(".git/objects/info/commit-graph"))?; *data.last_mut().expect("the graph has a checksum trailer") ^= 1; diff --git a/gix-config-value/src/path.rs b/gix-config-value/src/path.rs index fc710e449ec..accd4867452 100644 --- a/gix-config-value/src/path.rs +++ b/gix-config-value/src/path.rs @@ -161,10 +161,9 @@ impl Path { if self.starts_with(PREFIX) { let git_install_dir = git_install_dir.ok_or_raise(|| not_found("git install dir is missing"))?; let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len()); - let path_without_trailing_slash = - gix_path::try_from_bstring(path_without_trailing_slash).or_raise(|| { - validation("Ill-formed UTF-8 in path past %(prefix)").with_input(path_without_trailing_slash) - })?; + let path_without_trailing_slash = gix_path::from_bstring(path_without_trailing_slash).or_raise(|| { + validation("Ill-formed UTF-8 in path past %(prefix)").with_input(path_without_trailing_slash) + })?; Ok(git_install_dir.join(path_without_trailing_slash)) } else if let Some(val) = self.strip_prefix(b"~") { let (username, path) = match val.split_once_str(b"/") { @@ -187,13 +186,13 @@ impl Path { }; if let Some(path) = path { home.push( - gix_path::try_from_byte_slice(path) + gix_path::from_byte_slice(path) .or_raise(|| validation(format!("Ill-formed UTF-8 in {what}")).with_input(path))?, ); } Ok(home) } else { - Ok(gix_path::from_bstr(self.value.as_bstr()).into_owned()) + Ok(gix_path::from_bstr(self.value.as_bstr())?.into_owned()) } } diff --git a/gix-config-value/tests/value/boolean.rs b/gix-config-value/tests/value/boolean.rs index 58799158b90..147c9d9a79d 100644 --- a/gix-config-value/tests/value/boolean.rs +++ b/gix-config-value/tests/value/boolean.rs @@ -1,8 +1,8 @@ use gix_config_value::Boolean; -use gix_error::Result; +use gix_testtools::TestResult; #[test] -fn from_utf8_str() -> Result { +fn from_utf8_str() -> TestResult { assert_eq!( Boolean::try_from("yes")?, Boolean(true), @@ -12,7 +12,7 @@ fn from_utf8_str() -> Result { } #[test] -fn from_str_false() -> Result { +fn from_str_false() -> TestResult { assert!(!Boolean::try_from("no")?.0); assert!(!Boolean::try_from("off")?.0); assert!(!Boolean::try_from("false")?.0); @@ -22,7 +22,7 @@ fn from_str_false() -> Result { } #[test] -fn from_str_true() -> Result { +fn from_str_true() -> TestResult { assert!(Boolean::try_from("yes")?.0); assert!(Boolean::try_from("on")?.0); assert!(Boolean::try_from("true")?.0); @@ -33,19 +33,18 @@ fn from_str_true() -> Result { } #[test] -fn ignores_case() { +fn ignores_case() -> gix_testtools::TestResult { // Random subset for word in &["no", "yes", "on", "off", "true", "false"] { - let first: bool = Boolean::try_from(*word).expect("valid boolean").into(); - let second: bool = Boolean::try_from(word.to_uppercase().as_str()) - .expect("valid boolean") - .into(); + let first: bool = Boolean::try_from(*word)?.into(); + let second: bool = Boolean::try_from(word.to_uppercase().as_str())?.into(); assert_eq!(first, second); } + Ok(()) } #[test] -fn numbers_are_parsed_as_integers() -> Result { +fn numbers_are_parsed_as_integers() -> TestResult { // Use the same bases, suffixes, and full `i64` range as `Integer`. for (input, expected) in [ ("0x10", true), diff --git a/gix-config-value/tests/value/color.rs b/gix-config-value/tests/value/color.rs index 112e4b261d7..3a55009fb22 100644 --- a/gix-config-value/tests/value/color.rs +++ b/gix-config-value/tests/value/color.rs @@ -1,8 +1,8 @@ use gix_config_value::Color; -use gix_error::Result; +use gix_testtools::TestResult; #[test] -fn from_utf8_str() -> Result { +fn from_utf8_str() -> TestResult { assert_eq!( Color::try_from("red bold")?.to_string(), "red bold", diff --git a/gix-config-value/tests/value/integer.rs b/gix-config-value/tests/value/integer.rs index 22c886cc5be..fb95fe8c1ea 100644 --- a/gix-config-value/tests/value/integer.rs +++ b/gix-config-value/tests/value/integer.rs @@ -2,10 +2,10 @@ use std::borrow::Cow; use bstr::{BStr, BString}; use gix_config_value::{Integer, integer::Suffix}; -use gix_error::{MetadataValue, Result}; +use gix_error::MetadataValue; #[test] -fn from_utf8_str() -> Result { +fn from_utf8_str() -> gix_testtools::TestResult { assert_eq!( Integer::try_from("1k")?, Integer { @@ -70,7 +70,7 @@ fn invalid_from_str() { } #[test] -fn from_bytes_accepts_common_inputs() -> Result { +fn from_bytes_accepts_common_inputs() -> gix_testtools::TestResult { let owned = BString::from("0X800"); for actual in [ Integer::from_bytes::<i64>("2k")?, @@ -94,7 +94,7 @@ fn from_bytes_accepts_common_inputs() -> Result { } #[test] -fn from_bytes_applies_suffixes_before_converting_to_the_target() -> Result { +fn from_bytes_applies_suffixes_before_converting_to_the_target() -> gix_testtools::TestResult { for (input, expected) in [ ("12", 12), ("13k", 13 * 1024), diff --git a/gix-config-value/tests/value/path.rs b/gix-config-value/tests/value/path.rs index c5b585c43b9..ba0a67362b3 100644 --- a/gix-config-value/tests/value/path.rs +++ b/gix-config-value/tests/value/path.rs @@ -6,7 +6,7 @@ mod interpolate { use gix_config_value::path; #[test] - fn backslash_is_not_special_and_they_are_not_escaping_anything() -> Result { + fn backslash_is_not_special_and_they_are_not_escaping_anything() -> gix_testtools::TestResult { for path in [r"C:\foo\bar", "/foo/bar"] { let actual = gix_config_value::Path::from(path).interpolate(Default::default())?; assert_eq!(actual, Path::new(path)); @@ -22,42 +22,40 @@ mod interpolate { } #[test] - fn prefix_substitutes_git_install_dir() { + fn prefix_substitutes_git_install_dir() -> gix_testtools::TestResult { for git_install_dir in &["/tmp/git", r"C:\git"] { for (val, expected) in &[("%(prefix)/foo/bar", "foo/bar"), (r"%(prefix)/foo\bar", r"foo\bar")] { let expected = std::path::PathBuf::from(format!("{}{}{}", git_install_dir, std::path::MAIN_SEPARATOR, expected)); assert_eq!( - gix_config_value::Path::from(*val) - .interpolate(path::interpolate::Context { - git_install_dir: Path::new(git_install_dir).into(), - ..Default::default() - }) - .expect("valid interpolation"), + gix_config_value::Path::from(*val).interpolate(path::interpolate::Context { + git_install_dir: Path::new(git_install_dir).into(), + ..Default::default() + })?, expected, "prefix interpolation keeps separators as they are" ); } } + Ok(()) } #[test] - fn prefix_substitution_skipped_with_dot_slash() { + fn prefix_substitution_skipped_with_dot_slash() -> gix_testtools::TestResult { let path = "./%(prefix)/foo/bar"; let git_install_dir = "/tmp/git"; assert_eq!( - gix_config_value::Path::from(path) - .interpolate(path::interpolate::Context { - git_install_dir: Path::new(git_install_dir).into(), - ..Default::default() - }) - .expect("valid interpolation"), + gix_config_value::Path::from(path).interpolate(path::interpolate::Context { + git_install_dir: Path::new(git_install_dir).into(), + ..Default::default() + })?, Path::new(path) ); + Ok(()) } #[test] - fn tilde_alone_substitutes_current_user() -> Result { + fn tilde_alone_substitutes_current_user() -> gix_testtools::TestResult { let home = std::env::current_dir().expect("current directory is available"); assert_eq!( gix_config_value::Path::from("~").interpolate(path::interpolate::Context { @@ -80,8 +78,8 @@ mod interpolate { } #[test] - fn tilde_slash_substitutes_current_user() -> Result { - let home = std::env::current_dir().expect("current directory is available"); + fn tilde_slash_substitutes_current_user() -> gix_testtools::TestResult { + let home = std::env::current_dir()?; for suffix in ["", "user/bar", r"user\bar", "/user/bar"] { let actual = gix_config_value::Path::from(format!("~/{suffix}").as_str()).interpolate( path::interpolate::Context { @@ -100,7 +98,7 @@ mod interpolate { } #[test] - fn tilde_with_given_user() -> Result { + fn tilde_with_given_user() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let home = std::env::current_dir().expect("current directory is available"); diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index ea12c268c39..27249041603 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -248,7 +248,7 @@ fn gitdir_matches( let git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_dir.ok_or_raise(|| { not_found("The git directory must be provided to support `gitdir:` conditional includes") - })?)); + })?)?); let mut pattern_path = match check_interpolation_result( err_on_interpolation_failure, @@ -256,7 +256,7 @@ fn gitdir_matches( ) .or_raise(|| message("Could not interpolate conditional include path"))? { - Some(path) => gix_path::into_bstr(path).into_owned(), + Some(path) => gix_path::into_bstr(path)?.into_owned(), // Git keeps the original condition pattern when interpolation fails. None => condition_path.to_owned(), }; @@ -275,7 +275,7 @@ fn gitdir_matches( })? .parent() .expect("config path can never be /"); - let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)).into_owned(); + let mut joined_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(parent_dir)?).into_owned(); joined_path.push(b'/'); joined_path.extend_from_slice(relative_pattern_path); pattern_path = joined_path; @@ -283,7 +283,7 @@ fn gitdir_matches( // NOTE: this special handling of leading backslash is needed to do it like git does if pattern_path.iter().next() != Some(&(std::path::MAIN_SEPARATOR as u8)) - && !gix_path::from_bstr(pattern_path.clone()).is_absolute() + && !gix_path::from_bstr(pattern_path.clone())?.is_absolute() { pattern_path.insert_str(0, "**/"); } @@ -298,9 +298,9 @@ fn gitdir_matches( } let expanded_git_dir = gix_path::to_unix_separators_on_windows(gix_path::into_bstr( - gix_path::realpath(gix_path::from_byte_slice(&git_dir)) + gix_path::realpath(gix_path::from_byte_slice(&git_dir)?) .or_raise(|| message("Could not resolve the git directory to its real path"))?, - )); + )?); Ok(gix_glob::wildmatch( pattern_path.as_bstr(), expanded_git_dir.as_ref(), diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index 5ca094e089e..3b21d233596 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -1,8 +1,8 @@ mod new_section { - use crate::Result; + use crate::TestResult; #[test] - fn accepts_a_borrowed_subsection_name() -> Result { + fn accepts_a_borrowed_subsection_name() -> TestResult { let mut file = gix_config::File::default(); file.new_section("remote", "origin")?; file.new_section("branch", "main")?; @@ -17,7 +17,7 @@ mod new_section { } #[test] - fn owned_sections_accept_a_borrowed_subsection_name() -> Result { + fn owned_sections_accept_a_borrowed_subsection_name() -> TestResult { let section = gix_config::file::Section::new("remote", "origin", gix_config::file::Metadata::default())?; assert_eq!(section.to_ref().header().subsection_name(), Some("origin".into())); Ok(()) @@ -26,8 +26,8 @@ mod new_section { mod new_section_with_meta { use gix_config::{File, Source, file::Metadata}; - use gix_error::TestResult; use gix_features::threading::OwnShared; + use gix_testtools::TestResult; #[test] fn metadata_is_specific_to_the_new_section() -> TestResult { @@ -99,7 +99,7 @@ mod new_section_with_meta { } mod remove_section { - use crate::Result; + use crate::TestResult; #[test] fn removal_of_all_sections_programmatically_with_sections_and_ids_by_name() { @@ -127,8 +127,8 @@ mod remove_section { } #[test] - fn removal_is_complete_and_sections_can_be_read() { - let mut file = gix_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap(); + fn removal_is_complete_and_sections_can_be_read() -> gix_testtools::TestResult { + let mut file = gix_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2")?; assert_eq!(file.sections().count(), 2); let removed = file.remove_section("core", None).expect("removed correct section"); @@ -143,13 +143,13 @@ mod remove_section { assert_eq!(file.sections().count(), 0); assert!(file.remove_section("core", "name").is_none()); - file.section_mut_or_create_new("core", None).expect("creation succeeds"); - file.section_mut_or_create_new("core", "name") - .expect("creation succeeds"); + file.section_mut_or_create_new("core", None)?; + file.section_mut_or_create_new("core", "name")?; + Ok(()) } #[test] - fn removing_lookup_buckets_preserves_siblings_and_drops_the_final_name() -> Result { + fn removing_lookup_buckets_preserves_siblings_and_drops_the_final_name() -> TestResult { let mut file = gix_config::File::try_from( "[core] key=plain\n\ [core \"a\"] key=a\n\ @@ -173,7 +173,7 @@ mod remove_section { } #[test] - fn removed_sections_can_be_mutated_and_reinserted() -> Result { + fn removed_sections_can_be_mutated_and_reinserted() -> TestResult { let mut file = gix_config::File::try_from("[core]\na = b\n")?; let mut section = file.remove_section("core", None).expect("section is present"); let removed_id = section.to_ref().id(); @@ -190,8 +190,8 @@ mod remove_section { } mod remove_section_filter { #[test] - fn removal_of_section_is_complete() { - let mut file = gix_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2").unwrap(); + fn removal_of_section_is_complete() -> gix_testtools::TestResult { + let mut file = gix_config::File::try_from("[core] \na = b\nb=c\n\n[core \"name\"]\nd = 1\ne = 2")?; assert_eq!(file.sections().count(), 2); let removed = file @@ -211,14 +211,14 @@ mod remove_section_filter { ); assert!(file.remove_section_filter("core", "name", |_| true).is_none()); - file.section_mut_or_create_new("core", None).expect("creation succeeds"); - file.section_mut_or_create_new("core", "name") - .expect("creation succeeds"); + file.section_mut_or_create_new("core", None)?; + file.section_mut_or_create_new("core", "name")?; + Ok(()) } } mod rename_section { - use crate::Result; + use crate::TestResult; #[test] fn section_renaming_validates_new_name() { @@ -233,7 +233,7 @@ mod rename_section { } #[test] - fn accepts_borrowed_new_subsection_names() -> Result { + fn accepts_borrowed_new_subsection_names() -> TestResult { let mut file = gix_config::File::try_from("[core] a = b")?; file.rename_section("core", None, "remote", "origin")?; assert_eq!( @@ -251,7 +251,7 @@ mod rename_section { } #[test] - fn all_matching_sections_are_renamed_and_target_collisions_are_preserved() -> Result { + fn all_matching_sections_are_renamed_and_target_collisions_are_preserved() -> TestResult { let mut file = gix_config::File::try_from( "[branch \"source\"] key = one\n\ [some \"gar\"] key = unrelated\n\ @@ -275,7 +275,7 @@ mod rename_section { } #[test] - fn filter_renames_every_accepted_section() -> Result { + fn filter_renames_every_accepted_section() -> TestResult { let mut file = gix_config::File::try_from( "[branch \"source\"] key = one\n\ [branch \"source\"] key = two\n\ @@ -321,7 +321,7 @@ mod rename_section { } #[test] - fn renaming_to_the_same_identity_updates_all_headers() -> Result { + fn renaming_to_the_same_identity_updates_all_headers() -> TestResult { let mut file = gix_config::File::try_from( "[branch.source] one = 1\n\ [branch.source] two = 2\n", @@ -337,7 +337,7 @@ mod rename_section { } #[test] - fn an_empty_lookup_bucket_is_reported_as_missing() -> Result { + fn an_empty_lookup_bucket_is_reported_as_missing() -> TestResult { let mut file = gix_config::File::try_from("[core] key = value\n")?; file.remove_section("core", None).expect("section exists"); let err = file.rename_section("core", None, "other", None).unwrap_err(); @@ -347,11 +347,11 @@ mod rename_section { } } mod set_meta { - use crate::Result; + use crate::TestResult; use gix_config::file; #[test] - fn affects_newly_added_sections() -> Result { + fn affects_newly_added_sections() -> TestResult { let mut file = gix_config::File::default(); let expected = &file::Metadata::api(); assert_eq!(file.meta(), expected); diff --git a/gix-config/tests/config/file/access/raw/raw_multi_value.rs b/gix-config/tests/config/file/access/raw/raw_multi_value.rs index dca64d1c664..9dcd547750c 100644 --- a/gix-config/tests/config/file/access/raw/raw_multi_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_multi_value.rs @@ -1,24 +1,24 @@ -use crate::Result; +use crate::TestResult; use gix_config::File; use crate::file::bstring; #[test] -fn single_value_is_identical_to_single_value_query() -> Result { +fn single_value_is_identical_to_single_value_query() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; assert_eq!(vec![config.raw_value("core.a")?], config.raw_values("core.a")?); Ok(()) } #[test] -fn multi_value_in_section() -> Result { +fn multi_value_in_section() -> TestResult { let config = File::try_from("[core]\na=b\na=c")?; assert_eq!(config.raw_values("core.a")?, vec![bstring("b"), bstring("c")]); Ok(()) } #[test] -fn multi_value_across_sections() -> Result { +fn multi_value_across_sections() -> TestResult { let config = File::try_from( "[core]\n\ a=b\n\ @@ -33,7 +33,7 @@ fn multi_value_across_sections() -> Result { } #[test] -fn values_with_sections_identify_each_values_section_in_file_order() -> Result { +fn values_with_sections_identify_each_values_section_in_file_order() -> TestResult { let config = File::try_from( "[core]\n\ a=b\n\ @@ -62,7 +62,7 @@ fn values_with_sections_identify_each_values_section_in_file_order() -> Result { } #[test] -fn values_with_sections_filter_returns_values_from_accepted_sections() -> Result { +fn values_with_sections_filter_returns_values_from_accepted_sections() -> TestResult { let config = File::try_from( "[core]\n\ a=b\n\ @@ -93,7 +93,7 @@ fn values_with_sections_filter_returns_values_from_accepted_sections() -> Result } #[test] -fn section_not_found() -> Result { +fn section_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("foo.a").unwrap_err(); assert!(err.is_not_found()); @@ -102,7 +102,7 @@ fn section_not_found() -> Result { } #[test] -fn subsection_not_found() -> Result { +fn subsection_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.a.a").unwrap_err(); assert!(err.is_not_found()); @@ -111,7 +111,7 @@ fn subsection_not_found() -> Result { } #[test] -fn key_not_found() -> Result { +fn key_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_values("core.aaaaaa").unwrap_err(); assert!(err.is_not_found()); @@ -120,7 +120,7 @@ fn key_not_found() -> Result { } #[test] -fn subsection_must_be_respected() -> Result { +fn subsection_must_be_respected() -> TestResult { let config = File::try_from("[core]a=b\n[core.a]a=c")?; assert_eq!(config.raw_values("core.a")?, vec![bstring("b")]); assert_eq!(config.raw_values("core.a.a")?, vec![bstring("c")]); @@ -128,7 +128,7 @@ fn subsection_must_be_respected() -> Result { } #[test] -fn non_relevant_subsection_is_ignored() -> Result { +fn non_relevant_subsection_is_ignored() -> TestResult { let config = File::try_from("[core]\na=b\na=c\n[core]a=d\n[core]g=g")?; assert_eq!( config.raw_values("core.a")?, diff --git a/gix-config/tests/config/file/access/raw/raw_value.rs b/gix-config/tests/config/file/access/raw/raw_value.rs index 6274c0ef594..920f902ee38 100644 --- a/gix-config/tests/config/file/access/raw/raw_value.rs +++ b/gix-config/tests/config/file/access/raw/raw_value.rs @@ -1,8 +1,8 @@ -use crate::Result; +use crate::TestResult; use gix_config::File; #[test] -fn single_section() -> Result { +fn single_section() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; assert_eq!(config.raw_value("core.a")?, "b"); assert_eq!(config.raw_value_by("core", None, "c")?, "d"); @@ -10,28 +10,28 @@ fn single_section() -> Result { } #[test] -fn global_property_uses_empty_section_name() -> Result { +fn global_property_uses_empty_section_name() -> TestResult { let config = File::try_from("a=b\n[core]\na=c")?; insta::assert_debug_snapshot!(config.raw_value_by("", None, "a").expect_err("these are not readable because the supporting this adds a lot of complexity"), "these are not readable because the supporting this adds a lot of complexity", @"The requested section does not exist"); Ok(()) } #[test] -fn last_one_wins_respected_in_section() -> Result { +fn last_one_wins_respected_in_section() -> TestResult { let config = File::try_from("[core]\na=b\na=d")?; assert_eq!(config.raw_value("core.a")?, "d"); Ok(()) } #[test] -fn last_one_wins_respected_across_section() -> Result { +fn last_one_wins_respected_across_section() -> TestResult { let config = File::try_from("[core]\na=b\n[core]\na=d")?; assert_eq!(config.raw_value("core.a")?, "d"); Ok(()) } #[test] -fn value_with_section_identifies_the_section_containing_the_resolved_value() -> Result { +fn value_with_section_identifies_the_section_containing_the_resolved_value() -> TestResult { let config = File::try_from( "[core]\n\ a=first\n\ @@ -51,7 +51,7 @@ fn value_with_section_identifies_the_section_containing_the_resolved_value() -> } #[test] -fn value_with_section_filter_identifies_the_section_containing_the_resolved_value() -> Result { +fn value_with_section_filter_identifies_the_section_containing_the_resolved_value() -> TestResult { let config = File::try_from( "[core]\n\ a=first\n\ @@ -73,7 +73,7 @@ fn value_with_section_filter_identifies_the_section_containing_the_resolved_valu } #[test] -fn mutable_value_filters_have_key_and_component_variants() -> Result { +fn mutable_value_filters_have_key_and_component_variants() -> TestResult { let mut config = File::try_from( "[core]\n\ a=first\n\ @@ -110,7 +110,7 @@ fn mutable_value_filters_have_key_and_component_variants() -> Result { } #[test] -fn section_not_found() -> Result { +fn section_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("foo.a").unwrap_err(); assert!(err.is_not_found()); @@ -119,7 +119,7 @@ fn section_not_found() -> Result { } #[test] -fn subsection_not_found() -> Result { +fn subsection_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.a.a").unwrap_err(); assert!(err.is_not_found()); @@ -128,7 +128,7 @@ fn subsection_not_found() -> Result { } #[test] -fn key_not_found() -> Result { +fn key_not_found() -> TestResult { let config = File::try_from("[core]\na=b\nc=d")?; let err = config.raw_value("core.aaaaaa").unwrap_err(); assert!(err.is_not_found()); @@ -137,7 +137,7 @@ fn key_not_found() -> Result { } #[test] -fn invalid_value_names_are_reported_by_mutable_lookups() -> Result { +fn invalid_value_names_are_reported_by_mutable_lookups() -> TestResult { let mut config = File::try_from("[core]\na=b")?; let err = config.raw_value_mut_by("core", None, "1invalid").unwrap_err(); assert!(err.is_validation()); @@ -149,7 +149,7 @@ fn invalid_value_names_are_reported_by_mutable_lookups() -> Result { } #[test] -fn subsection_must_be_respected() -> Result { +fn subsection_must_be_respected() -> TestResult { let config = File::try_from("[core]a=b\n[core.a]a=c")?; assert_eq!(config.raw_value("core.a")?, "b"); assert_eq!(config.raw_value("core.a.a")?, "c"); diff --git a/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs b/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs index e3e0cd5f682..134a8aa3b49 100644 --- a/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_existing_raw_value.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; fn file(input: &str) -> gix_config::File { input.parse().unwrap() @@ -23,7 +23,7 @@ fn single_line() { } #[test] -fn global_property_uses_empty_section_name() -> Result { +fn global_property_uses_empty_section_name() -> TestResult { let mut file = file("a=b\n[core]\na=c"); let err = file.set_existing_raw_value_by("", None, "a", "d").unwrap_err(); insta::assert_debug_snapshot!(err, "cannot set global values", @"The requested section does not exist"); diff --git a/gix-config/tests/config/file/access/raw/set_raw_value.rs b/gix-config/tests/config/file/access/raw/set_raw_value.rs index d911cc617a2..2bdfca0cf19 100644 --- a/gix-config/tests/config/file/access/raw/set_raw_value.rs +++ b/gix-config/tests/config/file/access/raw/set_raw_value.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; fn file(input: &str) -> gix_config::File { input.parse().unwrap() @@ -51,7 +51,7 @@ fn comment_included() { } #[test] -fn non_existing_values_cannot_be_set() -> Result { +fn non_existing_values_cannot_be_set() -> TestResult { let mut file = gix_config::File::default(); file.set_raw_value_by("new", None, "key", "value")?; file.set_raw_value_by("new", "subsection", "key", "subsection-value")?; @@ -66,7 +66,7 @@ fn non_existing_values_cannot_be_set() -> Result { } #[test] -fn accepts_short_lived_keys() -> Result { +fn accepts_short_lived_keys() -> TestResult { let mut file = gix_config::File::default(); let key = String::from("new.key"); diff --git a/gix-config/tests/config/file/access/read_only.rs b/gix-config/tests/config/file/access/read_only.rs index 58cb750d371..b577196cddd 100644 --- a/gix-config/tests/config/file/access/read_only.rs +++ b/gix-config/tests/config/file/access/read_only.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::fs; use bstr::{BString, ByteSlice}; @@ -15,7 +15,7 @@ fn lookup_error(err: gix_config::lookup::Error<gix_error::Error>) -> gix_error:: } #[test] -fn typed_lookup_errors_can_be_erased() -> Result { +fn typed_lookup_errors_can_be_erased() -> TestResult { let mut error_snapshots = Vec::new(); use gix_error::ResultExt; @@ -56,7 +56,7 @@ fn typed_lookup_errors_can_be_erased() -> Result { } #[test] -fn integer_accessors_apply_suffixes() -> Result { +fn integer_accessors_apply_suffixes() -> TestResult { let config = File::try_from("[core]\nvalue = -2k\nvalue = 0x10m\n")?; assert_eq!( config.integer("core.value")?, @@ -84,7 +84,7 @@ fn integer_accessors_apply_suffixes() -> Result { } #[test] -fn integer_accessors_retain_classification_and_input() -> Result { +fn integer_accessors_retain_classification_and_input() -> TestResult { for input in [ b"invalid".as_slice(), b"9223372036854775808", @@ -113,7 +113,7 @@ fn integer_accessors_retain_classification_and_input() -> Result { } #[test] -fn parsed_section_header_legacy_check_uses_backing_buffer() -> Result { +fn parsed_section_header_legacy_check_uses_backing_buffer() -> TestResult { let config = File::try_from( "[remote.origin]\n\turl = https://example.com\n[remote \"upstream\"]\n\turl = https://example.com\n", )?; @@ -127,7 +127,7 @@ fn parsed_section_header_legacy_check_uses_backing_buffer() -> Result { /// Asserts we can cast into all variants of our type #[test] -fn get_value_for_all_provided_values() -> Result { +fn get_value_for_all_provided_values() -> TestResult { let config = r#" [core] other-quoted = "hello" @@ -295,7 +295,7 @@ fn get_value_for_all_provided_values() -> Result { } #[test] -fn get_value_looks_up_all_sections_before_failing() -> Result { +fn get_value_looks_up_all_sections_before_failing() -> TestResult { let config = r#" [core] bool-explicit = false @@ -325,7 +325,7 @@ fn get_value_looks_up_all_sections_before_failing() -> Result { } #[test] -fn interpreted_values_can_be_returned_with_their_sections() -> Result { +fn interpreted_values_can_be_returned_with_their_sections() -> TestResult { let file = File::try_from( "[core]\n\ a=1\n\ @@ -360,7 +360,7 @@ fn interpreted_values_can_be_returned_with_their_sections() -> Result { } #[test] -fn section_names_are_case_insensitive() -> Result { +fn section_names_are_case_insensitive() -> TestResult { let config = "[core] a=true"; let file = File::try_from(config)?; assert_eq!( @@ -372,7 +372,7 @@ fn section_names_are_case_insensitive() -> Result { } #[test] -fn value_names_are_case_insensitive() -> Result { +fn value_names_are_case_insensitive() -> TestResult { let config = "[core] a = true A = false"; @@ -387,7 +387,7 @@ fn value_names_are_case_insensitive() -> Result { } #[test] -fn section_value_access_is_case_insensitive() -> Result { +fn section_value_access_is_case_insensitive() -> TestResult { let file = File::try_from("[core]\nMixedCase = one\nMIXEDCASE = two")?; let section = file.section("core", None)?; @@ -415,7 +415,7 @@ fn single_section() { } #[test] -fn sections_by_name() -> Result { +fn sections_by_name() -> TestResult { let config = r#" [core] repositoryformatversion = 0 @@ -434,7 +434,7 @@ fn sections_by_name() -> Result { } #[test] -fn sections_by_name_ignores_subsections_and_preserves_file_order() -> Result { +fn sections_by_name_ignores_subsections_and_preserves_file_order() -> TestResult { let config = File::try_from( "[remote] marker=plain\n\ [other] marker=unrelated\n\ @@ -462,7 +462,7 @@ fn sections_by_name_ignores_subsections_and_preserves_file_order() -> Result { } #[test] -fn unknown_section() -> Result { +fn unknown_section() -> TestResult { let config = File::default(); let err = config.section("missing", None).unwrap_err(); assert!(err.is_not_found()); @@ -598,7 +598,7 @@ fn multi_line_value_with_empty_continuation_line() { } #[test] -fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> Result { +fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> gix_testtools::TestResult { let baseline = crate::scripted_fixture_read_only("make_value_whitespace_baseline.sh")?; let baseline = fs::read(baseline.join("baseline.git"))?; let baseline = baseline @@ -607,7 +607,7 @@ fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> Result let mut records = baseline.split(|byte| *byte == 0); while let Some(description) = records.next() { - let description = std::str::from_utf8(description).expect("fixture descriptions must be valid UTF-8"); + let description = std::str::from_utf8(description)?; let config = records.next().expect("each description must be followed by a config"); let expected = records.next().expect("each config must be followed by Git's value"); let expected = BString::from(expected); @@ -627,22 +627,19 @@ fn multi_line_value_starting_on_a_continuation_line_is_not_indented() -> Result } #[test] -fn overrides_with_implicit_booleans_work_in_single_section() { +fn overrides_with_implicit_booleans_work_in_single_section() -> gix_testtools::TestResult { let config = r#" [a] b = false b "#; - let config = File::try_from(config).expect("valid config"); - assert_eq!( - config.boolean("a.b").expect("valid boolean"), - Some(true), - "empty implicit booleans " - ); + let config = File::try_from(config)?; + assert_eq!(config.boolean("a.b")?, Some(true), "empty implicit booleans "); + Ok(()) } #[test] -fn implicit_booleans_may_be_followed_by_whitespace() -> Result { +fn implicit_booleans_may_be_followed_by_whitespace() -> TestResult { for config in [ "[a]\n\tb \n", "[a]\n\tb\t\n", @@ -683,17 +680,14 @@ fn implicit_booleans_may_be_followed_by_whitespace() -> Result { } #[test] -fn overrides_with_implicit_booleans_work_across_sections() { +fn overrides_with_implicit_booleans_work_across_sections() -> gix_testtools::TestResult { let config = r#" [a] b = false [a] b "#; - let config = File::try_from(config).expect("valid config"); - assert_eq!( - config.boolean("a.b").expect("valid boolean"), - Some(true), - "empty implicit booleans " - ); + let config = File::try_from(config)?; + assert_eq!(config.boolean("a.b")?, Some(true), "empty implicit booleans "); + Ok(()) } diff --git a/gix-config/tests/config/file/impls.rs b/gix-config/tests/config/file/impls.rs index fdc101e46bd..cd9ab292837 100644 --- a/gix-config/tests/config/file/impls.rs +++ b/gix-config/tests/config/file/impls.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use gix_config::File; #[test] @@ -71,7 +71,7 @@ fn can_reconstruct_configs_without_whitespace_in_middle() { } #[test] -fn equality_ignores_section_and_value_name_case_but_not_subsection_case() -> Result { +fn equality_ignores_section_and_value_name_case_but_not_subsection_case() -> TestResult { let mixed_case = File::try_from("[Core]\nMixedCase = value\n[Remote \"Origin\"]\nURL = location\n")?; let equivalent = File::try_from("[core]\nmixedcase = value\n[remote \"Origin\"]\nurl = location\n")?; assert_eq!(mixed_case, equivalent, "section and value names are case-insensitive"); diff --git a/gix-config/tests/config/file/init/comfort.rs b/gix-config/tests/config/file/init/comfort.rs index a7a5f4cd8f6..1542575cb79 100644 --- a/gix-config/tests/config/file/init/comfort.rs +++ b/gix-config/tests/config/file/init/comfort.rs @@ -1,11 +1,11 @@ -use crate::Result; +use crate::TestResult; use gix_config::source; use serial_test::serial; #[test] #[serial] -fn from_globals() -> Result { +fn from_globals() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let worktree_dir = crate::scripted_fixture_read_only("make_config_repo.sh")?.canonicalize()?; let _environment = _environment.set( @@ -23,7 +23,7 @@ fn from_globals() -> Result { #[test] #[serial] -fn from_environment_overrides() -> Result { +fn from_environment_overrides() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "0"); let config = gix_config::File::from_environment_overrides()?; assert!(config.is_void()); @@ -32,7 +32,7 @@ fn from_environment_overrides() -> Result { #[test] #[serial] -fn from_git_dir() -> Result { +fn from_git_dir() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let worktree_dir = crate::scripted_fixture_read_only("make_config_repo.sh")?; let git_dir = worktree_dir.join(".git"); @@ -97,7 +97,7 @@ fn from_git_dir() -> Result { #[test] #[serial] -fn from_git_dir_with_worktree_extension() -> Result { +fn from_git_dir_with_worktree_extension() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let git_dir = crate::scripted_fixture_read_only("config_with_worktree_extension.sh")? .join("main-worktree") diff --git a/gix-config/tests/config/file/init/from_env.rs b/gix-config/tests/config/file/init/from_env.rs index cc718b46b8f..38ef5a40078 100644 --- a/gix-config/tests/config/file/init/from_env.rs +++ b/gix-config/tests/config/file/init/from_env.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::fs; use gix_config::{ @@ -12,7 +12,7 @@ use crate::file::init::from_paths::escape_backslashes; #[test] #[serial] -fn empty_without_relevant_environment() -> Result { +fn empty_without_relevant_environment() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.unset("GIT_CONFIG_COUNT"); let config = File::from_env(Default::default())?; assert!(config.is_none()); @@ -21,7 +21,7 @@ fn empty_without_relevant_environment() -> Result { #[test] #[serial] -fn empty_with_zero_count() -> Result { +fn empty_with_zero_count() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "0"); let config = File::from_env(Default::default())?; assert!(config.is_none()); @@ -30,7 +30,7 @@ fn empty_with_zero_count() -> Result { #[test] #[serial] -fn parse_error_with_invalid_count() -> Result { +fn parse_error_with_invalid_count() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_CONFIG_COUNT", "invalid"); let err = File::from_env(Default::default()).expect_err("the configuration count is not an integer"); assert!(err.is_validation(), "invalid counts are validation errors"); @@ -45,7 +45,7 @@ fn parse_error_with_invalid_count() -> Result { #[test] #[serial] -fn single_key_value_pair() -> Result { +fn single_key_value_pair() -> TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "1") .set("GIT_CONFIG_KEY_0", "core.key") @@ -64,7 +64,7 @@ fn single_key_value_pair() -> Result { #[test] #[serial] -fn multiple_key_value_pairs() -> Result { +fn multiple_key_value_pairs() -> TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "3") .set("GIT_CONFIG_KEY_0", "core.a") @@ -85,7 +85,7 @@ fn multiple_key_value_pairs() -> Result { #[test] #[serial] -fn error_on_relative_paths_in_include_paths() -> Result { +fn error_on_relative_paths_in_include_paths() -> TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_CONFIG_COUNT", "1") .set("GIT_CONFIG_KEY_0", "include.path") @@ -114,7 +114,7 @@ fn error_on_relative_paths_in_include_paths() -> Result { #[test] #[serial] -fn follow_include_paths() -> Result { +fn follow_include_paths() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let dir = tempdir().unwrap(); let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs index 7cf56ae812f..8e6e31970ac 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/gitdir/mod.rs @@ -1,6 +1,6 @@ mod util; -use crate::Result; +use crate::TestResult; use gix_testtools::Env; use serial_test::serial; use util::{Condition, GitEnv, assert_section_value}; @@ -8,31 +8,31 @@ use util::{Condition, GitEnv, assert_section_value}; use crate::file::init::from_paths::escape_backslashes; #[test] -fn relative_path_with_trailing_slash_matches_like_star_star() -> Result { - assert_section_value(Condition::new("gitdir:worktree/"), GitEnv::repo_name("worktree")?) +fn relative_path_with_trailing_slash_matches_like_star_star() -> TestResult { + assert_section_value(Condition::new("gitdir:worktree/"), GitEnv::repo_name("worktree")?).map_err(Into::into) } #[test] -fn relative_path_without_trailing_slash_does_not_match() -> Result { - assert_section_value( +fn relative_path_without_trailing_slash_does_not_match() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:worktree").expect_original_value(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn relative_path_without_trailing_slash_and_dot_git_suffix_matches() -> Result { - assert_section_value(Condition::new("gitdir:worktree/.git"), GitEnv::repo_name("worktree")?) +fn relative_path_without_trailing_slash_and_dot_git_suffix_matches() -> TestResult { + assert_section_value(Condition::new("gitdir:worktree/.git"), GitEnv::repo_name("worktree")?).map_err(Into::into) } #[test] -fn tilde_slash_expands_the_current_user_home() -> Result { +fn tilde_slash_expands_the_current_user_home() -> TestResult { let env = GitEnv::repo_name(std::path::Path::new("subdir").join("worktree"))?; - assert_section_value(Condition::new("gitdir:~/subdir/worktree/"), env) + assert_section_value(Condition::new("gitdir:~/subdir/worktree/"), env).map_err(Into::into) } #[test] -fn failed_user_expansion_matches_the_literal_pattern() -> Result { +fn failed_user_expansion_matches_the_literal_pattern() -> TestResult { let temp = gix_testtools::tempfile::tempdir()?; let name = format!( "~gix-config-{}", @@ -87,63 +87,63 @@ path = included.config } #[test] -fn tilde_alone_does_not_match_even_if_home_is_git_directory() -> Result { +fn tilde_alone_does_not_match_even_if_home_is_git_directory() -> TestResult { let env = GitEnv::repo_in_home()?; - assert_section_value(Condition::new("gitdir:~").expect_original_value(), env) + assert_section_value(Condition::new("gitdir:~").expect_original_value(), env).map_err(Into::into) } #[test] -fn explicit_star_star_prefix_and_suffix_match_zero_or_more_path_components() -> Result { - assert_section_value(Condition::new("gitdir:**/worktree/**"), GitEnv::repo_name("worktree")?) +fn explicit_star_star_prefix_and_suffix_match_zero_or_more_path_components() -> TestResult { + assert_section_value(Condition::new("gitdir:**/worktree/**"), GitEnv::repo_name("worktree")?).map_err(Into::into) } #[test] -fn double_slash_does_not_match() -> Result { - assert_section_value( +fn double_slash_does_not_match() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir://worktree").expect_original_value(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn absolute_git_dir_with_os_separators_match() -> Result { - assert_section_value( +fn absolute_git_dir_with_os_separators_match() -> TestResult { + Ok(assert_section_value( original_value_on_windows(Condition::new("gitdir:$gitdir")), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn absolute_worktree_dir_with_os_separators_does_not_match_if_trailing_slash_is_missing() -> Result { - assert_section_value( +fn absolute_worktree_dir_with_os_separators_does_not_match_if_trailing_slash_is_missing() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:$worktree").expect_original_value(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn absolute_worktree_dir_with_os_separators_matches_with_trailing_glob() -> Result { - assert_section_value( +fn absolute_worktree_dir_with_os_separators_matches_with_trailing_glob() -> TestResult { + Ok(assert_section_value( original_value_on_windows(Condition::new(format!( "gitdir:$worktree{}**", std::path::MAIN_SEPARATOR ))), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn dot_slash_path_is_replaced_with_directory_containing_the_including_config_file() -> Result { - assert_section_value( +fn dot_slash_path_is_replaced_with_directory_containing_the_including_config_file() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:./").set_user_config_instead_of_repo_config(), GitEnv::repo_name("worktree")?, // the user configuration is in $HOME, which is parent to $HOME/worktree, and the pattern path ends up being $HOME/**, including worktree/.git - ) + )?) } #[test] #[serial] -fn dot_slash_from_environment_causes_error() -> Result { +fn dot_slash_from_environment_causes_error() -> TestResult { let _isolated_environment = gix_testtools::isolate_git_environment()?; let env = GitEnv::repo_name("worktree")?; // Only slashes can be used as matches, even on Windows. @@ -204,101 +204,101 @@ fn dot_slash_from_environment_causes_error() -> Result { } #[test] -fn dot_dot_slash_prefixes_are_not_special_and_are_not_what_you_want() -> Result { - assert_section_value( +fn dot_dot_slash_prefixes_are_not_special_and_are_not_what_you_want() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:../") .set_user_config_instead_of_repo_config() .expect_no_value(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn leading_dots_are_not_special() -> Result { - assert_section_value(Condition::new("gitdir:.hidden/"), GitEnv::repo_name(".hidden")?) +fn leading_dots_are_not_special() -> TestResult { + assert_section_value(Condition::new("gitdir:.hidden/"), GitEnv::repo_name(".hidden")?).map_err(Into::into) } #[test] -fn dot_slash_path_with_dot_git_suffix_matches() -> Result { - assert_section_value( +fn dot_slash_path_with_dot_git_suffix_matches() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:./worktree/.git").set_user_config_instead_of_repo_config(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn globbing_and_wildcards() -> Result { - assert_section_value( +fn globbing_and_wildcards() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:stan?ard/glo*ng/[xwz]ildcards/.git").set_user_config_instead_of_repo_config(), GitEnv::repo_name("standard/globbing/wildcards")?, - ) + )?) } #[test] -fn case_insensitive_matches_any_case() -> Result { +fn case_insensitive_matches_any_case() -> TestResult { assert_section_value(Condition::new("gitdir/i:WORKTREE/"), GitEnv::repo_name("worktree")?)?; - assert_section_value( + Ok(assert_section_value( Condition::new("gitdir:WORKTREE/").expect_original_value(), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn pattern_with_escaped_backslash() -> Result { - assert_section_value( +fn pattern_with_escaped_backslash() -> TestResult { + Ok(assert_section_value( original_value_on_windows(Condition::new(r"gitdir:\\work\\tree\\/")), GitEnv::repo_name("worktree")?, - ) + )?) } #[test] -fn pattern_with_backslash() -> Result { - assert_section_value(Condition::new(r"gitdir:work\tree/"), GitEnv::repo_name("worktree")?) +fn pattern_with_backslash() -> TestResult { + assert_section_value(Condition::new(r"gitdir:work\tree/"), GitEnv::repo_name("worktree")?).map_err(Into::into) } #[test] -fn star_star_in_the_middle() -> Result { - assert_section_value( +fn star_star_in_the_middle() -> TestResult { + Ok(assert_section_value( Condition::new("gitdir:**/dir/**/worktree/**"), GitEnv::repo_name("dir/worktree")?, - ) + )?) } #[test] #[cfg(not(windows))] -fn tilde_expansion_with_symlink() -> Result { +fn tilde_expansion_with_symlink() -> TestResult { let env = util::git_env_with_symlinked_repo()?; - assert_section_value(Condition::new("gitdir:~/worktree/"), env) + assert_section_value(Condition::new("gitdir:~/worktree/"), env).map_err(Into::into) } #[test] #[cfg(not(windows))] -fn dot_path_with_symlink() -> Result { +fn dot_path_with_symlink() -> TestResult { let env = util::git_env_with_symlinked_repo()?; - assert_section_value( + Ok(assert_section_value( Condition::new("gitdir:./symlink-worktree/.git").set_user_config_instead_of_repo_config(), env, - ) + )?) } #[test] #[cfg(not(windows))] -fn relative_path_matching_symlink() -> Result { +fn relative_path_matching_symlink() -> TestResult { let env = util::git_env_with_symlinked_repo()?; - assert_section_value( + Ok(assert_section_value( Condition::new("gitdir:symlink-worktree/").set_user_config_instead_of_repo_config(), env, - ) + )?) } #[test] #[cfg(not(windows))] -fn dot_path_matching_symlink_with_icase() -> Result { +fn dot_path_matching_symlink_with_icase() -> TestResult { let env = util::git_env_with_symlinked_repo()?; - assert_section_value( + Ok(assert_section_value( Condition::new("gitdir/i:SYMLINK-WORKTREE/").set_user_config_instead_of_repo_config(), env, - ) + )?) } fn original_value_on_windows(c: Condition) -> Condition { diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs index 5a977ac9698..eed0dbd4ee4 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/hasconfig.rs @@ -1,10 +1,10 @@ -use crate::Result; +use crate::{Result, TestResult}; use std::path::{Path, PathBuf}; use gix_config::file::{includes, init}; #[test] -fn simple() -> Result { +fn simple() -> TestResult { let (config, root) = config_with_includes("basic")?; compare_baseline(&config, "user.this", root.join("expected")); assert_eq!(config.string("user.that"), None); @@ -12,7 +12,7 @@ fn simple() -> Result { } #[test] -fn inclusion_order() -> Result { +fn inclusion_order() -> TestResult { let (config, root) = config_with_includes("inclusion-order")?; for key in ["one", "two", "three"] { compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}"))); @@ -21,7 +21,7 @@ fn inclusion_order() -> Result { } #[test] -fn globs() -> Result { +fn globs() -> TestResult { let (config, root) = config_with_includes("globs")?; for key in ["dss", "dse", "dsm", "ssm"] { compare_baseline(&config, format!("user.{key}"), root.join(format!("expected.{key}"))); @@ -31,7 +31,7 @@ fn globs() -> Result { } #[test] -fn cycle_breaker() -> Result { +fn cycle_breaker() -> TestResult { for name in ["cycle-breaker-direct", "cycle-breaker-indirect"] { let (_config, _root) = config_with_includes(name)?; } @@ -40,7 +40,7 @@ fn cycle_breaker() -> Result { } #[test] -fn no_cycle() -> Result { +fn no_cycle() -> TestResult { let (config, root) = config_with_includes("no-cycle")?; compare_baseline(&config, "user.name", root.join("expected")); Ok(()) diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs index 00c741f23d5..a3e82bc64b7 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::{Result, TestResult}; use std::{fs, path::Path, str::FromStr}; use gix_config::{ @@ -15,7 +15,7 @@ mod hasconfig; mod onbranch; #[test] -fn include_and_includeif_correct_inclusion_order_and_delayed_resolve_include() -> Result { +fn include_and_includeif_correct_inclusion_order_and_delayed_resolve_include() -> TestResult { let dir = tempdir()?; let config_path = dir.path().join("root"); let first_include_path = dir.path().join("first-incl"); diff --git a/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs b/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs index 3bd48eb9479..8884bf3a68e 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/conditional/onbranch.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::{Result, TestResult}; use std::fs; use bstr::{BString, ByteSlice}; @@ -13,7 +13,7 @@ use gix_testtools::tempfile::tempdir; use crate::file::{bstring, init::from_paths::includes::conditional::git_init}; #[test] -fn literal_branch_names_match() -> Result { +fn literal_branch_names_match() -> TestResult { assert_section_value( Options { condition: "literal-match", @@ -26,7 +26,7 @@ fn literal_branch_names_match() -> Result { } #[test] -fn full_ref_names_do_not_match() -> Result { +fn full_ref_names_do_not_match() -> TestResult { assert_section_value( Options { condition: "refs/heads/simple", @@ -39,7 +39,7 @@ fn full_ref_names_do_not_match() -> Result { } #[test] -fn non_branches_never_match() -> Result { +fn non_branches_never_match() -> TestResult { assert_section_value( Options { condition: "good", @@ -52,7 +52,7 @@ fn non_branches_never_match() -> Result { } #[test] -fn patterns_ending_with_slash_match_subdirectories_recursively() -> Result { +fn patterns_ending_with_slash_match_subdirectories_recursively() -> TestResult { let mut env = GitEnv::new()?; assert_section_value( Options { @@ -83,7 +83,7 @@ fn patterns_ending_with_slash_match_subdirectories_recursively() -> Result { } #[test] -fn simple_glob_patterns() -> Result { +fn simple_glob_patterns() -> TestResult { let mut env = GitEnv::new()?; assert_section_value( Options { @@ -131,7 +131,7 @@ fn simple_glob_patterns() -> Result { } #[test] -fn simple_globs_do_not_cross_component_boundary() -> Result { +fn simple_globs_do_not_cross_component_boundary() -> TestResult { let mut env = GitEnv::new()?; assert_section_value( Options { @@ -154,7 +154,7 @@ fn simple_globs_do_not_cross_component_boundary() -> Result { } #[test] -fn double_star_globs_cross_component_boundaries() -> Result { +fn double_star_globs_cross_component_boundaries() -> TestResult { assert_section_value( Options { condition: "feature/**/start", diff --git a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs index 6a1c6f71731..d80c71c50be 100644 --- a/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs +++ b/gix-config/tests/config/file/init/from_paths/includes/unconditional.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::fs; use gix_config::{ @@ -28,7 +28,7 @@ fn assert_include_depth(err: gix_error::Error) -> gix_error::Error { } #[test] -fn multiple() -> Result { +fn multiple() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -95,7 +95,7 @@ fn multiple() -> Result { } #[test] -fn respect_max_depth() -> Result { +fn respect_max_depth() -> TestResult { let dir = tempdir()?; // 0 includes 1 - base level @@ -194,7 +194,7 @@ fn respect_max_depth() -> Result { } #[test] -fn simple() -> Result { +fn simple() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -230,7 +230,7 @@ fn simple() -> Result { } #[test] -fn cycle_detection() -> Result { +fn cycle_detection() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -291,7 +291,7 @@ fn cycle_detection() -> Result { } #[test] -fn nested() -> Result { +fn nested() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_paths/mod.rs b/gix-config/tests/config/file/init/from_paths/mod.rs index a6be638197f..eb8c6489a3c 100644 --- a/gix-config/tests/config/file/init/from_paths/mod.rs +++ b/gix-config/tests/config/file/init/from_paths/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::{fs, path::PathBuf}; use gix_config::{File, Source}; @@ -55,7 +55,7 @@ mod from_path_no_includes { } #[test] -fn multiple_paths_single_value() -> Result { +fn multiple_paths_single_value() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -83,7 +83,7 @@ fn multiple_paths_single_value() -> Result { } #[test] -fn frontmatter_is_maintained_in_multiple_files() -> Result { +fn frontmatter_is_maintained_in_multiple_files() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); @@ -144,7 +144,7 @@ fn frontmatter_is_maintained_in_multiple_files() -> Result { } #[test] -fn multiple_paths_multi_value_and_filter() -> Result { +fn multiple_paths_multi_value_and_filter() -> TestResult { let dir = tempdir()?; let a_path = dir.path().join("a"); diff --git a/gix-config/tests/config/file/init/from_str.rs b/gix-config/tests/config/file/init/from_str.rs index 6ceb2464e22..46481e0f55e 100644 --- a/gix-config/tests/config/file/init/from_str.rs +++ b/gix-config/tests/config/file/init/from_str.rs @@ -1,7 +1,7 @@ -use crate::Result; +use crate::TestResult; #[test] -fn empty_yields_default_file() -> Result { +fn empty_yields_default_file() -> TestResult { let a: gix_config::File = "".parse()?; assert_eq!(a, gix_config::File::default()); assert_eq!(a.to_string(), ""); @@ -9,7 +9,7 @@ fn empty_yields_default_file() -> Result { } #[test] -fn whitespace_without_section_contains_front_matter() -> Result { +fn whitespace_without_section_contains_front_matter() -> TestResult { let input = " \t"; let a: gix_config::File = input.parse()?; assert_eq!(a.to_string(), input); diff --git a/gix-config/tests/config/file/mod.rs b/gix-config/tests/config/file/mod.rs index 1c017039269..798c6346b8f 100644 --- a/gix-config/tests/config/file/mod.rs +++ b/gix-config/tests/config/file/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::path::PathBuf; use bstr::BString; @@ -74,7 +74,7 @@ fn fuzzed_stackoverflow() { } #[test] -fn fuzzed_long_runtime() -> Result { +fn fuzzed_long_runtime() -> TestResult { let config = std::fs::read(fixture_path("fuzzed/long-parsetime.config"))?; let file = File::from_bytes_no_includes(&config, gix_config::file::Metadata::default(), Default::default())?; assert_eq!(file.sections().count(), 52); diff --git a/gix-config/tests/config/file/mutable/multi_value.rs b/gix-config/tests/config/file/mutable/multi_value.rs index 1f141a8783a..16d14b339ab 100644 --- a/gix-config/tests/config/file/mutable/multi_value.rs +++ b/gix-config/tests/config/file/mutable/multi_value.rs @@ -1,9 +1,9 @@ mod get { - use crate::Result; + use crate::TestResult; use crate::file::{bstring, mutable::multi_value::init_config}; #[test] - fn single_lines() -> Result { + fn single_lines() -> TestResult { let mut config = init_config(); let value = config.raw_values_mut_by("core", None, "a")?; @@ -12,7 +12,7 @@ mod get { } #[test] - fn multi_line() -> Result { + fn multi_line() -> TestResult { let mut config: gix_config::File = r#"[core] a=b\ "100" @@ -37,7 +37,7 @@ c } #[test] - fn value_names_are_case_insensitive() -> Result { + fn value_names_are_case_insensitive() -> TestResult { let mut config: gix_config::File = "[core]\nMixedCase = one\nMIXEDCASE = two".parse()?; assert_eq!( config.raw_values_mut_by("core", None, "mixedcase")?.get()?, @@ -48,11 +48,11 @@ c } mod access { - use crate::Result; + use crate::TestResult; use crate::file::mutable::multi_value::init_config; #[test] - fn non_empty_sizes() -> Result { + fn non_empty_sizes() -> TestResult { let mut config = init_config(); assert_eq!(config.raw_values_mut_by("core", None, "a")?.len(), 3); assert!(!config.raw_values_mut_by("core", None, "a")?.is_empty()); @@ -61,11 +61,11 @@ mod access { } mod set { - use crate::Result; + use crate::TestResult; use crate::file::{bstring, mutable::multi_value::init_config}; #[test] - fn values_are_escaped() -> Result { + fn values_are_escaped() -> TestResult { for value in ["a b", " a b", "a b\t", ";c", "#c", "a\nb\n\tc"] { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; @@ -83,7 +83,7 @@ mod set { } #[test] - fn single_at_start() -> Result { + fn single_at_start() -> TestResult { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_string_at(0, "Hello")?; @@ -95,7 +95,7 @@ mod set { } #[test] - fn single_at_end() -> Result { + fn single_at_end() -> TestResult { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_string_at(2, "Hello")?; @@ -107,7 +107,7 @@ mod set { } #[test] - fn all() -> Result { + fn all() -> TestResult { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_all("Hello")?; @@ -119,7 +119,7 @@ mod set { } #[test] - fn all_empty() -> Result { + fn all_empty() -> TestResult { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.set_all("")?; @@ -132,11 +132,11 @@ mod set { } mod delete { - use crate::Result; + use crate::TestResult; use crate::file::mutable::multi_value::init_config; #[test] - fn single_at_start_and_end() -> Result { + fn single_at_start_and_end() -> TestResult { let mut config = init_config(); { let mut values = config.raw_values_mut_by("core", None, "a")?; @@ -154,7 +154,7 @@ mod delete { } #[test] - fn all() -> Result { + fn all() -> TestResult { let mut config = init_config(); let mut values = config.raw_values_mut_by("core", None, "a")?; values.delete_all(); diff --git a/gix-config/tests/config/file/mutable/section.rs b/gix-config/tests/config/file/mutable/section.rs index a929f6e569c..4a068af9099 100644 --- a/gix-config/tests/config/file/mutable/section.rs +++ b/gix-config/tests/config/file/mutable/section.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; #[test] fn section_mut_must_exist_as_section_is_not_created_automatically() { @@ -7,7 +7,7 @@ fn section_mut_must_exist_as_section_is_not_created_automatically() { } #[test] -fn section_mut_or_create_new_is_infallible() -> Result { +fn section_mut_or_create_new_is_infallible() -> TestResult { let mut config = multi_value_section(); let section = config.section_mut_or_create_new("name", "subsection")?; assert_eq!(section.header().name(), "name"); @@ -16,7 +16,7 @@ fn section_mut_or_create_new_is_infallible() -> Result { } #[test] -fn section_mut_or_create_new_filter_may_reject_existing_sections() -> Result { +fn section_mut_or_create_new_filter_may_reject_existing_sections() -> TestResult { let mut config = multi_value_section(); let section = config.section_mut_or_create_new_filter("a", None, |_| false)?; assert_eq!(section.header().name(), "a"); @@ -40,11 +40,11 @@ fn section_mut_by_id() { } mod rename { - use crate::Result; + use crate::TestResult; use bstr::ByteSlice; #[test] - fn detached_sections_can_be_renamed() -> Result { + fn detached_sections_can_be_renamed() -> TestResult { let mut section = gix_config::file::Section::new("remote", "origin", gix_config::file::Metadata::default())?; section.to_mut().rename("branch", "main")?; @@ -55,7 +55,7 @@ mod rename { } #[test] - fn attached_sections_are_renamed_unambiguously_and_update_lookups() -> Result { + fn attached_sections_are_renamed_unambiguously_and_update_lookups() -> TestResult { let mut file = gix_config::File::try_from( "[target \"same\"] key = first\n\ [source \"old\"] key = selected\n\ @@ -93,7 +93,7 @@ mod rename { } #[test] - fn invalid_names_leave_attached_sections_unchanged() -> Result { + fn invalid_names_leave_attached_sections_unchanged() -> TestResult { let mut file = gix_config::File::try_from("[core] key = value\n")?; assert!(file.section_mut("core", None)?.rename("not_valid", None).is_err()); assert_eq!( @@ -111,10 +111,10 @@ mod rename { mod remove { use super::multi_value_section; - use crate::Result; + use crate::TestResult; #[test] - fn all() -> Result { + fn all() -> TestResult { let mut config = multi_value_section(); let mut section = config.section_mut("a", None)?; @@ -138,10 +138,10 @@ mod remove { mod pop { use super::multi_value_section; - use crate::Result; + use crate::TestResult; #[test] - fn all() -> Result { + fn all() -> TestResult { let mut config = multi_value_section(); let mut section = config.section_mut_by_key("a")?; @@ -165,10 +165,10 @@ mod pop { mod set { use super::multi_value_section; - use crate::Result; + use crate::TestResult; #[test] - fn various_escapes_onto_various_kinds_of_values() -> Result { + fn various_escapes_onto_various_kinds_of_values() -> TestResult { let mut config = multi_value_section(); let mut section = config.section_mut("a", None)?; let values = vec!["", " a", "b\t", "; comment", "a\n\tc d\\ \"x\""]; @@ -195,10 +195,10 @@ mod set { } mod value_name_validation { - use crate::Result; + use crate::TestResult; #[test] - fn mutations_validate_names_and_leave_the_section_unchanged_on_error() -> Result { + fn mutations_validate_names_and_leave_the_section_unchanged_on_error() -> TestResult { let mut config = gix_config::File::default(); let mut section = config.new_section("core", None)?; @@ -242,7 +242,7 @@ mod value_name_validation { } #[test] - fn names_returned_by_public_apis_are_strings() -> Result { + fn names_returned_by_public_apis_are_strings() -> TestResult { let mut config = super::multi_value_section(); let mut section = config.section_mut("a", None)?; let names: Vec<String> = section.value_names().collect(); @@ -255,11 +255,11 @@ mod value_name_validation { } mod push { - use crate::Result; + use crate::TestResult; use crate::file::bstring; #[test] - fn none_as_value_omits_the_key_value_separator() -> Result { + fn none_as_value_omits_the_key_value_separator() -> TestResult { let mut file = gix_config::File::default(); let mut section = file.section_mut_or_create_new("a", "sub")?; section.push("key", None)?; @@ -275,7 +275,7 @@ mod push { } #[test] - fn whitespace_is_derived_from_whitespace_before_first_value() -> Result { + fn whitespace_is_derived_from_whitespace_before_first_value() -> TestResult { for (input, expected_pre_key, expected_sep) in [ ("[a]\n\t\tb=c", Some("\t\t".into()), (None, None)), ("[a]\nb= c", None, (None, Some(" "))), @@ -308,7 +308,7 @@ mod push { } #[test] - fn values_are_escaped() { + fn values_are_escaped() -> gix_testtools::TestResult { for (value, expected) in [ ("a b", "$head\tk = a b$nl"), (" a b", "$head\tk = \" a b\"$nl"), @@ -318,22 +318,21 @@ mod push { ("a\nb\n\tc", "$head\tk = a\\nb\\n\\tc$nl"), ] { let mut config = gix_config::File::default(); - let mut section = config.new_section("a", None).unwrap(); + let mut section = config.new_section("a", None)?; section.set_implicit_newline(false); - section - .push("k", Some(value.into())) - .expect("the fixture fits into the backing buffer"); + section.push("k", Some(value.into()))?; let expected = expected .replace("$head", &format!("[a]{nl}", nl = section.newline())) .replace("$nl", §ion.newline().to_string()); assert_eq!(config.to_bstring(), expected); } + Ok(()) } } mod push_with_comment { #[test] - fn various_comments_and_escaping() { + fn various_comments_and_escaping() -> gix_testtools::TestResult { for (comment, expected) in [ ("", "$head\tk = v #$nl"), ("this is v!", "$head\tk = v # this is v!$nl"), @@ -349,24 +348,23 @@ mod push_with_comment { ), ] { let mut config = gix_config::File::default(); - let mut section = config.new_section("a", None).unwrap(); + let mut section = config.new_section("a", None)?; section.set_implicit_newline(false); - section - .push_with_comment("k", Some("v".into()), comment) - .expect("the fixture fits into the backing buffer"); + section.push_with_comment("k", Some("v".into()), comment)?; let expected = expected .replace("$head", &format!("[a]{nl}", nl = section.newline())) .replace("$nl", §ion.newline().to_string()); assert_eq!(config.to_bstring(), expected); } + Ok(()) } } mod set_leading_whitespace { - use crate::Result; + use crate::TestResult; #[test] - fn any_whitespace_is_ok() -> Result { + fn any_whitespace_is_ok() -> TestResult { let mut config = gix_config::File::default(); let mut section = config.new_section("core", None)?; diff --git a/gix-config/tests/config/file/mutable/value.rs b/gix-config/tests/config/file/mutable/value.rs index ce175671924..c8a0d4d2dc8 100644 --- a/gix-config/tests/config/file/mutable/value.rs +++ b/gix-config/tests/config/file/mutable/value.rs @@ -1,5 +1,5 @@ mod get { - use crate::Result; + use crate::TestResult; use bstr::BString; use crate::file::mutable::value::init_config; @@ -30,7 +30,7 @@ mod get { } #[test] - fn value_is_correct() -> Result { + fn value_is_correct() -> TestResult { let mut config = init_config(); let value = config.raw_value_mut_by("core", None, "a")?; @@ -39,7 +39,7 @@ mod get { } #[test] - fn value_names_are_case_insensitive() -> Result { + fn value_names_are_case_insensitive() -> TestResult { let mut config: gix_config::File = "[core]\nMixedCase = value".parse()?; assert_eq!(config.raw_value_mut_by("core", None, "mIxEdCaSe")?.get()?, "value"); Ok(()) @@ -47,7 +47,7 @@ mod get { } mod set_string { - use crate::Result; + use crate::TestResult; use crate::file::mutable::value::init_config; fn assert_set_string(expected: &str) { @@ -127,7 +127,7 @@ mod set_string { } #[test] - fn unquoted_comments_end_continued_values_and_survive_replacement() -> Result { + fn unquoted_comments_end_continued_values_and_survive_replacement() -> TestResult { for newline in ["\n", "\r\n"] { for comment in ["# comment", "; comment"] { let mut config: gix_config::File = @@ -146,7 +146,7 @@ mod set_string { } #[test] - fn quoted_comment_markers_in_continued_values_are_value_content() -> Result { + fn quoted_comment_markers_in_continued_values_are_value_content() -> TestResult { let mut config: gix_config::File = r#"[a] k="one\ #not;comments" @@ -168,7 +168,7 @@ next=value"# } #[test] - fn simple_value_and_empty_string() -> Result { + fn simple_value_and_empty_string() -> TestResult { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -200,10 +200,10 @@ next=value"# mod delete { use super::init_config; - use crate::Result; + use crate::TestResult; #[test] - fn single_line_value() -> Result { + fn single_line_value() -> TestResult { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -223,7 +223,7 @@ mod delete { } #[test] - fn get_value_after_deleted() -> Result { + fn get_value_after_deleted() -> TestResult { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -233,7 +233,7 @@ mod delete { } #[test] - fn set_string_after_deleted() -> Result { + fn set_string_after_deleted() -> TestResult { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -252,7 +252,7 @@ mod delete { } #[test] - fn idempotency() -> Result { + fn idempotency() -> TestResult { let mut config = init_config(); let mut value = config.raw_value_mut_by("core", None, "a")?; @@ -267,7 +267,7 @@ mod delete { } #[test] - fn multi_line_value() -> Result { + fn multi_line_value() -> TestResult { let mut config: gix_config::File = r#"[core] a=b"100"\ c\ diff --git a/gix-config/tests/config/file/resolve_includes.rs b/gix-config/tests/config/file/resolve_includes.rs index a91ec08a5c2..f6a990501cf 100644 --- a/gix-config/tests/config/file/resolve_includes.rs +++ b/gix-config/tests/config/file/resolve_includes.rs @@ -1,8 +1,8 @@ -use crate::Result; +use crate::TestResult; use gix_config::{file, file::init}; #[test] -fn missing_includes_are_ignored_by_default() -> Result { +fn missing_includes_are_ignored_by_default() -> TestResult { let input = r#" [include] path = /etc/absolute/missing.config diff --git a/gix-config/tests/config/file/write.rs b/gix-config/tests/config/file/write.rs index 5355c47257b..52bac857977 100644 --- a/gix-config/tests/config/file/write.rs +++ b/gix-config/tests/config/file/write.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteVec; use gix_config::file::{Metadata, init}; @@ -17,7 +17,7 @@ fn empty_sections_roundtrip() { } #[test] -fn empty_sections_with_comments_roundtrip() { +fn empty_sections_with_comments_roundtrip() -> gix_testtools::TestResult { let input = r#"; pre-a [a] # side a ; post a @@ -27,14 +27,11 @@ fn empty_sections_with_comments_roundtrip() { [d] # side d "#; - let mut config = gix_config::File::try_from(input).unwrap(); + let mut config = gix_config::File::try_from(input)?; let mut single_string = config.to_bstring(); assert_eq!(single_string, input); assert_eq!( - config - .append(config.clone()) - .expect("the small fixture fits into the backing buffer") - .to_string(), + config.append(config.clone())?.to_string(), { let clone = single_string.clone(); single_string.push_str(&clone); @@ -42,16 +39,15 @@ fn empty_sections_with_comments_roundtrip() { }, "string-duplication is the same as data structure duplication" ); + Ok(()) } #[test] -fn decoded_subsection_names_keep_their_raw_spelling() { +fn decoded_subsection_names_keep_their_raw_spelling() -> gix_testtools::TestResult { let input = r#"[remote "single \t \0"] "#; - let config = gix_config::File::try_from(input).expect("valid config"); - let section = config - .section("remote", Some("single t 0".into())) - .expect("the decoded subsection name is used for lookup"); + let config = gix_config::File::try_from(input)?; + let section = config.section("remote", Some("single t 0".into()))?; assert_eq!( section.header().subsection_name(), @@ -63,6 +59,7 @@ fn decoded_subsection_names_keep_their_raw_spelling() { input, "serialization uses the original raw subsection spelling" ); + Ok(()) } #[test] @@ -78,7 +75,7 @@ fn inserted_newlines_use_each_sections_newline_style() { } #[test] -fn crlf_after_a_comment_is_detected_and_used_for_insertions() -> Result { +fn crlf_after_a_comment_is_detected_and_used_for_insertions() -> TestResult { let input = "; root\r\n[core]\nkey=value\n"; let mut config = gix_config::File::try_from(input)?; assert_eq!( @@ -164,12 +161,12 @@ fn complex_lossless_roundtrip() { } mod to_filter { - use crate::Result; + use crate::TestResult; use bstr::ByteSlice; use gix_config::file::Metadata; #[test] - fn allows_only_selected_sections() -> Result { + fn allows_only_selected_sections() -> TestResult { let mut config = gix_config::File::new(Metadata::api()); config.set_raw_value_by("a", None, "b", "c")?; diff --git a/gix-config/tests/config/main.rs b/gix-config/tests/config/main.rs index dafdcecf942..fa1586a6cb9 100644 --- a/gix-config/tests/config/main.rs +++ b/gix-config/tests/config/main.rs @@ -1,4 +1,4 @@ -pub use gix_testtools::{Result, scripted_fixture_read_only}; +pub use gix_testtools::{Result, TestResult, scripted_fixture_read_only}; mod file; mod format; diff --git a/gix-config/tests/config/parse/from_bytes.rs b/gix-config/tests/config/parse/from_bytes.rs index 1afbfc01d08..985a75fb7be 100644 --- a/gix-config/tests/config/parse/from_bytes.rs +++ b/gix-config/tests/config/parse/from_bytes.rs @@ -16,12 +16,11 @@ fn fuzz() { } #[test] -fn filters_receive_event_refs_for_content_access() { +fn filters_receive_event_refs_for_content_access() -> gix_testtools::TestResult { fn reject_drop_values(event: EventRef<'_>) -> bool { !matches!(event, EventRef::Value(value) if value == b"drop".as_slice()) } - let events = Events::from_bytes(b"[core]\nkeep = keep\ndrop = drop\n", Some(reject_drop_values)) - .expect("content-based filters can inspect event bytes through views"); + let events = Events::from_bytes(b"[core]\nkeep = keep\ndrop = drop\n", Some(reject_drop_values))?; assert!( !events @@ -29,6 +28,7 @@ fn filters_receive_event_refs_for_content_access() { .any(|event| matches!(event, EventRef::Value(value) if value == b"drop".as_slice())), "the filter can reject events by inspecting their value bytes" ); + Ok(()) } #[test] diff --git a/gix-config/tests/config/parse/section.rs b/gix-config/tests/config/parse/section.rs index 805281fa8a4..b96036942c3 100644 --- a/gix-config/tests/config/parse/section.rs +++ b/gix-config/tests/config/parse/section.rs @@ -19,18 +19,18 @@ mod header { } mod write_to { - use crate::Result; + use crate::TestResult; use crate::parse::section::header::serialized; #[test] - fn subsection_backslashes_and_quotes_are_escaped() -> Result { + fn subsection_backslashes_and_quotes_are_escaped() -> TestResult { assert_eq!(serialized("core", r"a\b")?, r#"[core "a\\b"]"#); assert_eq!(serialized("core", r#"a:"b""#)?, r#"[core "a:\"b\""]"#); Ok(()) } #[test] - fn everything_is_allowed() -> Result { + fn everything_is_allowed() -> TestResult { assert_eq!(serialized("core", "a/b \t\t a\\b")?, "[core \"a/b \t\t a\\\\b\"]"); Ok(()) } diff --git a/gix-credentials/src/helper/cascade.rs b/gix-credentials/src/helper/cascade.rs index ef9d3af400f..ae68187914f 100644 --- a/gix-credentials/src/helper/cascade.rs +++ b/gix-credentials/src/helper/cascade.rs @@ -40,7 +40,11 @@ impl Cascade { } else { None } - .map(|name| vec![Program::from_custom_definition(name)]) + .map(|name| { + vec![Program::from_kind(crate::program::Kind::ExternalName { + name_and_args: name.into(), + })] + }) .unwrap_or_default() } } diff --git a/gix-credentials/src/program/mod.rs b/gix-credentials/src/program/mod.rs index 658b817a453..24795baa5b4 100644 --- a/gix-credentials/src/program/mod.rs +++ b/gix-credentials/src/program/mod.rs @@ -4,6 +4,7 @@ use std::{ }; use bstr::{BString, ByteSlice, ByteVec}; +use gix_error::Result; use crate::{Program, helper}; @@ -12,15 +13,15 @@ fn external_name_command( git_program: &Path, name_and_args: &bstr::BStr, action: &helper::Action, -) -> std::process::Command { - let git_program = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_program)); +) -> Result<std::process::Command> { + let git_program = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(git_program)?); let mut args = gix_quote::single(git_program.as_ref()); args.push_str(" credential-"); args.push_str(name_and_args); - gix_command::prepare(gix_path::from_bstr(args.as_bstr()).into_owned()) + gix_command::prepare(gix_path::from_bstr(args.as_bstr())?.into_owned()) .arg(action.as_arg(true)) .command_may_be_shell_script_allow_manual_argument_splitting() - .into() + .try_into() } /// The kind of helper program to use. @@ -58,8 +59,10 @@ impl Program { /// Parse the given input as per the custom helper definition, supporting `!<script>`, `name` and `/absolute/name`, the latter two /// also support arguments which are ignored here. - pub fn from_custom_definition(input: impl Into<BString>) -> Self { - fn from_custom_definition_inner(mut input: BString) -> Program { + /// + /// Returns an error if the helper name or path cannot be represented by the platform. + pub fn from_custom_definition(input: impl Into<BString>) -> Result<Self> { + fn from_custom_definition_inner(mut input: BString) -> Result<Program> { let kind = if input.starts_with(b"!") { input.remove(0); Kind::ExternalShellScript(input) @@ -69,39 +72,43 @@ impl Program { .find_byte(b' ') .map_or(input.as_slice(), |pos| &input[..pos]) .as_bstr(), - ); + )?; if gix_path::is_absolute(path) { Kind::ExternalPath { path_and_args: input } } else { Kind::ExternalName { name_and_args: input } } }; - Program { + Ok(Program { kind, child: None, stderr: true, - } + }) } from_custom_definition_inner(input.into()) } /// Convert the program into the respective command, suitable to invoke `action`. - pub fn to_command(&self, action: &helper::Action) -> std::process::Command { + /// + /// Returns an error if helper or executable paths cannot be represented in the required encoding. + pub fn to_command(&self, action: &helper::Action) -> Result<std::process::Command> { let git_program = gix_path::env::exe_invocation(); let mut cmd = match &self.kind { Kind::Builtin => { - let mut cmd = Command::from(gix_command::prepare(git_program)); + let mut cmd = Command::try_from(gix_command::prepare(git_program))?; cmd.arg("credential").arg(action.as_arg(false)); cmd } - Kind::ExternalName { name_and_args } => external_name_command(git_program, name_and_args.as_bstr(), action), + Kind::ExternalName { name_and_args } => { + external_name_command(git_program, name_and_args.as_bstr(), action)? + } Kind::ExternalShellScript(for_shell) | Kind::ExternalPath { path_and_args: for_shell, - } => gix_command::prepare(gix_path::from_bstr(for_shell.as_bstr()).as_ref()) + } => gix_command::prepare(gix_path::from_bstr(for_shell.as_bstr())?.as_ref()) .command_may_be_shell_script() .arg(action.as_arg(true)) - .into(), + .try_into()?, }; cmd.stdin(Stdio::piped()) .stdout(if action.expects_output() { @@ -110,7 +117,7 @@ impl Program { Stdio::null() }) .stderr(if self.stderr { Stdio::inherit() } else { Stdio::null() }); - cmd + Ok(cmd) } } @@ -129,7 +136,9 @@ impl Program { action: &helper::Action, ) -> std::io::Result<(std::process::ChildStdin, Option<std::process::ChildStdout>)> { assert!(self.child.is_none(), "BUG: must not call `start()` twice"); - let mut cmd = self.to_command(action); + let mut cmd = self + .to_command(action) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; gix_trace::debug!(cmd = ?cmd, "launching credential helper"); let mut child = cmd.spawn()?; let stdin = child.stdin.take().expect("stdin to be configured"); @@ -162,12 +171,12 @@ mod tests { use crate::helper; #[test] - fn git_program_with_spaces_is_quoted_in_external_name_shell_scripts() { + fn git_program_with_spaces_is_quoted_in_external_name_shell_scripts() -> gix_testtools::TestResult { let cmd = super::external_name_command( Path::new(r"C:\Program Files\Git\mingw64\bin\git.exe"), "manager --config=~/credentials".into(), &helper::Action::Get(Default::default()), - ); + )?; let script = cmd .get_args() .skip_while(|arg| *arg != OsStr::new("-c")) @@ -183,5 +192,6 @@ mod tests { }), "the Git executable is a single shell token even when its path contains spaces" ); + Ok(()) } } diff --git a/gix-credentials/tests/credentials.rs b/gix-credentials/tests/credentials.rs index f94dfd05e90..618e3ca0e11 100644 --- a/gix-credentials/tests/credentials.rs +++ b/gix-credentials/tests/credentials.rs @@ -1,4 +1,4 @@ -pub use gix_testtools::Result; +pub use gix_testtools::TestResult; mod helper; mod program; diff --git a/gix-credentials/tests/helper/cascade.rs b/gix-credentials/tests/helper/cascade.rs index 46a653d8f5f..01b8dc1327e 100644 --- a/gix-credentials/tests/helper/cascade.rs +++ b/gix-credentials/tests/helper/cascade.rs @@ -1,5 +1,5 @@ mod invoke { - use crate::Result; + use crate::TestResult; use bstr::ByteSlice; use gix_credentials::{ Program, @@ -10,7 +10,7 @@ mod invoke { use gix_sec::identity::Account; #[test] - fn reconstructed_urls_cannot_change_the_host_sent_to_helpers() -> Result { + fn reconstructed_urls_cannot_change_the_host_sent_to_helpers() -> TestResult { for user in ["victim@trusted.example?", "victim@trusted.example/", "github.com/"] { let outcome = Cascade::default() .extend([Program::from_custom_definition( @@ -19,7 +19,7 @@ mod invoke { case "$line" in host=*) printf 'password=%s\n' "${line#host=}" ;; esac done }; f"#, - )]) + )?]) .invoke( Action::Get(Context { protocol: Some("https".into()), @@ -86,13 +86,13 @@ mod invoke { } #[test] - fn a_helper_closing_its_input_does_not_prevent_fallback_with_challenges() -> Result { + fn a_helper_closing_its_input_does_not_prevent_fallback_with_challenges() -> TestResult { let outcome = Cascade::default() .extend([ - Program::from_custom_definition("!f() { exit 1; }; f"), + Program::from_custom_definition("!f() { exit 1; }; f")?, Program::from_custom_definition( "!f() { cat >/dev/null; printf 'username=user\\npassword=pass\\n'; }; f", - ), + )?, ]) .invoke( Action::Get(Context { @@ -116,10 +116,10 @@ mod invoke { } #[test] - fn authentication_challenges_reach_all_helpers_until_credentials_are_complete() -> Result { + fn authentication_challenges_reach_all_helpers_until_credentials_are_complete() -> TestResult { let outcome = Cascade::default() .extend([ - Program::from_custom_definition("!f() { cat >/dev/null; echo username=user; }; f"), + Program::from_custom_definition("!f() { cat >/dev/null; echo username=user; }; f")?, Program::from_custom_definition( r#"!f() { while IFS= read -r line; do @@ -128,7 +128,7 @@ mod invoke { fi done }; f"#, - ), + )?, ]) .invoke( Action::Get(Context { @@ -156,15 +156,14 @@ mod invoke { } #[test] - fn credentials_are_filled_in_one_by_one_and_stop_when_complete() { - let actual = invoke_cascade(["username", "password", "custom-helper"], action_get()) - .unwrap() - .expect("credentials"); + fn credentials_are_filled_in_one_by_one_and_stop_when_complete() -> TestResult { + let actual = invoke_cascade(["username", "password", "custom-helper"], action_get())?.expect("credentials"); assert_eq!(actual.identity, identity("user", "pass")); + Ok(()) } #[test] - fn disabled_protocol_protection_is_preserved_for_the_next_action() { + fn disabled_protocol_protection_is_preserved_for_the_next_action() -> TestResult { let actual = Cascade { context_options: protocol::ContextOptions { protect_protocol: false, @@ -178,86 +177,79 @@ mod invoke { mode: gix_prompt::Mode::Disable, askpass: None, }, - ) - .expect("CR is allowed") + )? .expect("credentials are complete"); assert_eq!(actual.identity, identity("user\rname", "pass")); - let context: Context = (&actual.next).try_into().expect("the next action retains its options"); + let context: Context = (&actual.next).try_into()?; assert_eq!(context.username.as_deref(), Some("user\rname")); let mut serialized = Vec::new(); - actual - .next - .store() - .send(&mut serialized) - .expect("in-memory write succeeds"); + actual.next.store().send(&mut serialized)?; assert!(serialized.contains_str("username=user\rname")); + Ok(()) } #[test] - fn usernames_in_urls_are_kept_if_the_helper_does_not_overwrite_it() { + fn usernames_in_urls_are_kept_if_the_helper_does_not_overwrite_it() -> TestResult { let actual = invoke_cascade( ["password", "custom-helper"], Action::get_for_url("ssh://git@host.org/path"), - ) - .unwrap() + )? .expect("credentials"); assert_eq!(actual.identity, identity("git", "pass")); + Ok(()) } #[test] - fn partial_credentials_can_be_overwritten_by_complete_ones() { - let actual = invoke_cascade(["username", "custom-helper"], action_get()) - .unwrap() - .expect("credentials"); + fn partial_credentials_can_be_overwritten_by_complete_ones() -> TestResult { + let actual = invoke_cascade(["username", "custom-helper"], action_get())?.expect("credentials"); assert_eq!(actual.identity, identity("user-script", "pass-script")); + Ok(()) } #[test] - fn failing_helpers_for_filling_dont_interrupt() { - let actual = invoke_cascade(["fail", "custom-helper"], action_get()) - .unwrap() - .expect("credentials"); + fn failing_helpers_for_filling_dont_interrupt() -> TestResult { + let actual = invoke_cascade(["fail", "custom-helper"], action_get())?.expect("credentials"); assert_eq!(actual.identity, identity("user-script", "pass-script")); + Ok(()) } #[test] - fn urls_are_split_in_get_to_support_scripts() { + fn urls_are_split_in_get_to_support_scripts() -> TestResult { let actual = invoke_cascade( ["reflect", "custom-helper"], Action::get_for_url("https://example.com:8080/path/git/"), - ) - .unwrap() + )? .expect("credentials"); - let ctx: Context = (&actual.next).try_into().unwrap(); + let ctx: Context = (&actual.next).try_into()?; assert_eq!(ctx.protocol.as_deref().expect("protocol"), "https"); assert_eq!(ctx.host.as_deref().expect("host"), "example.com:8080"); assert_eq!(ctx.path.as_deref().expect("path").as_bstr(), "path/git"); + Ok(()) } #[test] - fn urls_are_split_in_get_but_can_skip_the_path_in_host_only_urls() { - let actual = invoke_cascade(["reflect", "custom-helper"], Action::get_for_url("http://example.com")) - .unwrap() + fn urls_are_split_in_get_but_can_skip_the_path_in_host_only_urls() -> TestResult { + let actual = invoke_cascade(["reflect", "custom-helper"], Action::get_for_url("http://example.com"))? .expect("credentials"); - let ctx: Context = (&actual.next).try_into().unwrap(); + let ctx: Context = (&actual.next).try_into()?; assert_eq!(ctx.protocol.as_deref().expect("protocol"), "http"); assert_eq!(ctx.host.as_deref().expect("host"), "example.com"); assert_eq!(ctx.path, None); + Ok(()) } #[test] - fn helpers_can_set_any_context_value() { + fn helpers_can_set_any_context_value() -> TestResult { let actual = invoke_cascade( ["all-but-credentials", "custom-helper"], Action::get_for_url("http://github.com"), - ) - .unwrap() + )? .expect("credentials"); - let ctx: Context = (&actual.next).try_into().unwrap(); + let ctx: Context = (&actual.next).try_into()?; assert_eq!(ctx.protocol.as_deref().expect("protocol"), "ftp"); assert_eq!(ctx.host.as_deref().expect("host"), "example.com:8080"); assert_eq!( @@ -265,15 +257,15 @@ mod invoke { "/path/to/git/", "values are passed verbatim even if they would otherwise look different" ); + Ok(()) } #[test] - fn helpers_can_set_any_context_value_using_the_url_only() { - let actual = invoke_cascade(["url", "custom-helper"], Action::get_for_url("http://github.com")) - .unwrap() - .expect("credentials"); + fn helpers_can_set_any_context_value_using_the_url_only() -> TestResult { + let actual = + invoke_cascade(["url", "custom-helper"], Action::get_for_url("http://github.com"))?.expect("credentials"); - let ctx: Context = (&actual.next).try_into().unwrap(); + let ctx: Context = (&actual.next).try_into()?; assert_eq!( ctx.protocol.as_deref().expect("protocol"), "http", @@ -285,24 +277,24 @@ mod invoke { "path/to/git", "the url is processed like any other" ); + Ok(()) } #[test] - fn helpers_can_quit_and_their_creds_are_taken_if_complete() { - let actual = invoke_cascade(["last-pass", "custom-helper"], Action::get_for_url("http://github.com")) - .unwrap() + fn helpers_can_quit_and_their_creds_are_taken_if_complete() -> TestResult { + let actual = invoke_cascade(["last-pass", "custom-helper"], Action::get_for_url("http://github.com"))? .expect("credentials"); assert_eq!(actual.identity, identity("user", "pass")); + Ok(()) } #[test] - fn expired_credentials_are_not_returned() { + fn expired_credentials_are_not_returned() -> TestResult { let actual = invoke_cascade( ["expired", "oauth-token", "custom-helper"], Action::get_for_url("http://github.com"), - ) - .unwrap() + )? .expect("credentials"); assert_eq!( @@ -313,10 +305,11 @@ mod invoke { }, "it ignored the expired password, which otherwise would have come first" ); + Ok(()) } #[test] - fn bogus_password_overrides_any_helper_and_helper_overrides_username_in_url() { + fn bogus_password_overrides_any_helper_and_helper_overrides_username_in_url() -> TestResult { let actual = Cascade::default() .query_user_only(true) .extend(fixtures(["username", "password"])) @@ -326,10 +319,10 @@ mod invoke { mode: gix_prompt::Mode::Disable, askpass: None, }, - ) - .unwrap() + )? .expect("credentials"); assert_eq!(actual.identity, identity("user", "")); + Ok(()) } fn action_get() -> Action { diff --git a/gix-credentials/tests/helper/context.rs b/gix-credentials/tests/helper/context.rs index 82d02c4173f..1202eea6e5b 100644 --- a/gix-credentials/tests/helper/context.rs +++ b/gix-credentials/tests/helper/context.rs @@ -1,9 +1,9 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteSlice; use gix_credentials::protocol::{Context, ContextOptions}; #[test] -fn authentication_challenges_survive_a_protocol_roundtrip() -> Result { +fn authentication_challenges_survive_a_protocol_roundtrip() -> TestResult { let input = b"protocol=https host=github.com wwwauth[]=Basic realm=\"GitHub\" domain_hint=\"example\" @@ -63,26 +63,26 @@ mod write_to { use gix_credentials::protocol::{Context, ContextOptions}; #[test] - fn quit_is_not_serialized_but_can_be_parsed() { + fn quit_is_not_serialized_but_can_be_parsed() -> gix_testtools::TestResult { let mut buf = Vec::<u8>::new(); Context { quit: Some(true), ..Default::default() } - .write_to(&mut buf) - .unwrap(); + .write_to(&mut buf)?; assert_eq!( - Context::from_bytes(&buf, ContextOptions::default()).unwrap(), + Context::from_bytes(&buf, ContextOptions::default())?, Context::default() ); assert_eq!( - Context::from_bytes(b"quit=true\nurl=https://example.com", ContextOptions::default()).unwrap(), + Context::from_bytes(b"quit=true\nurl=https://example.com", ContextOptions::default())?, Context { quit: Some(true), url: Some("https://example.com".into()), ..Default::default() } ); + Ok(()) } #[test] @@ -166,13 +166,11 @@ username=bob"; } #[test] - fn quit_supports_git_config_boolean_values() { + fn quit_supports_git_config_boolean_values() -> gix_testtools::TestResult { for true_value in ["1", "42", "-42", "true", "on", "yes"] { let input = format!("quit={true_value}"); assert_eq!( - Context::from_bytes(input.as_bytes(), ContextOptions::default()) - .unwrap() - .quit, + Context::from_bytes(input.as_bytes(), ContextOptions::default())?.quit, Some(true), "{input}" ); @@ -180,13 +178,12 @@ username=bob"; for false_value in ["0", "false", "off", "no"] { let input = format!("quit={false_value}"); assert_eq!( - Context::from_bytes(input.as_bytes(), ContextOptions::default()) - .unwrap() - .quit, + Context::from_bytes(input.as_bytes(), ContextOptions::default())?.quit, Some(false), "{input}" ); } + Ok(()) } #[test] @@ -196,14 +193,13 @@ username=bob"; } #[test] - fn carriage_returns_can_be_allowed() { + fn carriage_returns_can_be_allowed() -> gix_testtools::TestResult { let ctx = Context::from_bytes( b"url=https://example.com/with\rreturn\n", ContextOptions { protect_protocol: false, }, - ) - .expect("CR protection is disabled"); + )?; assert_eq!(ctx.url, Some(b"https://example.com/with\rreturn".as_slice().into())); assert_eq!( ctx.options, @@ -214,8 +210,8 @@ username=bob"; ); let mut out = Vec::new(); - ctx.write_to(&mut out) - .expect("retained options allow the same value to be encoded again"); + ctx.write_to(&mut out)?; assert_eq!(out, b"url=https://example.com/with\rreturn\n"); + Ok(()) } } diff --git a/gix-credentials/tests/helper/invoke.rs b/gix-credentials/tests/helper/invoke.rs index d75f271c4d4..81f853a920a 100644 --- a/gix-credentials/tests/helper/invoke.rs +++ b/gix-credentials/tests/helper/invoke.rs @@ -7,12 +7,11 @@ use gix_credentials::{ use crate::helper::script_helper; #[test] -fn get() { +fn get() -> gix_testtools::TestResult { let mut outcome = gix_credentials::helper::invoke( &mut script_helper("last-pass"), &helper::Action::get_for_url("https://github.com/byron/gitoxide"), - ) - .unwrap() + )? .expect("mock provides credentials"); assert_eq!( outcome.consume_identity().expect("complete"), @@ -23,27 +22,28 @@ fn get() { } ); assert_eq!( - outcome.next.store().payload().unwrap(), + outcome.next.store().payload().expect("store action has a payload"), "username=user\npassword=pass\nquit=1\n" ); + Ok(()) } #[test] -fn get_uses_context_options_for_the_entire_exchange() { +fn get_uses_context_options_for_the_entire_exchange() -> gix_testtools::TestResult { let action = helper::Action::Get(Context::from_url( "https://github.com/byron/gitoxide", ContextOptions { protect_protocol: false, }, )); - let outcome = gix_credentials::helper::invoke(&mut script_helper("carriage-return"), &action) - .expect("CR is allowed") + let outcome = gix_credentials::helper::invoke(&mut script_helper("carriage-return"), &action)? .expect("mock provides credentials"); assert_eq!(outcome.username.as_deref(), Some("user\rname")); - let context: Context = (&outcome.next).try_into().expect("the next action retains its options"); + let context: Context = (&outcome.next).try_into()?; assert_eq!(context.options, action.context().expect("get action").options); assert_eq!(context.username.as_deref(), Some("user\rname")); + Ok(()) } #[test] @@ -67,13 +67,13 @@ fn store_and_reject() { } mod program { - use crate::Result; + use crate::TestResult; use gix_credentials::{Program, helper, program::Kind}; use crate::helper::script_helper; #[test] - fn builtin() -> Result { + fn builtin() -> TestResult { // Other tests resolve fixture paths relative to the working directory, so change it only in a child. if gix_testtools::run_in_isolated_process()? { return Ok(()); @@ -100,15 +100,14 @@ mod program { } #[test] - fn script() { + fn script() -> gix_testtools::TestResult { assert_eq!( gix_credentials::helper::invoke( &mut Program::from_custom_definition( "!f() { test \"$1\" = get && echo \"password=pass\" && echo \"username=user\"; }; f" - ), + )?, &helper::Action::get_for_url("/does/not/matter"), - ) - .unwrap() + )? .expect("present") .consume_identity() .expect("complete"), @@ -118,6 +117,7 @@ mod program { oauth_refresh_token: None } ); + Ok(()) } #[cfg(unix)] // needs executable bits to work @@ -126,9 +126,9 @@ mod program { assert_eq!( gix_credentials::helper::invoke( &mut Program::from_custom_definition( - gix_path::into_bstr(gix_path::realpath(gix_testtools::fixture_path("custom-helper.sh"))?) + gix_path::into_bstr(gix_path::realpath(gix_testtools::fixture_path("custom-helper.sh"))?)? .into_owned(), - ), + )?, &helper::Action::get_for_url("/does/not/matter"), )? .expect("present") @@ -144,7 +144,7 @@ mod program { } #[test] - fn path_to_helper_as_script_to_workaround_executable_bits() -> Result { + fn path_to_helper_as_script_to_workaround_executable_bits() -> TestResult { assert_eq!( gix_credentials::helper::invoke( &mut script_helper("custom-helper"), diff --git a/gix-credentials/tests/helper/mod.rs b/gix-credentials/tests/helper/mod.rs index 497eef4cdc4..febb1f552fa 100644 --- a/gix-credentials/tests/helper/mod.rs +++ b/gix-credentials/tests/helper/mod.rs @@ -98,7 +98,7 @@ use std::{borrow::Cow, path::Path}; pub fn script_helper(name: &str) -> Program { fn to_arg<'a>(path: impl Into<Cow<'a, Path>>) -> BString { - let utf8_encoded = gix_path::into_bstr(path); + let utf8_encoded = gix_path::into_bstr(path).expect("fixture path is representable"); let slash_separated = gix_path::to_unix_separators_on_windows(utf8_encoded); gix_quote::single(slash_separated.as_ref()) } diff --git a/gix-credentials/tests/program/from_custom_definition.rs b/gix-credentials/tests/program/from_custom_definition.rs index beb0adda9bf..67adb09b228 100644 --- a/gix-credentials/tests/program/from_custom_definition.rs +++ b/gix-credentials/tests/program/from_custom_definition.rs @@ -1,4 +1,5 @@ use gix_credentials::{Program, helper, program::Kind}; +use gix_testtools::TestResult; use std::sync::LazyLock; static GIT: std::sync::LazyLock<&'static str> = std::sync::LazyLock::new(|| { @@ -20,14 +21,14 @@ static SH: LazyLock<&'static str> = LazyLock::new(|| { const SH_BASENAME: &str = if cfg!(windows) { "sh.exe" } else { "sh" }; #[test] -fn empty() { - let prog = Program::from_custom_definition(""); +fn empty() -> TestResult { + let prog = Program::from_custom_definition("")?; assert!(matches!(&prog.kind, Kind::ExternalName { name_and_args } if name_and_args.is_empty())); - let cmd = prog.to_command(&helper::Action::Store("egal".into())); + let cmd = prog.to_command(&helper::Action::Store("egal".into()))?; assert_eq!( std::path::Path::new(cmd.get_program()), std::path::Path::new( - std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + std::process::Command::try_from(gix_command::prepare(gix_path::env::exe_invocation()))?.get_program() ), "Git uses the platform's command resolution" ); @@ -36,16 +37,17 @@ fn empty() { ["credential-", "store"], "not useful, but allowed, would have to be caught elsewhere" ); + Ok(()) } #[test] -fn simple_script_in_path() { - let prog = Program::from_custom_definition("!exe"); +fn simple_script_in_path() -> TestResult { + let prog = Program::from_custom_definition("!exe")?; assert!(matches!(&prog.kind, Kind::ExternalShellScript(script) if script == "exe")); - let cmd = prog.to_command(&helper::Action::Store("egal".into())); + let cmd = prog.to_command(&helper::Action::Store("egal".into()))?; assert_eq!( cmd.get_program(), - std::process::Command::from(gix_command::prepare("exe")).get_program(), + std::process::Command::try_from(gix_command::prepare("exe"))?.get_program(), "it didn't detect anything shell-scripty, and thus doesn't use a shell" ); assert_eq!( @@ -53,18 +55,19 @@ fn simple_script_in_path() { ["store"], "the helper receives its action" ); + Ok(()) } #[test] -fn name_with_args() { +fn name_with_args() -> TestResult { let input = "name --arg --bar=\"a b\""; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); - let cmd = prog.to_command(&helper::Action::Store("egal".into())); + let cmd = prog.to_command(&helper::Action::Store("egal".into()))?; assert_eq!( std::path::Path::new(cmd.get_program()), std::path::Path::new( - std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + std::process::Command::try_from(gix_command::prepare(gix_path::env::exe_invocation()))?.get_program() ), "Git uses the platform's command resolution" ); @@ -73,34 +76,36 @@ fn name_with_args() { ["credential-name", "--arg", "--bar=a b", "store"], "quoted arguments are split without losing their contents" ); + Ok(()) } #[test] -fn name_with_special_args() { +fn name_with_special_args() -> TestResult { let input = "name --arg --bar=~/folder/in/home"; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; let sh = gix_path::env::shell_command(); - let git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::path::Path::new(*GIT))); + let git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::path::Path::new(*GIT))?); let quoted_git = gix_quote::single(git.as_ref()); assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), + format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))?), format!( r#"{sh:?} "-c" "{quoted_git} credential-name --arg --bar=~/folder/in/home \"$@\"" "{SH_BASENAME}" "store""# ) ); + Ok(()) } #[test] -fn name() { +fn name() -> TestResult { let input = "name"; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input)); - let cmd = prog.to_command(&helper::Action::Store("egal".into())); + let cmd = prog.to_command(&helper::Action::Store("egal".into()))?; assert_eq!( std::path::Path::new(cmd.get_program()), std::path::Path::new( - std::process::Command::from(gix_command::prepare(gix_path::env::exe_invocation())).get_program() + std::process::Command::try_from(gix_command::prepare(gix_path::env::exe_invocation()))?.get_program() ), "we detect that this can run without shell, which is also more portable on windows" ); @@ -109,44 +114,47 @@ fn name() { ["credential-name", "store"], "Git receives the helper name and action" ); + Ok(()) } #[test] -fn path_with_args_that_definitely_need_shell() { +fn path_with_args_that_definitely_need_shell() -> TestResult { let input = "/abs/name --arg --bar=\"a b\""; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input)); let sh = *SH; assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), + format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))?), if cfg!(windows) { r#""/abs/name" "--arg" "--bar=a b" "store""#.to_owned() } else { format!(r#""{sh}" "-c" "/abs/name --arg --bar=\"a b\" \"$@\"" "{SH_BASENAME}" "store""#) } ); + Ok(()) } #[test] -fn path_without_args() { +fn path_without_args() -> TestResult { let input = "/abs/name"; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input)); assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), + format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))?), r#""/abs/name" "store""#, "no shell is used" ); + Ok(()) } #[test] -fn path_with_simple_args() { +fn path_with_simple_args() -> TestResult { let input = "/abs/name a b"; - let prog = Program::from_custom_definition(input); + let prog = Program::from_custom_definition(input)?; assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input)); let sh = *SH; assert_eq!( - format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))), + format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))?), if cfg!(windows) { r#""/abs/name" "a" "b" "store""#.to_owned() } else { @@ -154,4 +162,5 @@ fn path_with_simple_args() { }, "a shell is used as there are arguments, and it's generally more flexible, but on windows we split ourselves" ); + Ok(()) } diff --git a/gix-credentials/tests/program/main.rs b/gix-credentials/tests/program/main.rs index da4fd74d444..a9eaad7bcec 100644 --- a/gix-credentials/tests/program/main.rs +++ b/gix-credentials/tests/program/main.rs @@ -4,7 +4,7 @@ use gix_error::Result; use std::{ffi::OsString, io::Cursor}; #[test] -fn action_aliases_are_accepted() -> Result { +fn action_aliases_are_accepted() -> gix_testtools::TestResult { for (input, expected) in [ ("get", "get"), ("fill", "get"), @@ -34,7 +34,7 @@ fn invalid_actions_are_validation_errors() { } #[test] -fn context_options_apply_to_input_and_output() { +fn context_options_apply_to_input_and_output() -> gix_testtools::TestResult { let input = b"url=https://github.com/with\rreturn\n"; let mut output = Vec::new(); let options = gix_credentials::protocol::ContextOptions { @@ -56,10 +56,10 @@ fn context_options_apply_to_input_and_output() { ..Default::default() })) }, - ) - .expect("carriage returns are allowed"); + )?; assert!(output.contains_str("user\rname")); + Ok(()) } #[test] diff --git a/gix-credentials/tests/protocol/context.rs b/gix-credentials/tests/protocol/context.rs index 9b3310e4c9e..156739af104 100644 --- a/gix-credentials/tests/protocol/context.rs +++ b/gix-credentials/tests/protocol/context.rs @@ -1,5 +1,5 @@ mod destructure_url_in_place { - use crate::Result; + use crate::TestResult; use gix_credentials::protocol::Context; fn url_ctx(url: &str) -> Context { @@ -39,7 +39,7 @@ mod destructure_url_in_place { } #[test] - fn passwords_are_placed_in_context_too() -> Result { + fn passwords_are_placed_in_context_too() -> TestResult { let mut ctx = url_ctx("http://user:password@host/path"); ctx.destructure_url_in_place(false)?; assert_eq!(ctx.password.as_deref(), Some("password")); @@ -80,7 +80,7 @@ mod destructure_url_in_place { } #[test] - fn component_paths_retain_existing_http_path_semantics() -> Result { + fn component_paths_retain_existing_http_path_semantics() -> TestResult { for protocol in ["https", "ssh"] { for use_http_path in [false, true] { for (path, normalized) in [("/repo/", Some("repo")), ("/", None)] { @@ -108,7 +108,7 @@ mod destructure_url_in_place { } #[test] - fn an_explicit_url_still_replaces_supplied_components() -> Result { + fn an_explicit_url_still_replaces_supplied_components() -> TestResult { let mut ctx = Context { url: Some("https://url-user:url-password@url.example/repo".into()), protocol: Some("http".into()), @@ -142,7 +142,7 @@ mod destructure_url_in_place { } #[test] - fn protocol_and_host_with_path_without_url_constructs_full_url() { + fn protocol_and_host_with_path_without_url_constructs_full_url() -> TestResult { let mut ctx = Context { protocol: Some("https".into()), host: Some("github.com".into()), @@ -151,8 +151,7 @@ mod destructure_url_in_place { password: Some("pass-to-be-ignored".into()), ..Default::default() }; - ctx.destructure_url_in_place(false) - .expect("should work with protocol, host and path"); + ctx.destructure_url_in_place(false)?; assert_eq!( ctx.url.unwrap(), @@ -163,6 +162,7 @@ mod destructure_url_in_place { assert_eq!(ctx.protocol.as_deref(), Some("https")); assert_eq!(ctx.host.as_deref(), Some("github.com")); assert_eq!(ctx.path.unwrap(), "org/repo"); + Ok(()) } #[test] @@ -204,11 +204,11 @@ mod to_prompt { } mod to_url { - use crate::Result; + use crate::TestResult; use gix_credentials::protocol::Context; #[test] - fn component_delimiters_cannot_change_the_credential_identity() -> Result { + fn component_delimiters_cannot_change_the_credential_identity() -> TestResult { for protocol in ["http", "https", "ssh", "git"] { for user in [ "github.com/", @@ -259,7 +259,7 @@ mod to_url { } #[test] - fn hosts_keep_ports_ipv6_and_scheme_specific_percent_encoding() -> Result { + fn hosts_keep_ports_ipv6_and_scheme_specific_percent_encoding() -> TestResult { for (protocol, host, encoded_host) in [ ("https", "[::1]:8443", "[::1]:8443"), ("https", "[fe80::1%25eth0]:8443", "[fe80::1%25eth0]:8443"), diff --git a/gix-date/tests/time/baseline.rs b/gix-date/tests/time/baseline.rs index cd3b46ed6f4..17e67fea4d8 100644 --- a/gix-date/tests/time/baseline.rs +++ b/gix-date/tests/time/baseline.rs @@ -47,7 +47,7 @@ static BASELINE: LazyLock<Vec<(String, Sample)>> = LazyLock::new(|| { }); #[test] -fn parse_compare_format() { +fn parse_compare_format() -> gix_testtools::TestResult { for ( pattern, Sample { @@ -79,18 +79,16 @@ fn parse_compare_format() { "{pattern:?} disagrees with baseline seconds since epoch: {actual:?}" ); if let Some(format_name) = format_name { - let reformatted = t - .format(match format_name.as_str() { - "SHORT" => Format::Custom(format::SHORT), - "RFC2822" => Format::Custom(format::RFC2822), - "ISO8601" => Format::Custom(format::ISO8601), - "ISO8601_STRICT" => Format::Custom(format::ISO8601_STRICT), - "GITOXIDE" => Format::Custom(format::GITOXIDE), - "UNIX" => Format::Unix, - "RAW" => Format::Raw, - unknown => unreachable!("All formats should be well-known and implemented: {unknown:?}"), - }) - .expect("valid input time"); + let reformatted = t.format(match format_name.as_str() { + "SHORT" => Format::Custom(format::SHORT), + "RFC2822" => Format::Custom(format::RFC2822), + "ISO8601" => Format::Custom(format::ISO8601), + "ISO8601_STRICT" => Format::Custom(format::ISO8601_STRICT), + "GITOXIDE" => Format::Custom(format::GITOXIDE), + "UNIX" => Format::Unix, + "RAW" => Format::Raw, + unknown => unreachable!("All formats should be well-known and implemented: {unknown:?}"), + })?; assert_eq!( reformatted, *pattern, "{reformatted:?} disagrees with baseline pattern: {pattern:?}" @@ -98,4 +96,5 @@ fn parse_compare_format() { } } } + Ok(()) } diff --git a/gix-date/tests/time/format.rs b/gix-date/tests/time/format.rs index 83cf4687872..207d0d24fcd 100644 --- a/gix-date/tests/time/format.rs +++ b/gix-date/tests/time/format.rs @@ -4,13 +4,13 @@ use gix_date::{ }; #[test] -fn short() -> gix_testtools::Result { +fn short() -> gix_testtools::TestResult { assert_eq!(time().format(format::SHORT)?, "1973-11-30"); Ok(()) } #[test] -fn unix() -> gix_testtools::Result { +fn unix() -> gix_testtools::TestResult { let expected = "123456789"; assert_eq!(time().format(Format::Unix)?, expected); assert_eq!(time().format(format::UNIX)?, expected); @@ -18,7 +18,7 @@ fn unix() -> gix_testtools::Result { } #[test] -fn raw() -> gix_testtools::Result { +fn raw() -> gix_testtools::TestResult { for (time, expected) in [ (time(), "123456789 +0230"), ( @@ -36,26 +36,26 @@ fn raw() -> gix_testtools::Result { } #[test] -fn iso8601() -> gix_testtools::Result { +fn iso8601() -> gix_testtools::TestResult { assert_eq!(time().format(format::ISO8601)?, "1973-11-30 00:03:09 +0230"); Ok(()) } #[test] -fn iso8601_strict() -> gix_testtools::Result { +fn iso8601_strict() -> gix_testtools::TestResult { assert_eq!(time().format(format::ISO8601_STRICT)?, "1973-11-30T00:03:09+02:30"); Ok(()) } #[test] -fn rfc2822() -> gix_testtools::Result { +fn rfc2822() -> gix_testtools::TestResult { assert_eq!(time().format(format::RFC2822)?, "Fri, 30 Nov 1973 00:03:09 +0230"); assert_eq!(time_dec1().format(format::RFC2822)?, "Sat, 01 Dec 1973 00:03:09 +0230"); Ok(()) } #[test] -fn git_rfc2822() -> gix_testtools::Result { +fn git_rfc2822() -> gix_testtools::TestResult { assert_eq!(time().format(format::GIT_RFC2822)?, "Fri, 30 Nov 1973 00:03:09 +0230"); assert_eq!( time_dec1().format(format::GIT_RFC2822)?, @@ -65,7 +65,7 @@ fn git_rfc2822() -> gix_testtools::Result { } #[test] -fn default() -> gix_testtools::Result { +fn default() -> gix_testtools::TestResult { assert_eq!(time().format(format::GITOXIDE)?, "Fri Nov 30 1973 00:03:09 +0230"); assert_eq!(time_dec1().format(format::GITOXIDE)?, "Sat Dec 01 1973 00:03:09 +0230"); Ok(()) @@ -84,7 +84,7 @@ fn format_or_unix() { } #[test] -fn git_default() -> gix_testtools::Result { +fn git_default() -> gix_testtools::TestResult { assert_eq!( time().format(gix_date::time::format::DEFAULT)?, "Fri Nov 30 00:03:09 1973 +0230" diff --git a/gix-date/tests/time/main.rs b/gix-date/tests/time/main.rs index 45f25e80e5d..3ec8374d008 100644 --- a/gix-date/tests/time/main.rs +++ b/gix-date/tests/time/main.rs @@ -36,7 +36,7 @@ mod write_to { } #[test] - fn valid_roundtrips() -> Result<(), Box<dyn std::error::Error>> { + fn valid_roundtrips() -> gix_testtools::TestResult { for (time, expected) in [ ( Time { @@ -111,14 +111,14 @@ mod write_to { let mut buf = TimeBuf::default(); assert_eq!(time.to_str(&mut buf), expected); - let actual = output.as_bstr().to_string().parse::<Time>().expect("round-trippable"); + let actual = output.as_bstr().to_string().parse::<Time>()?; assert_eq!(time, actual); } Ok(()) } #[test] - fn max() -> gix_testtools::Result { + fn max() -> gix_testtools::TestResult { let mut buf = Vec::new(); Time::MAX.write_to(&mut buf)?; assert_eq!(Time::MAX.size(), 25, "The serialized size of the largest possible time"); @@ -133,7 +133,7 @@ mod write_to { } #[test] - fn min() -> gix_testtools::Result { + fn min() -> gix_testtools::TestResult { let mut buf = Vec::new(); Time::MIN.write_to(&mut buf)?; assert_eq!( diff --git a/gix-date/tests/time/parse/mod.rs b/gix-date/tests/time/parse/mod.rs index 72a1427f9e6..18b9005cd72 100644 --- a/gix-date/tests/time/parse/mod.rs +++ b/gix-date/tests/time/parse/mod.rs @@ -1,5 +1,5 @@ use gix_date::Time; -use gix_error::Result; +use gix_testtools::TestResult; #[test] fn time_without_offset_defaults_to_utc() { @@ -85,7 +85,7 @@ fn git_rfc2822() { } #[test] -fn raw() -> Result { +fn raw() -> TestResult { assert_eq!( gix_date::parse("1660874655 +0800", None)?, Time { diff --git a/gix-date/tests/time/parse/relative.rs b/gix-date/tests/time/parse/relative.rs index 5160c6a29f3..aabebaf0175 100644 --- a/gix-date/tests/time/parse/relative.rs +++ b/gix-date/tests/time/parse/relative.rs @@ -37,16 +37,15 @@ fn offset_leading_to_before_unix_epoch_can_be_represented() { } #[test] -fn the_timezone_of_now_controls_calendar_arithmetic() { - let now = jiff::Timestamp::from_second(1_772_325_000) - .expect("valid timestamp") - .to_zoned(TimeZone::fixed(jiff::tz::Offset::from_hours(-5).expect("valid offset"))); - let actual = gix_date::parse("1 month ago", Some(now)).expect("relative date parses"); +fn the_timezone_of_now_controls_calendar_arithmetic() -> gix_testtools::TestResult { + let now = jiff::Timestamp::from_second(1_772_325_000)?.to_zoned(TimeZone::fixed(jiff::tz::Offset::from_hours(-5)?)); + let actual = gix_date::parse("1 month ago", Some(now))?; assert_eq!( actual.seconds, 1_769_646_600, "local February 28th becomes January 28th" ); assert_eq!(actual.offset, -5 * 60 * 60, "the timezone offset is retained"); + Ok(()) } #[test] @@ -134,7 +133,7 @@ fn various() { } #[test] -fn months_and_years_roll_over_month_ends_like_git() { +fn months_and_years_roll_over_month_ends_like_git() -> gix_testtools::TestResult { // Each expected value is Git's own: `TZ=UTC GIT_TEST_DATE_NOW=<now> git rev-parse --since='<input>'` // (Git 2.48.1). Git subtracts months and years from the calendar fields while keeping the // day, so a day beyond the end of the target month rolls over into the following month, @@ -179,14 +178,13 @@ fn months_and_years_roll_over_month_ends_like_git() { ]; for (now, input, expected) in cases { let now = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(now); - let actual = gix_date::parse(input, Some(utc(now))) - .expect("these relative dates parse") - .seconds; + let actual = gix_date::parse(input, Some(utc(now)))?.seconds; assert_eq!( actual, expected, "'{input}' should produce the same point in time as `git rev-parse --since`" ); } + Ok(()) } #[test] diff --git a/gix-diff/src/blob/pipeline.rs b/gix-diff/src/blob/pipeline.rs index e039b61d69e..8d4b252633f 100644 --- a/gix-diff/src/blob/pipeline.rs +++ b/gix-diff/src/blob/pipeline.rs @@ -231,7 +231,7 @@ impl Pipeline { Some(root) => { self.path.clear(); self.path.push(root); - self.path.push(gix_path::from_bstr(rela_path)); + self.path.push(gix_path::from_bstr(rela_path)?); let data = if is_symlink { if !self.options.fs.symlink { bail!( @@ -241,10 +241,12 @@ impl Pipeline { } let target = none_if_missing(std::fs::read_link(&self.path)) .or_raise(|| message!("Entry at {rela_path:?} could not be read as symbolic link"))?; - target.map(|target| { - out.extend_from_slice(gix_path::into_bstr(target).as_ref()); - Data::Buffer { is_derived: false } - }) + target + .map(|target| -> Result<_> { + out.extend_from_slice(gix_path::into_bstr(target)?.as_ref()); + Ok(Data::Buffer { is_derived: false }) + }) + .transpose()? } else { let need_size_only = is_binary == Some(true); let size_in_bytes = (need_size_only @@ -263,7 +265,9 @@ impl Pipeline { _ => { match driver .filter(|_| convert.to_worktree()) - .and_then(|d| d.prepare_binary_to_text_cmd(&self.path)) + .map(|d| d.prepare_binary_to_text_cmd(&self.path)) + .transpose()? + .flatten() { Some(cmd) => { // Avoid letting the driver program fail if it doesn't exist. @@ -294,7 +298,7 @@ impl Pipeline { .worktree_filter .convert_to_git( file, - gix_path::from_bstr(rela_path).as_ref(), + gix_path::from_bstr(rela_path)?.as_ref(), attributes, &mut |buf| { if id.is_null() { @@ -385,21 +389,7 @@ impl Pipeline { || (convert == Mode::ToGitUnlessBinaryToTextIsPresent && driver.is_some_and(|d| d.binary_to_text_command.is_some())) { - let res = self - .worktree_filter - .convert_to_worktree( - out, - rela_path, - attributes, - to_worktree::Options { - can_delay: Delay::Forbid, - unknown_encoding: to_worktree::UnknownEncoding::Fail, - }, - ) - .or_raise(|| { - message!("Entry at {rela_path:?} could not be converted to worktree form") - })?; - + // Prepare before starting a streaming filter whose output must be consumed. let cmd_and_file = driver .and_then(|d| { d.binary_to_text_command.is_some().then(|| { @@ -413,21 +403,38 @@ impl Pipeline { tmp_file.with_mut(|tmp| self.path.push(tmp.path()))?; Ok(tmp_file) }) - .map(|tmp_file| { - ( + .and_then(|tmp_file| { + Ok(( d.prepare_binary_to_text_cmd(&self.path) + .map_err(|err| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, err) + })? .expect("always get cmd if command is set"), tmp_file, - ) + )) }) }) }) .transpose() .or_raise(|| { message!( - "Tempfile for binary-to-text conversion for entry at {rela_path:?} could not be created" + "Binary-to-text conversion for entry at {rela_path:?} could not be prepared" ) })?; + let res = self + .worktree_filter + .convert_to_worktree( + out, + rela_path, + attributes, + to_worktree::Options { + can_delay: Delay::Forbid, + unknown_encoding: to_worktree::UnknownEncoding::Fail, + }, + ) + .or_raise(|| { + message!("Entry at {rela_path:?} could not be converted to worktree form") + })?; match cmd_and_file { Some((cmd, mut tmp_file)) => { match res { @@ -524,9 +531,12 @@ fn run_cmd(rela_path: &BStr, mut cmd: Command, out: &mut Vec<u8>) -> Result { impl Driver { /// Produce an invocable command pre-configured to produce the filtered output on stdout after reading `path`. - pub fn prepare_binary_to_text_cmd(&self, path: &Path) -> Option<std::process::Command> { - let command: &BStr = self.binary_to_text_command.as_ref()?.as_ref(); - let cmd = gix_command::prepare(gix_path::from_bstr(command).into_owned()) + /// Returns an error if the configured command cannot be represented by the platform. + pub fn prepare_binary_to_text_cmd(&self, path: &Path) -> Result<Option<std::process::Command>> { + let Some(command) = self.binary_to_text_command.as_deref() else { + return Ok(None); + }; + let cmd = gix_command::prepare(gix_path::from_bstr(command.as_bstr())?.into_owned()) // TODO: Add support for an actual Context, validate it *can* match Git .with_context(Default::default()) .with_shell() @@ -534,7 +544,7 @@ impl Driver { .stdout(Stdio::piped()) .stderr(Stdio::piped()) .arg(path) - .into(); - Some(cmd) + .try_into()?; + Ok(Some(cmd)) } } diff --git a/gix-diff/src/blob/platform.rs b/gix-diff/src/blob/platform.rs index 2920c3eed66..4a2f9abaf4f 100644 --- a/gix-diff/src/blob/platform.rs +++ b/gix-diff/src/blob/platform.rs @@ -576,7 +576,7 @@ impl Platform { let (old, new) = self .resources() .ok_or_raise(|| message("Either the source or the destination of the diff operation were not set"))?; - let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstring(diff_command)) + let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstring(diff_command)?) .command_may_be_shell_script_disallow_manual_argument_splitting() .with_context(context) .env("GIT_DIFF_PATH_COUNTER", (count + 1).to_string()) @@ -584,9 +584,9 @@ impl Platform { .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) - .into(); + .try_into()?; - cmd.arg(gix_path::from_bstr(old.rela_path).into_owned()); + cmd.arg(gix_path::from_bstr(old.rela_path)?.into_owned()); let mut out = prepare_diff_command::Command { cmd, old: None, @@ -597,7 +597,7 @@ impl Platform { out.new = add_resource(&mut out.cmd, new)?; if old.rela_path != new.rela_path { - out.cmd.arg(gix_path::from_bstr(new.rela_path).into_owned()); + out.cmd.arg(gix_path::from_bstr(new.rela_path)?.into_owned()); } Ok(out) diff --git a/gix-diff/tests/diff/blob/pipeline.rs b/gix-diff/tests/diff/blob/pipeline.rs index 22292eee83e..45443d01571 100644 --- a/gix-diff/tests/diff/blob/pipeline.rs +++ b/gix-diff/tests/diff/blob/pipeline.rs @@ -1,17 +1,17 @@ pub(crate) mod convert_to_diffable { - use crate::Result; use gix_diff::blob::{ ResourceKind, pipeline, pipeline::{Options, WorktreeRoots}, }; use gix_filter::{eol, eol::AutoCrlf}; use gix_object::{bstr::ByteSlice, tree::EntryKind}; + use gix_testtools::TestResult; use crate::util::{insert, object_db}; #[test] - fn simple() -> Result { + fn simple() -> TestResult { for mode in [ pipeline::Mode::ToWorktreeAndBinaryToText, pipeline::Mode::ToGit, @@ -148,7 +148,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn binary_below_large_file_threshold() -> Result { + fn binary_below_large_file_threshold() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = gix_diff::blob::Pipeline::new( WorktreeRoots { @@ -203,7 +203,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn above_large_file_threshold() -> Result { + fn above_large_file_threshold() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = gix_diff::blob::Pipeline::new( WorktreeRoots { @@ -286,7 +286,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn non_existing() -> Result { + fn non_existing() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = gix_diff::blob::Pipeline::new( WorktreeRoots { @@ -356,7 +356,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn worktree_filter() -> Result { + fn worktree_filter() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let filter = gix_filter::Pipeline::new( Default::default(), @@ -491,7 +491,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn worktree_filter_skips_null_id_lookups() -> Result { + fn worktree_filter_skips_null_id_lookups() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; std::fs::write(tmp.path().join("a"), "worktree\r\n")?; let mut filter = gix_diff::blob::Pipeline::new( @@ -552,7 +552,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn binary_by_buffer_inspection() -> Result { + fn binary_by_buffer_inspection() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let root = crate::scripted_fixture_read_only("make_blob_repo.sh")?; let mut attributes = crate::blob::new_attributes_stack(root); @@ -692,7 +692,7 @@ pub(crate) mod convert_to_diffable { } #[test] - fn with_driver() -> Result { + fn with_driver() -> TestResult { let root = crate::scripted_fixture_read_only("make_blob_repo.sh")?; let command = "echo to-text; cat <"; let mut attributes = crate::blob::new_attributes_stack(&root); diff --git a/gix-diff/tests/diff/blob/platform.rs b/gix-diff/tests/diff/blob/platform.rs index 5f1a7c42c82..18c06de6d41 100644 --- a/gix-diff/tests/diff/blob/platform.rs +++ b/gix-diff/tests/diff/blob/platform.rs @@ -1,11 +1,11 @@ use std::process::Command; -use crate::Result; use gix_diff::blob::{Algorithm, Platform, ResourceKind, pipeline, platform, platform::prepare_diff::Operation}; use gix_object::{ bstr::{BString, ByteSlice}, tree::EntryKind, }; +use gix_testtools::TestResult; use crate::{ blob::pipeline::convert_to_diffable::default_options, @@ -14,7 +14,7 @@ use crate::{ }; #[test] -fn resources_of_worktree_and_odb_and_check_link() -> Result { +fn resources_of_worktree_and_odb_and_check_link() -> TestResult { let mut platform = new_platform( Some(gix_diff::blob::Driver { name: "a".into(), @@ -66,7 +66,7 @@ fn resources_of_worktree_and_odb_and_check_link() -> Result { "it ends up with the default, as it's not overridden anywhere" ); - let program = Command::from(gix_diff::command::prepare("test")); + let program = Command::try_from(gix_diff::command::prepare("test"))?; let program = program.get_program().to_string_lossy(); assert_eq!( comparable_ext_diff(platform.prepare_diff_command( @@ -213,7 +213,7 @@ fn comparable_ext_diff(cmd: gix_error::Result<gix_diff::blob::platform::prepare_ } #[test] -fn diff_binary() -> Result { +fn diff_binary() -> TestResult { let mut platform = new_platform( Some(gix_diff::blob::Driver { name: "a".into(), @@ -256,7 +256,7 @@ fn diff_binary() -> Result { } #[test] -fn diff_performed_despite_external_command() -> Result { +fn diff_performed_despite_external_command() -> TestResult { let mut platform = new_platform( Some(gix_diff::blob::Driver { name: "a".into(), @@ -295,7 +295,7 @@ fn diff_performed_despite_external_command() -> Result { } #[test] -fn diff_skipped_due_to_external_command_and_enabled_option() -> Result { +fn diff_skipped_due_to_external_command_and_enabled_option() -> TestResult { let command: BString = "something-to-be-ignored".into(); let mut platform = new_platform( Some(gix_diff::blob::Driver { @@ -333,7 +333,7 @@ fn diff_skipped_due_to_external_command_and_enabled_option() -> Result { } #[test] -fn source_and_destination_do_not_exist() -> Result { +fn source_and_destination_do_not_exist() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::default()); let err = platform.prepare_diff().expect_err("neither resource has been set"); assert!( @@ -381,21 +381,19 @@ fn source_and_destination_do_not_exist() -> Result { ); insta::assert_debug_snapshot!(err, "source and destination do not exist", @"Tried to diff resources that are both considered removed"); - let program = Command::from(gix_diff::command::prepare("test")); + let program = Command::try_from(gix_diff::command::prepare("test"))?; assert_eq!( format!( "{:?}", - *platform - .prepare_diff_command( - "test".into(), - gix_diff::command::Context { - git_dir: Some(".".into()), - ..Default::default() - }, - 0, - 1 - ) - .expect("resources set") + *platform.prepare_diff_command( + "test".into(), + gix_diff::command::Context { + git_dir: Some(".".into()), + ..Default::default() + }, + 0, + 1 + )? ), format!( r#"{}{program:?} "missing" "/dev/null" "." "." "/dev/null" "." "." "a""#, @@ -410,7 +408,7 @@ fn source_and_destination_do_not_exist() -> Result { } #[test] -fn invalid_resource_types() -> Result { +fn invalid_resource_types() -> TestResult { let mut error_snapshots = Vec::new(); let mut platform = new_platform(None, pipeline::Mode::default()); for mode in [EntryKind::Commit, EntryKind::Tree] { @@ -470,7 +468,7 @@ fn invalid_resource_types() -> Result { } #[test] -fn resource_setup_errors_retain_paths_causes_and_clear_the_failed_side() -> Result { +fn resource_setup_errors_retain_paths_causes_and_clear_the_failed_side() -> TestResult { use platform::set_resource::Error; let mut platform = new_platform(None, pipeline::Mode::default()); diff --git a/gix-diff/tests/diff/blob/slider.rs b/gix-diff/tests/diff/blob/slider.rs index 7cc8690f7fc..db5dda6afab 100644 --- a/gix-diff/tests/diff/blob/slider.rs +++ b/gix-diff/tests/diff/blob/slider.rs @@ -9,7 +9,7 @@ use gix_object::bstr::ByteSlice; use pretty_assertions::StrComparison; #[test] -fn baseline() -> gix_testtools::Result { +fn baseline() -> gix_testtools::TestResult { let should_assert_strictly = std::env::var_os("GIX_DIFF_SLIDER_STRICT").is_some(); if let Some(case) = try_single_case(should_assert_strictly)? { eprintln!("{}", selected_case_report(&case)?); @@ -693,12 +693,12 @@ mod baseline { mod heuristics { //! We can consider to move some of these tests to the actual imara-diff test-suite as well. - use crate::Result; use gix_diff::blob::{self, diff_with_slider_heuristics}; use gix_object::bstr::BStr; + use gix_testtools::TestResult; #[test] - fn basic_usage() -> Result { + fn basic_usage() -> TestResult { let before = r#"fn foo() { let x = 1; println!("x = {}", x); @@ -726,7 +726,7 @@ mod heuristics { } #[test] - fn unified_diff_with_bstr_printer_usage() -> Result { + fn unified_diff_with_bstr_printer_usage() -> TestResult { let before: &BStr = r#"fn foo() { let x = 1; println!("x = {}", x); @@ -757,7 +757,7 @@ mod heuristics { /// Test slider heuristics with indentation #[test] - fn slider_heuristics_with_indentation() -> Result { + fn slider_heuristics_with_indentation() -> TestResult { let before = r#"fn main() { if true { println!("hello"); @@ -786,7 +786,7 @@ mod heuristics { /// Test that Myers algorithm also works with slider heuristics #[test] - fn myers_with_slider_heuristics() -> Result { + fn myers_with_slider_heuristics() -> TestResult { let before = "a\nb\nc\n"; let after = "a\nx\nc\n"; @@ -804,7 +804,7 @@ mod heuristics { /// Test empty diff #[test] - fn empty_diff_with_slider_heuristics() -> Result { + fn empty_diff_with_slider_heuristics() -> TestResult { let before = "unchanged\n"; let after = "unchanged\n"; @@ -819,7 +819,7 @@ mod heuristics { /// Test complex multi-hunk diff with slider heuristics #[test] - fn multi_hunk_diff_with_slider_heuristics() -> Result { + fn multi_hunk_diff_with_slider_heuristics() -> TestResult { let before = r#"struct Foo { x: i32, } @@ -859,7 +859,7 @@ mod heuristics { /// Test custom context size in the local unified diff printer. #[test] - fn custom_context_size() -> Result { + fn custom_context_size() -> TestResult { let before = "line1\nline2\nline3\nline4\nline5\nline6\nline7\n"; let after = "line1\nline2\nline3\nMODIFIED\nline5\nline6\nline7\n"; @@ -897,7 +897,7 @@ mod heuristics { /// Test that hunks iterator works correctly #[test] - fn hunks_iterator() -> Result { + fn hunks_iterator() -> TestResult { let before = "a\nb\nc\nd\ne\n"; let after = "a\nX\nc\nY\ne\n"; @@ -932,7 +932,7 @@ mod heuristics { /// Test postprocessing without heuristic #[test] - fn postprocess_no_heuristic() -> Result { + fn postprocess_no_heuristic() -> TestResult { let before = "a\nb\nc\n"; let after = "a\nX\nc\n"; @@ -952,7 +952,7 @@ mod heuristics { } #[test] - fn indent_heuristic_available() -> Result { + fn indent_heuristic_available() -> TestResult { let before = "fn foo() {\n x\n}\n"; let after = "fn foo() {\n y\n}\n"; diff --git a/gix-diff/tests/diff/blob/textconv.rs b/gix-diff/tests/diff/blob/textconv.rs index c8a71d1da47..9561c021ae1 100644 --- a/gix-diff/tests/diff/blob/textconv.rs +++ b/gix-diff/tests/diff/blob/textconv.rs @@ -8,7 +8,7 @@ use imara_diff::{Algorithm::Histogram, Diff}; use crate::blob::pipeline::convert_to_diffable::default_options; #[test] -fn binary_diff_with_textconv() -> gix_testtools::Result { +fn binary_diff_with_textconv() -> gix_testtools::TestResult { let workdir = crate::scripted_fixture_read_only("make_blob_textconv_repo.sh")?; let new_file_id = read_id(&workdir.join("new-file.id"))?; diff --git a/gix-diff/tests/diff/blob/unified_diff.rs b/gix-diff/tests/diff/blob/unified_diff.rs index 7e070afff8c..5912bf54555 100644 --- a/gix-diff/tests/diff/blob/unified_diff.rs +++ b/gix-diff/tests/diff/blob/unified_diff.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_diff::blob::UnifiedDiff; use gix_diff::blob::unified_diff::ConsumeBinaryHunk; use gix_diff::blob::{ @@ -6,9 +5,10 @@ use gix_diff::blob::{ unified_diff::{ConsumeHunk, ContextSize, DiffLineKind, HunkHeader}, }; use gix_object::bstr::BString; +use gix_testtools::TestResult; #[test] -fn removed_modified_added() -> Result { +fn removed_modified_added() -> TestResult { let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; let b = "2\n3\n4\n5\nsix\n7\n8\n9\n10\neleven\ntwelve"; @@ -100,7 +100,7 @@ fn removed_modified_added() -> Result { } #[test] -fn context_overlap_by_one_line_move_up() -> Result { +fn context_overlap_by_one_line_move_up() -> TestResult { let a = "2\n3\n4\n5\n6\n7\n"; let b = "7\n2\n3\n4\n5\n6\n"; @@ -127,7 +127,7 @@ fn context_overlap_by_one_line_move_up() -> Result { } #[test] -fn non_utf8() -> Result { +fn non_utf8() -> TestResult { let a = &b"\xC0\x80"[..]; let b = b"ascii"; @@ -166,7 +166,7 @@ fn non_utf8() -> Result { } #[test] -fn context_overlap_by_one_line_move_down() -> Result { +fn context_overlap_by_one_line_move_down() -> TestResult { let a = "2\n3\n4\n5\n6\n7\n"; let b = "7\n2\n3\n4\n5\n6\n"; @@ -193,7 +193,7 @@ fn context_overlap_by_one_line_move_down() -> Result { } #[test] -fn added_on_top_keeps_context_correctly_sized() -> Result { +fn added_on_top_keeps_context_correctly_sized() -> TestResult { let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; let b = "1\n2\n3\n4\n4.5\n5\n6\n7\n8\n9\n10"; @@ -296,7 +296,7 @@ fn added_on_top_keeps_context_correctly_sized() -> Result { } #[test] -fn removed_modified_added_with_newlines_in_tokens() -> Result { +fn removed_modified_added_with_newlines_in_tokens() -> TestResult { let a = "1\n2\n3\n4\n5\n6\n7\n8\n9\n10"; let b = "2\n3\n4\n5\nsix\n7\n8\n9\n10\neleven\ntwelve"; @@ -420,7 +420,7 @@ fn removed_modified_added_with_newlines_in_tokens() -> Result { } #[test] -fn all_added_or_removed() -> Result { +fn all_added_or_removed() -> TestResult { let content = "1\n2\n3\n4\n5"; let samples = [0, 1, 3, 100]; @@ -469,7 +469,7 @@ fn all_added_or_removed() -> Result { } #[test] -fn empty() -> Result { +fn empty() -> TestResult { let interner = gix_diff::blob::InternedInput::new(&b""[..], &b""[..]); let actual = render( Algorithm::Myers, diff --git a/gix-diff/tests/diff/index.rs b/gix-diff/tests/diff/index.rs index 01659a1e2ef..e700c0b2b18 100644 --- a/gix-diff/tests/diff/index.rs +++ b/gix-diff/tests/diff/index.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_diff::{ Rewrites, index::Change, @@ -6,10 +5,11 @@ use gix_diff::{ }; use gix_error::ErrorExt; use gix_object::bstr::BStr; +use gix_testtools::TestResult; #[test] -fn empty_to_new_tree_without_rename_tracking() -> Result { - let changes = collect_changes_no_renames(None, "c1 - initial").expect("really just an addition - nothing to track"); +fn empty_to_new_tree_without_rename_tracking() -> TestResult { + let changes = collect_changes_no_renames(None, "c1 - initial")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Addition { @@ -69,7 +69,7 @@ fn empty_to_new_tree_without_rename_tracking() -> Result { } #[test] -fn changes_against_modified_tree_with_filename_tracking() -> Result { +fn changes_against_modified_tree_with_filename_tracking() -> TestResult { let changes = collect_changes_no_renames("c2", "c3-modification")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ @@ -105,7 +105,7 @@ fn changes_against_modified_tree_with_filename_tracking() -> Result { } #[test] -fn renames_by_identity() -> Result { +fn renames_by_identity() -> TestResult { for (from, to, expected, assert_msg, track_empty) in [ ( "c3-modification", @@ -196,8 +196,7 @@ fn renames_by_identity() -> Result { } #[test] -fn rename_by_similarity() -> Result { - insta::allow_duplicates! { +fn rename_by_similarity() -> TestResult { for percentage in [ None, Some(0.76), /*cutoff point where git stops seeing it as equal */ @@ -209,7 +208,8 @@ fn rename_by_similarity() -> Result { percentage, ..Default::default() }), - ).expect("errors can only happen with IO or ODB access fails"); + )?; + insta::allow_duplicates! { insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Modification { @@ -243,11 +243,11 @@ fn rename_by_similarity() -> Result { }, ] "#); - let out = out.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); } + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); } let (changes, out) = collect_changes_opts( @@ -258,8 +258,7 @@ fn rename_by_similarity() -> Result { limit: 1, // has no effect as it's just one item here. ..Default::default() }), - ) - .expect("it found all items at the cut-off point, similar to git"); + )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ @@ -302,7 +301,7 @@ fn rename_by_similarity() -> Result { } #[test] -fn renames_by_similarity_with_limit() -> Result { +fn renames_by_similarity_with_limit() -> TestResult { let (changes, out) = collect_changes_opts( "c6", "r5", @@ -360,7 +359,7 @@ fn renames_by_similarity_with_limit() -> Result { } #[test] -fn copies_by_identity() -> Result { +fn copies_by_identity() -> TestResult { let (changes, out) = collect_changes_opts( "c7", "tc1-identity", @@ -431,7 +430,7 @@ fn copies_by_identity() -> Result { } #[test] -fn copies_by_similarity() -> Result { +fn copies_by_similarity() -> TestResult { let (changes, out) = collect_changes_opts( "tc1-identity", "tc2-similarity", @@ -502,7 +501,7 @@ fn copies_by_similarity() -> Result { } #[test] -fn copies_in_entire_tree_by_similarity() -> Result { +fn copies_in_entire_tree_by_similarity() -> TestResult { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", @@ -616,7 +615,7 @@ fn copies_in_entire_tree_by_similarity() -> Result { } #[test] -fn copies_in_entire_tree_by_similarity_with_limit() -> Result { +fn copies_in_entire_tree_by_similarity_with_limit() -> TestResult { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", @@ -700,7 +699,7 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> Result { } #[test] -fn realistic_renames_by_identity() -> Result { +fn realistic_renames_by_identity() -> TestResult { let (changes, out) = collect_changes_opts( "r1-base", "r1-change", @@ -762,7 +761,7 @@ fn realistic_renames_by_identity() -> Result { } #[test] -fn realistic_renames_disabled() -> Result { +fn realistic_renames_disabled() -> TestResult { let changes = collect_changes_no_renames("r1-base", "r1-change")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().collect::<Vec<_>>())).0, @r#" [ @@ -809,7 +808,7 @@ fn realistic_renames_disabled() -> Result { } #[test] -fn realistic_renames_disabled_3() -> Result { +fn realistic_renames_disabled_3() -> TestResult { let changes = collect_changes_no_renames("r3-base", "r3-change")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&(changes.into_iter().collect::<Vec<_>>())).0, @r#" @@ -853,7 +852,7 @@ fn realistic_renames_disabled_3() -> Result { } #[test] -fn realistic_renames_by_identity_3() -> Result { +fn realistic_renames_by_identity_3() -> TestResult { let (changes, out) = collect_changes_opts( "r3-base", "r3-change", @@ -912,7 +911,7 @@ fn realistic_renames_by_identity_3() -> Result { } #[test] -fn realistic_renames_2() -> Result { +fn realistic_renames_2() -> TestResult { let (changes, out) = collect_changes_opts( "r2-base", "r2-change", @@ -1071,7 +1070,7 @@ fn realistic_renames_2() -> Result { } #[test] -fn realistic_renames_3_without_identity() -> Result { +fn realistic_renames_3_without_identity() -> TestResult { let (changes, out) = collect_changes_opts( "r4-base", "r4-dir-rename-non-identity", @@ -1318,7 +1317,7 @@ fn sparse_indices_are_invalid_diff_input() -> gix_error::TestResult { } #[test] -fn unmerged_entries_and_intent_to_add() -> Result { +fn unmerged_entries_and_intent_to_add() -> TestResult { let (changes, _out) = collect_changes_opts( "r4-dir-rename-non-identity", ".git/index", diff --git a/gix-diff/tests/diff/rewrites/tracker.rs b/gix-diff/tests/diff/rewrites/tracker.rs index 82436fabb9a..13f5e4075ff 100644 --- a/gix-diff/tests/diff/rewrites/tracker.rs +++ b/gix-diff/tests/diff/rewrites/tracker.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_diff::{ Rewrites, blob::DiffLineStats, @@ -13,6 +12,7 @@ use gix_diff::{ tree::visit::Relation, }; use gix_object::tree::EntryKind; +use gix_testtools::TestResult; use pretty_assertions::assert_eq; use crate::{ @@ -21,7 +21,7 @@ use crate::{ }; #[test] -fn rename_by_id() -> Result { +fn rename_by_id() -> TestResult { // Limits are only applied when doing rewrite-checks for limit in [0, 1] { let rewrites = Rewrites { @@ -131,7 +131,7 @@ fn gitlinks_are_renamed_only_by_id() { } #[test] -fn copy_by_similarity_reports_limit_if_encountered() -> Result { +fn copy_by_similarity_reports_limit_if_encountered() -> TestResult { let rewrites = Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFiles, @@ -182,7 +182,7 @@ fn copy_by_similarity_reports_limit_if_encountered() -> Result { } #[test] -fn copy_by_id() -> Result { +fn copy_by_id() -> TestResult { // Limits are only applied when doing rewrite-checks for limit in [0, 1] { let rewrites = Rewrites { @@ -260,7 +260,7 @@ fn copy_by_id() -> Result { } #[test] -fn copy_by_id_search_in_all_sources() -> Result { +fn copy_by_id_search_in_all_sources() -> TestResult { // Limits are only applied when doing rewrite-checks for limit in [0, 1] { let rewrites = Rewrites { @@ -345,7 +345,7 @@ fn copy_by_id_search_in_all_sources() -> Result { } #[test] -fn copy_by_50_percent_similarity() -> Result { +fn copy_by_50_percent_similarity() -> TestResult { let rewrites = Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFiles, @@ -428,7 +428,7 @@ fn copy_by_50_percent_similarity() -> Result { } #[test] -fn copy_by_id_in_additions_only() -> Result { +fn copy_by_id_in_additions_only() -> TestResult { let rewrites = Rewrites { copies: Some(Copies { source: CopySource::FromSetOfModifiedFiles, @@ -482,7 +482,7 @@ fn copy_by_id_in_additions_only() -> Result { } #[test] -fn rename_by_similarity_reports_limit_if_encountered() -> Result { +fn rename_by_similarity_reports_limit_if_encountered() -> TestResult { let rewrites = Rewrites { copies: None, percentage: Some(0.5), @@ -528,7 +528,7 @@ fn rename_by_similarity_reports_limit_if_encountered() -> Result { } #[test] -fn rename_by_50_percent_similarity() -> Result { +fn rename_by_50_percent_similarity() -> TestResult { let rewrites = Rewrites { copies: None, percentage: Some(0.5), @@ -613,7 +613,7 @@ fn rename_by_50_percent_similarity() -> Result { } #[test] -fn rename_by_similarity_prefers_stronger_match_over_same_filename_match() -> Result { +fn rename_by_similarity_prefers_stronger_match_over_same_filename_match() -> TestResult { let rewrites = Rewrites { copies: None, percentage: Some(0.5), @@ -672,7 +672,7 @@ fn rename_by_similarity_prefers_stronger_match_over_same_filename_match() -> Res } #[test] -fn directories_without_relation_are_ignored() -> Result { +fn directories_without_relation_are_ignored() -> TestResult { let mut track = util::new_tracker(Default::default()); let tree_without_relation = Change { id: *NULL_ID, @@ -689,7 +689,7 @@ fn directories_without_relation_are_ignored() -> Result { } #[test] -fn directory_renames_by_id_can_fail_gracefully() -> Result { +fn directory_renames_by_id_can_fail_gracefully() -> TestResult { let rename_by_similarity = Rewrites { copies: None, percentage: Some(0.5), @@ -838,7 +838,7 @@ fn directory_renames_by_id_can_fail_gracefully() -> Result { } #[test] -fn simple_directory_rename_by_id() -> Result { +fn simple_directory_rename_by_id() -> TestResult { let renames_by_identity = Rewrites { copies: None, percentage: None, @@ -1005,7 +1005,7 @@ fn simple_directory_rename_by_id() -> Result { } #[test] -fn remove_only() -> Result { +fn remove_only() -> TestResult { let mut track = util::new_tracker(Default::default()); assert!( track.try_push_change(Change::deletion(), "a".into()).is_none(), @@ -1026,7 +1026,7 @@ fn remove_only() -> Result { } #[test] -fn add_only() -> Result { +fn add_only() -> TestResult { let mut track = util::new_tracker(Default::default()); assert!( track.try_push_change(Change::addition(), "a".into()).is_none(), @@ -1046,7 +1046,7 @@ fn add_only() -> Result { } #[test] -fn rename_tracking_is_order_independent() -> Result { +fn rename_tracking_is_order_independent() -> TestResult { // #1832: exactly one of several identical-content additions can be matched as the rename of a // deletion. Which one is chosen must not depend on the order in which items are pushed - but // the parallel dirwalk and index-traversal threads deliver them in a nondeterministic order, @@ -1084,7 +1084,7 @@ fn rename_tracking_is_order_independent() -> Result { } #[test] -fn copy_source_selection_is_order_independent() -> Result { +fn copy_source_selection_is_order_independent() -> TestResult { // #1832, exhaustive-copy variant: with copies searched against all sources - including the whole // source tree that is pushed in during `emit` - the source chosen for an identical-content // destination must not depend on the order items were pushed. This also exercises the second diff --git a/gix-diff/tests/diff/tree.rs b/gix-diff/tests/diff/tree.rs index db18405b5fa..43cc6c5d212 100644 --- a/gix-diff/tests/diff/tree.rs +++ b/gix-diff/tests/diff/tree.rs @@ -202,7 +202,7 @@ mod changes { } #[test] - fn many_different_states() -> Result { + fn many_different_states() -> gix_testtools::TestResult { let db = db(None)?; let all_commits = all_commits(&db); pretty_assertions::assert_eq!( @@ -606,7 +606,7 @@ mod changes { } #[test] - fn many_different_states_nested() -> Result { + fn many_different_states_nested() -> gix_testtools::TestResult { let db = db(["a"].iter().copied())?; let all_commits = all_commits(&db); @@ -677,7 +677,7 @@ mod changes { } #[test] - fn maximal_difference() -> Result { + fn maximal_difference() -> gix_testtools::TestResult { let db = db(None)?; let all_commits = all_commits(&db); @@ -743,7 +743,7 @@ mod changes { } #[test] - fn maximal_difference_nested() -> Result { + fn maximal_difference_nested() -> gix_testtools::TestResult { let db = db(["a"].iter().copied())?; let all_commits = all_commits(&db); let empty_blob_id = hex_to_id( @@ -814,7 +814,7 @@ mod changes { } #[test] - fn directory_rename() -> Result { + fn directory_rename() -> gix_testtools::TestResult { let db = db(None)?; let all_commits = all_commits(&db); let empty_blob_id = hex_to_id( @@ -903,7 +903,7 @@ mod changes { } #[test] - fn reverse_directory_rename() -> Result { + fn reverse_directory_rename() -> gix_testtools::TestResult { let db = db(None)?; let all_commits = all_commits(&db); let empty_blob_id = hex_to_id( diff --git a/gix-diff/tests/diff/tree_with_rewrites.rs b/gix-diff/tests/diff/tree_with_rewrites.rs index 1eaf60b708f..6ce139fce86 100644 --- a/gix-diff/tests/diff/tree_with_rewrites.rs +++ b/gix-diff/tests/diff/tree_with_rewrites.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_diff::{ Rewrites, rewrites::{Copies, CopySource}, @@ -7,11 +6,12 @@ use gix_diff::{ }; use gix_error::ErrorExt; use gix_object::{TreeRefIter, bstr::BStr}; +use gix_testtools::TestResult; #[test] -fn empty_to_new_tree_without_rename_tracking() -> Result { +fn empty_to_new_tree_without_rename_tracking() -> TestResult { let mut error_snapshots = Vec::new(); - let (changes, _out) = collect_changes(None, "c1 - initial").expect("full path tracking is the default"); + let (changes, _out) = collect_changes(None, "c1 - initial")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Addition { @@ -62,8 +62,7 @@ fn empty_to_new_tree_without_rename_tracking() -> Result { location: Some(Location::FileName), ..Default::default() }, - ) - .expect("the path-options are respected - we only see the filename here"); + )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ Addition { @@ -135,7 +134,7 @@ fn empty_to_new_tree_without_rename_tracking() -> Result { } #[test] -fn changes_against_modified_tree_with_filename_tracking() -> Result { +fn changes_against_modified_tree_with_filename_tracking() -> TestResult { let (changes, _out) = collect_changes("c2", "c3-modification")?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" @@ -200,7 +199,7 @@ fn changes_against_modified_tree_with_filename_tracking() -> Result { } #[test] -fn renames_by_identity() -> Result { +fn renames_by_identity() -> TestResult { for (from, to, expected, assert_msg, track_empty) in [ ( "c3-modification", @@ -295,8 +294,7 @@ fn renames_by_identity() -> Result { } #[test] -fn rename_by_similarity() -> Result { - insta::allow_duplicates! { +fn rename_by_similarity() -> TestResult { for percentage in [ None, Some(0.76), /*cutoff point where git stops seeing it as equal */ @@ -311,7 +309,8 @@ fn rename_by_similarity() -> Result { ..Default::default() }), }, - ).expect("errors can only happen with IO or ODB access fails"); + )?; + insta::allow_duplicates! { insta::assert_snapshot!(crate::normalize_debug_snapshot(&( changes)).0, @r#" @@ -345,11 +344,11 @@ fn rename_by_similarity() -> Result { ] "# ); - let out = out.expect("tracking enabled"); - assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); - assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); - assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); } + let out = out.expect("tracking enabled"); + assert_eq!(out.num_similarity_checks, if percentage.is_some() { 1 } else { 0 }); + assert_eq!(out.num_similarity_checks_skipped_for_rename_tracking_due_to_limit, 0); + assert_eq!(out.num_similarity_checks_skipped_for_copy_tracking_due_to_limit, 0); } let (changes, out) = collect_changes_opts( @@ -363,8 +362,7 @@ fn rename_by_similarity() -> Result { ..Default::default() }), }, - ) - .expect("it found all items at the cut-off point, similar to git"); + )?; insta::assert_snapshot!(crate::normalize_debug_snapshot(&changes).0, @r#" [ @@ -413,7 +411,7 @@ fn rename_by_similarity() -> Result { } #[test] -fn renames_by_similarity_with_limit() -> Result { +fn renames_by_similarity_with_limit() -> TestResult { let (changes, out) = collect_changes_opts( "c6", "r5", @@ -442,7 +440,7 @@ fn renames_by_similarity_with_limit() -> Result { } #[test] -fn copies_by_identity() -> Result { +fn copies_by_identity() -> TestResult { let (changes, out) = collect_changes_opts( "c7", "tc1-identity", @@ -514,7 +512,7 @@ fn copies_by_identity() -> Result { } #[test] -fn copies_by_similarity() -> Result { +fn copies_by_similarity() -> TestResult { let (changes, out) = collect_changes_opts( "tc1-identity", "tc2-similarity", @@ -602,7 +600,7 @@ fn copies_by_similarity() -> Result { } #[test] -fn copies_in_entire_tree_by_similarity() -> Result { +fn copies_in_entire_tree_by_similarity() -> TestResult { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", @@ -714,7 +712,7 @@ fn copies_in_entire_tree_by_similarity() -> Result { } #[test] -fn copies_in_entire_tree_by_similarity_with_limit() -> Result { +fn copies_in_entire_tree_by_similarity_with_limit() -> TestResult { let (changes, out) = collect_changes_opts( "tc2-similarity", "tc3-find-harder", @@ -785,7 +783,7 @@ fn copies_in_entire_tree_by_similarity_with_limit() -> Result { } #[test] -fn copies_by_similarity_with_limit() -> Result { +fn copies_by_similarity_with_limit() -> TestResult { let (changes, out) = collect_changes_opts( "tc1-identity", "tc2-similarity", @@ -847,7 +845,7 @@ fn copies_by_similarity_with_limit() -> Result { } #[test] -fn realistic_renames_by_identity() -> Result { +fn realistic_renames_by_identity() -> TestResult { let (changes, out) = collect_changes_opts( "r1-base", "r1-change", @@ -927,7 +925,7 @@ index e69de29..8ba3a16 100644 } #[test] -fn realistic_renames_disabled() -> Result { +fn realistic_renames_disabled() -> TestResult { let (changes, out) = collect_changes_opts( "r1-base", "r1-change", @@ -1000,7 +998,7 @@ index e69de29..8ba3a16 100644 } #[test] -fn realistic_renames_disabled_2() -> Result { +fn realistic_renames_disabled_2() -> TestResult { let (changes, out) = collect_changes_opts( "r2-base", "r2-change", @@ -1267,7 +1265,7 @@ index 0000000..e69de29 } #[test] -fn realistic_renames_disabled_3() -> Result { +fn realistic_renames_disabled_3() -> TestResult { let (changes, out) = collect_changes_opts( "r3-base", "r3-change", @@ -1339,7 +1337,7 @@ index e69de29..0000000 } #[test] -fn realistic_renames_by_identity_3() -> Result { +fn realistic_renames_by_identity_3() -> TestResult { let (changes, out) = collect_changes_opts( "r3-base", "r3-change", @@ -1418,7 +1416,7 @@ rename to src/gix.rs } #[test] -fn realistic_renames_2() -> Result { +fn realistic_renames_2() -> TestResult { let (changes, out) = collect_changes_opts( "r2-base", "r2-change", @@ -1681,7 +1679,7 @@ rename to gix-sec/tests/sec.rs } #[test] -fn realistic_renames_3_without_identity() -> Result { +fn realistic_renames_3_without_identity() -> TestResult { let (changes, out) = collect_changes_opts( "r4-base", "r4-dir-rename-non-identity", diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index 2312b1b5a8f..2184faf6210 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -37,7 +37,7 @@ pub fn root( buf.push(b'/'); } path_buf.push(component); - buf.extend_from_slice(gix_path::os_str_into_bstr(component.as_os_str()).expect("no illformed UTF8")); + buf.extend_from_slice(gix_path::os_str_into_bstr(component.as_os_str())?); let file_kind = path_buf.symlink_metadata().ok().map(|m| m.file_type().into()); out = path( diff --git a/gix-dir/src/walk/function.rs b/gix-dir/src/walk/function.rs index 913d9265800..86415b1ca6c 100644 --- a/gix-dir/src/walk/function.rs +++ b/gix-dir/src/walk/function.rs @@ -50,14 +50,13 @@ pub fn walk( ) -> Result<(Outcome, PathBuf)> { let root = match ctx.explicit_traversal_root { Some(root) => root.to_owned(), - None => ctx - .pathspec - .longest_common_directory() - .and_then(|candidate| { - let candidate = worktree_root.join(candidate); - candidate.is_dir().then_some(candidate) - }) - .unwrap_or_else(|| worktree_root.join(ctx.pathspec.prefix_directory())), + None => match ctx.pathspec.longest_common_directory()?.and_then(|candidate| { + let candidate = worktree_root.join(candidate); + candidate.is_dir().then_some(candidate) + }) { + Some(root) => root, + None => worktree_root.join(ctx.pathspec.prefix_directory()?), + }, }; let _span = gix_trace::coarse!("walk", root = ?root, worktree_root = ?worktree_root, options = ?options); let (mut current, worktree_root_relative) = assure_no_symlink_in_root(worktree_root, &root)?; @@ -93,7 +92,7 @@ pub fn walk( ); } if options.precompose_unicode { - buf = gix_path::into_bstr(gix_utils::str::precompose_path(gix_path::from_bstr(buf))).into_owned(); + buf = gix_path::into_bstr(gix_utils::str::precompose_path(gix_path::from_bstr(buf)?))?.into_owned(); } let _ = emit_entry( Cow::Borrowed(buf.as_bstr()), diff --git a/gix-dir/src/walk/readdir.rs b/gix-dir/src/walk/readdir.rs index a47069eb8ae..f4e6b4a57f6 100644 --- a/gix-dir/src/walk/readdir.rs +++ b/gix-dir/src/walk/readdir.rs @@ -56,11 +56,7 @@ pub(super) fn recursive( current_bstr.push(b'/'); } let file_name = entry.file_name(); - current_bstr.extend_from_slice( - gix_path::try_os_str_into_bstr(Cow::Borrowed(file_name.as_ref())) - .expect("no illformed UTF-8") - .as_ref(), - ); + current_bstr.extend_from_slice(gix_path::try_os_str_into_bstr(Cow::Borrowed(file_name.as_ref()))?.as_ref()); current.push(file_name); let mut info = classify::path( diff --git a/gix-dir/tests/dir/walk.rs b/gix-dir/tests/dir/walk.rs index f47ae75369d..a71253fdf33 100644 --- a/gix-dir/tests/dir/walk.rs +++ b/gix-dir/tests/dir/walk.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::{collections::BTreeSet, sync::atomic::AtomicBool}; use gix_dir::{ @@ -20,6 +20,39 @@ use crate::walk_utils::{ try_collect_filtered_opts, try_collect_filtered_opts_collect, try_collect_filtered_opts_collect_with_root, }; +#[cfg(windows)] +#[test] +fn invalid_directory_names_return_errors_for_entries_and_traversal_roots() -> TestResult { + use std::{ffi::OsString, os::windows::ffi::OsStringExt}; + + let root = gix_testtools::tempfile::tempdir()?; + let invalid_directory = root.path().join(OsString::from_wide(&[0xd800])); + std::fs::create_dir(&invalid_directory)?; + for explicit_root in [None, Some(invalid_directory.as_path())] { + let err = try_collect_filtered_opts_collect_with_root( + root.path(), + None, + explicit_root, + |keep, mut ctx| { + ctx.excludes = None; + walk(root.path(), ctx, options(), keep) + }, + None::<&str>, + Default::default(), + ) + .expect_err("unpaired surrogates must return an error during traversal"); + assert!( + err.is_validation(), + "the path encoding error remains a validation failure" + ); + assert!( + err.downcast_any_ref::<std::str::Utf8Error>().is_some(), + "the encoding failure remains available to the caller" + ); + } + Ok(()) +} + #[test] #[cfg(unix)] fn root_is_fifo() { @@ -112,7 +145,7 @@ fn fifo_in_traversal() { } #[test] -fn symlink_to_dir_can_be_excluded() -> Result { +fn symlink_to_dir_can_be_excluded() -> TestResult { let root = fixture_in("many-symlinks", "excluded-symlinks-to-dir"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -186,7 +219,7 @@ fn symlink_to_dir_can_be_excluded() -> Result { } #[test] -fn root_may_not_lead_through_symlinks() -> Result { +fn root_may_not_lead_through_symlinks() -> TestResult { let mut error_snapshots = Vec::new(); for (name, intermediate) in [ ("immediate-breakout-symlink", ""), @@ -221,7 +254,7 @@ fn root_may_not_lead_through_symlinks() -> Result { } #[test] -fn root_may_be_a_symlink_if_it_is_the_worktree() -> Result { +fn root_may_be_a_symlink_if_it_is_the_worktree() -> TestResult { let root = fixture_in("many-symlinks", "worktree-root-is-symlink"); let ((_out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -254,7 +287,7 @@ fn root_may_be_a_symlink_if_it_is_the_worktree() -> Result { #[test] #[cfg(unix)] -fn assume_unchanged_submodule_replaced_with_symlink_is_hidden() -> Result { +fn assume_unchanged_submodule_replaced_with_symlink_is_hidden() -> TestResult { let root = fixture_in("many-symlinks", "submodule-assume-unchanged-symlink"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -284,7 +317,7 @@ fn assume_unchanged_submodule_replaced_with_symlink_is_hidden() -> Result { #[test] #[cfg(unix)] -fn submodule_replaced_with_symlink_without_assume_unchanged_is_untracked() -> Result { +fn submodule_replaced_with_symlink_without_assume_unchanged_is_untracked() -> TestResult { let root = fixture_in("many-symlinks", "submodule-symlink"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -337,7 +370,7 @@ fn should_interrupt_works_even_in_empty_directories() { } #[test] -fn empty_root() -> Result { +fn empty_root() -> TestResult { let root = fixture("empty"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep)); assert_eq!( @@ -382,7 +415,7 @@ fn empty_root() -> Result { } #[test] -fn complex_empty() -> Result { +fn complex_empty() -> TestResult { let root = fixture("complex-empty"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -482,7 +515,7 @@ fn complex_empty() -> Result { } #[test] -fn ignored_with_prefix_pathspec_collapses_just_like_untracked() -> Result { +fn ignored_with_prefix_pathspec_collapses_just_like_untracked() -> TestResult { let root = fixture("untracked-and-ignored-for-collapse"); let ((out, _root), entries) = collect_filtered( &root, @@ -556,7 +589,7 @@ fn ignored_with_prefix_pathspec_collapses_just_like_untracked() -> Result { } #[test] -fn ignored_collapse_of_empty_directories_is_not_classified_as_empty_directory() -> Result { +fn ignored_collapse_of_empty_directories_is_not_classified_as_empty_directory() -> TestResult { let root = fixture("ignored-with-only-empty-dirs"); let ((out, _root), entries) = collect_filtered( &root, @@ -593,7 +626,7 @@ fn ignored_collapse_of_empty_directories_is_not_classified_as_empty_directory() } #[test] -fn ignored_dir_with_cwd_handling() -> Result { +fn ignored_dir_with_cwd_handling() -> TestResult { let root = fixture("untracked-and-ignored-for-collapse"); let ((out, _root), entries) = collect_filtered_with_cwd( &root, @@ -705,7 +738,7 @@ fn ignored_dir_with_cwd_handling() -> Result { } #[test] -fn ignored_with_cwd_handling() -> Result { +fn ignored_with_cwd_handling() -> TestResult { let root = gix_path::realpath(fixture("ignored-with-empty"))?; let ((out, _root), entries) = collect_filtered_with_cwd( &root, @@ -785,7 +818,7 @@ fn ignored_with_cwd_handling() -> Result { } #[test] -fn only_untracked_with_cwd_handling() -> Result { +fn only_untracked_with_cwd_handling() -> TestResult { let root = fixture("only-untracked"); let ((out, _root), entries) = collect_filtered_with_cwd( &root, @@ -940,7 +973,7 @@ fn only_untracked_with_cwd_handling() -> Result { } #[test] -fn only_untracked_with_pathspec() -> Result { +fn only_untracked_with_pathspec() -> TestResult { let root = fixture("only-untracked"); let ((out, _root), entries) = collect_filtered( &root, @@ -1009,7 +1042,7 @@ fn only_untracked_with_pathspec() -> Result { } #[test] -fn only_untracked_with_prefix_deletion() -> Result { +fn only_untracked_with_prefix_deletion() -> TestResult { let root = fixture("only-untracked"); let troot = root.join("d"); let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| { @@ -1067,7 +1100,7 @@ fn only_untracked_with_prefix_deletion() -> Result { } #[test] -fn only_untracked() -> Result { +fn only_untracked() -> TestResult { let root = fixture("only-untracked"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep)); assert_eq!( @@ -1142,7 +1175,7 @@ fn only_untracked() -> Result { } #[test] -fn only_untracked_explicit_pathspec_selection() -> Result { +fn only_untracked_explicit_pathspec_selection() -> TestResult { let root = fixture("only-untracked"); let ((out, _root), entries) = collect_filtered( &root, @@ -1361,7 +1394,7 @@ fn expendable_and_precious() { } #[test] -fn subdir_untracked() -> Result { +fn subdir_untracked() -> TestResult { let root = fixture("subdir-untracked"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options(), keep)); assert_eq!( @@ -1419,7 +1452,7 @@ fn subdir_untracked() -> Result { } #[test] -fn only_untracked_from_subdir() -> Result { +fn only_untracked_from_subdir() -> TestResult { let root = fixture("only-untracked"); let troot = root.join("d").join("d"); let ((out, _root), entries) = collect(&root, Some(&troot), |keep, ctx| walk(&root, ctx, options(), keep)); @@ -1440,7 +1473,7 @@ fn only_untracked_from_subdir() -> Result { } #[test] -fn untracked_and_ignored_pathspec_guidance() -> Result { +fn untracked_and_ignored_pathspec_guidance() -> TestResult { for for_deletion in [None, Some(Default::default())] { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( @@ -1479,7 +1512,7 @@ fn untracked_and_ignored_pathspec_guidance() -> Result { } #[test] -fn untracked_and_ignored_for_deletion_negative_wildcard_spec() -> Result { +fn untracked_and_ignored_for_deletion_negative_wildcard_spec() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -1534,7 +1567,7 @@ fn untracked_and_ignored_for_deletion_negative_wildcard_spec() -> Result { } #[test] -fn untracked_and_ignored_for_deletion_positive_wildcard_spec() -> Result { +fn untracked_and_ignored_for_deletion_positive_wildcard_spec() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -1587,7 +1620,7 @@ fn untracked_and_ignored_for_deletion_positive_wildcard_spec() -> Result { } #[test] -fn untracked_and_ignored_for_deletion_nonmatching_wildcard_spec() -> Result { +fn untracked_and_ignored_for_deletion_nonmatching_wildcard_spec() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -1637,7 +1670,7 @@ fn untracked_and_ignored_for_deletion_nonmatching_wildcard_spec() -> Result { Ok(()) } #[test] -fn nested_precious_repo_respects_wildcards() -> Result { +fn nested_precious_repo_respects_wildcards() -> TestResult { let root = fixture("precious-nested-repository"); for for_deletion in [ Some(ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories), @@ -1670,7 +1703,7 @@ fn nested_precious_repo_respects_wildcards() -> Result { } #[test] -fn nested_ignored_dirs_for_deletion_nonmatching_wildcard_spec() -> Result { +fn nested_ignored_dirs_for_deletion_nonmatching_wildcard_spec() -> TestResult { let root = fixture("ignored-dir-nested-minimal"); let (_out, entries) = collect_filtered( &root, @@ -1737,7 +1770,7 @@ fn nested_ignored_dirs_for_deletion_nonmatching_wildcard_spec() -> Result { } #[test] -fn expendable_and_precious_in_ignored_dir_with_pathspec() -> Result { +fn expendable_and_precious_in_ignored_dir_with_pathspec() -> TestResult { let root = fixture("expendable-and-precious-nested-in-ignored-dir"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -1885,7 +1918,7 @@ fn expendable_and_precious_in_ignored_dir_with_pathspec() -> Result { } #[test] -fn untracked_and_ignored() -> Result { +fn untracked_and_ignored() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -2079,7 +2112,7 @@ fn untracked_and_ignored() -> Result { } #[test] -fn untracked_and_ignored_collapse_handling_mixed() -> Result { +fn untracked_and_ignored_collapse_handling_mixed() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -2167,7 +2200,7 @@ fn untracked_and_ignored_collapse_handling_mixed() -> Result { } #[test] -fn untracked_and_ignored_collapse_handling_mixed_with_prefix() -> Result { +fn untracked_and_ignored_collapse_handling_mixed_with_prefix() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -2260,7 +2293,7 @@ fn untracked_and_ignored_collapse_handling_mixed_with_prefix() -> Result { } #[test] -fn untracked_and_ignored_collapse_handling_for_deletion_with_wildcards() -> Result { +fn untracked_and_ignored_collapse_handling_for_deletion_with_wildcards() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -2363,7 +2396,7 @@ fn untracked_and_ignored_collapse_handling_for_deletion_with_wildcards() -> Resu } #[test] -fn untracked_and_ignored_collapse_handling_for_deletion_with_prefix_wildcards() -> Result { +fn untracked_and_ignored_collapse_handling_for_deletion_with_prefix_wildcards() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect_filtered( &root, @@ -2400,7 +2433,7 @@ fn untracked_and_ignored_collapse_handling_for_deletion_with_prefix_wildcards() } #[test] -fn untracked_and_ignored_collapse_handling_for_deletion_mixed() -> Result { +fn untracked_and_ignored_collapse_handling_for_deletion_mixed() -> TestResult { let root = fixture("subdir-untracked-and-ignored"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -2859,7 +2892,7 @@ fn precious_are_not_expendable() { not(target_vendor = "apple"), ignore = "Needs filesystem that folds unicode composition" )] -fn decomposed_unicode_in_directory_is_returned_precomposed() -> Result { +fn decomposed_unicode_in_directory_is_returned_precomposed() -> TestResult { let root = gix_testtools::tempfile::TempDir::new()?; let decomposed = "a\u{308}"; @@ -2928,7 +2961,7 @@ fn decomposed_unicode_in_directory_is_returned_precomposed() -> Result { } #[test] -fn worktree_root_can_be_symlink() -> Result { +fn worktree_root_can_be_symlink() -> TestResult { let root = fixture_in("many-symlinks", "symlink-to-breakout-symlink"); let troot = root.join("file"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -2956,7 +2989,7 @@ fn worktree_root_can_be_symlink() -> Result { } #[test] -fn root_may_not_go_through_dot_git() -> Result { +fn root_may_not_go_through_dot_git() -> TestResult { let root = fixture("with-nested-dot-git"); for (dir, expected_pathspec) in [("", Some(Verbatim)), ("subdir", None)] { let troot = root.join("dir").join(".git").join(dir); @@ -2985,7 +3018,7 @@ fn root_may_not_go_through_dot_git() -> Result { } #[test] -fn root_at_submodule_repository_allows_walk() -> Result { +fn root_at_submodule_repository_allows_walk() -> TestResult { let root = fixture("repo-with-submodule"); let troot = root.join("submodule"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3026,7 +3059,7 @@ fn root_at_submodule_repository_allows_walk() -> Result { } #[test] -fn root_in_submodule_repository_allows_walk() -> Result { +fn root_in_submodule_repository_allows_walk() -> TestResult { let root = fixture("repo-with-submodule"); let troot = root.join("submodule"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3067,7 +3100,7 @@ fn root_in_submodule_repository_allows_walk() -> Result { } #[test] -fn root_in_submodule_from_superproject_repository_allows_walk() -> Result { +fn root_in_submodule_from_superproject_repository_allows_walk() -> TestResult { let root = fixture("repo-with-submodule"); let troot = root.join("submodule").join("dir"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3109,7 +3142,7 @@ fn root_in_submodule_from_superproject_repository_allows_walk() -> Result { } #[test] -fn root_enters_directory_with_dot_git_in_reconfigured_worktree_tracked() -> Result { +fn root_enters_directory_with_dot_git_in_reconfigured_worktree_tracked() -> TestResult { let root = fixture("nonstandard-worktree"); let troot = root.join("dir-with-dot-git").join("inside"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3179,7 +3212,7 @@ fn root_enters_directory_with_dot_git_in_reconfigured_worktree_tracked() -> Resu } #[test] -fn root_enters_directory_with_dot_git_in_reconfigured_worktree_untracked() -> Result { +fn root_enters_directory_with_dot_git_in_reconfigured_worktree_untracked() -> TestResult { let root = fixture("nonstandard-worktree-untracked"); let troot = root.join("dir-with-dot-git").join("inside"); let (_out, entries) = try_collect_filtered_opts_collect_with_root( @@ -3200,7 +3233,7 @@ fn root_enters_directory_with_dot_git_in_reconfigured_worktree_untracked() -> Re } #[test] -fn root_may_not_go_through_nested_repository_unless_enabled() -> Result { +fn root_may_not_go_through_nested_repository_unless_enabled() -> TestResult { let root = fixture("nested-repository"); let troot = root.join("nested").join("file"); let (_out, entries) = try_collect_filtered_opts_collect_with_root( @@ -3252,7 +3285,7 @@ fn root_may_not_go_through_nested_repository_unless_enabled() -> Result { } #[test] -fn root_may_not_go_through_submodule() -> Result { +fn root_may_not_go_through_submodule() -> TestResult { let root = fixture("with-submodule"); let troot = root.join("submodule").join("dir").join("file"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3281,7 +3314,7 @@ fn root_may_not_go_through_submodule() -> Result { } #[test] -fn walk_with_submodule() -> Result { +fn walk_with_submodule() -> TestResult { let root = fixture("with-submodule"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -3306,7 +3339,7 @@ fn walk_with_submodule() -> Result { } #[test] -fn root_that_is_tracked_file_is_returned() -> Result { +fn root_that_is_tracked_file_is_returned() -> TestResult { let root = fixture("dir-with-tracked-file"); let troot = &root.join("dir").join("file"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3335,7 +3368,7 @@ fn root_that_is_tracked_file_is_returned() -> Result { } #[test] -fn root_that_is_untracked_file_is_returned() -> Result { +fn root_that_is_untracked_file_is_returned() -> TestResult { let root = fixture("dir-with-file"); let troot = root.join("dir").join("file"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3372,7 +3405,7 @@ fn top_level_root_that_is_a_file() { } #[test] -fn root_can_be_pruned_early_with_pathspec() -> Result { +fn root_can_be_pruned_early_with_pathspec() -> TestResult { let root = fixture("dir-with-file"); let troot = root.join("dir"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3401,7 +3434,7 @@ fn root_can_be_pruned_early_with_pathspec() -> Result { } #[test] -fn submodules() -> Result { +fn submodules() -> TestResult { let root = fixture("multiple-submodules"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -3466,7 +3499,7 @@ fn submodules() -> Result { } #[test] -fn uninitialized_submodules_with_files_remain_tracked() -> Result { +fn uninitialized_submodules_with_files_remain_tracked() -> TestResult { let root = fixture("uninitialized-submodules-with-files"); assert_eq!( gix_testtools::git(&root, "--no-optional-locks status --porcelain=v1 --untracked-files=all")?, @@ -3514,7 +3547,7 @@ fn uninitialized_submodules_with_files_remain_tracked() -> Result { } #[test] -fn cancel_with_collection_does_not_fail() -> Result { +fn cancel_with_collection_does_not_fail() -> TestResult { struct CancelDelegate { emits_left_until_cancel: usize, } @@ -3578,7 +3611,7 @@ fn cancel_with_collection_does_not_fail() -> Result { } #[test] -fn file_root_is_shown_if_pathspec_matches_exactly() -> Result { +fn file_root_is_shown_if_pathspec_matches_exactly() -> TestResult { let root = fixture("dir-with-file"); let troot = root.join("dir").join("file"); let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root( @@ -3607,7 +3640,7 @@ fn file_root_is_shown_if_pathspec_matches_exactly() -> Result { } #[test] -fn root_that_is_tracked_and_ignored_is_considered_tracked() -> Result { +fn root_that_is_tracked_and_ignored_is_considered_tracked() -> TestResult { let root = fixture("tracked-is-ignored"); let walk_root = "dir/file"; let troot = root.join(walk_root); @@ -3638,7 +3671,7 @@ fn root_that_is_tracked_and_ignored_is_considered_tracked() -> Result { } #[test] -fn root_with_dir_that_is_tracked_and_ignored() -> Result { +fn root_with_dir_that_is_tracked_and_ignored() -> TestResult { let root = fixture("tracked-is-ignored"); for emission in [Matching, CollapseDirectory] { let ((out, _root), entries) = collect(&root, None, |keep, ctx| { @@ -3679,7 +3712,7 @@ fn root_with_dir_that_is_tracked_and_ignored() -> Result { } #[test] -fn empty_and_nested_untracked() -> Result { +fn empty_and_nested_untracked() -> TestResult { let root = fixture("empty-and-untracked-dir"); for for_deletion in [None, Some(Default::default())] { let ((out, _root), entries) = collect(&root, None, |keep, ctx| { @@ -3747,7 +3780,7 @@ fn empty_and_nested_untracked() -> Result { } #[test] -fn root_that_is_ignored_is_listed_for_files_and_directories() -> Result { +fn root_that_is_ignored_is_listed_for_files_and_directories() -> TestResult { let root = fixture("ignored-dir"); for walk_root in ["dir", "dir/file"] { let troot = root.join(walk_root); @@ -3791,7 +3824,7 @@ fn root_that_is_ignored_is_listed_for_files_and_directories() -> Result { } #[test] -fn nested_bare_repos_in_ignored_directories() -> Result { +fn nested_bare_repos_in_ignored_directories() -> TestResult { let root = fixture("ignored-dir-with-nested-bare-repository"); let (_out, entries) = collect(&root, None, |keep, ctx| { walk( @@ -3871,7 +3904,7 @@ fn nested_bare_repos_in_ignored_directories() -> Result { } #[test] -fn nested_repos_in_untracked_directories() -> Result { +fn nested_repos_in_untracked_directories() -> TestResult { let root = fixture("untracked-hidden-bare"); let (_out, entries) = collect(&root, None, |keep, ctx| { walk( @@ -3918,7 +3951,7 @@ fn nested_repos_in_untracked_directories() -> Result { } #[test] -fn nested_repos_in_ignored_directories() -> Result { +fn nested_repos_in_ignored_directories() -> TestResult { let root = fixture("ignored-dir-with-nested-repository"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| { walk( @@ -4024,7 +4057,7 @@ fn nested_repos_in_ignored_directories() -> Result { not(target_vendor = "apple"), ignore = "Needs filesystem that folds unicode composition" )] -fn decomposed_unicode_in_root_is_returned_precomposed() -> Result { +fn decomposed_unicode_in_root_is_returned_precomposed() -> TestResult { for (decomposed, precomposed) in [ ("a\u{308}", "ä"), ("📹/a\u{308}", "📹/ä"), @@ -4075,7 +4108,7 @@ fn decomposed_unicode_in_root_is_returned_precomposed() -> Result { } #[test] -fn precompose_unicode_preserves_noncanonical_combining_mark_order() -> Result { +fn precompose_unicode_preserves_noncanonical_combining_mark_order() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("noncanonical-combining-mark-order.sh")?.join("tracked"); let ((_out, _root), entries) = collect(&root, None, |keep, ctx| { @@ -4099,7 +4132,7 @@ fn precompose_unicode_preserves_noncanonical_combining_mark_order() -> Result { } #[test] -fn precompose_unicode_preserves_untracked_noncanonical_combining_mark_order() -> Result { +fn precompose_unicode_preserves_untracked_noncanonical_combining_mark_order() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("noncanonical-combining-mark-order.sh")?.join("untracked"); let ((_out, _root), entries) = collect(&root, None, |keep, ctx| { @@ -4238,7 +4271,7 @@ fn untracked_and_ignored_collapse_mix() { } #[test] -fn root_cannot_pass_through_case_altered_capital_dot_git_if_case_insensitive() -> Result { +fn root_cannot_pass_through_case_altered_capital_dot_git_if_case_insensitive() -> TestResult { let root = fixture("with-nested-capitalized-dot-git"); for (dir, expected_pathspec) in [("", Some(Verbatim)), ("subdir", None)] { let troot = root.join("dir").join(".GIT").join(dir); @@ -4301,7 +4334,7 @@ fn root_cannot_pass_through_case_altered_capital_dot_git_if_case_insensitive() - } #[test] -fn partial_checkout_cone_and_non_one() -> Result { +fn partial_checkout_cone_and_non_one() -> TestResult { for fixture_name in ["partial-checkout-cone-mode", "partial-checkout-non-cone"] { let root = fixture(fixture_name); let not_in_cone_but_created_locally_by_hand = "d/file-created-manually"; @@ -4336,7 +4369,7 @@ fn partial_checkout_cone_and_non_one() -> Result { } #[test] -fn type_mismatch() { +fn type_mismatch() -> TestResult { let root = fixture("type-mismatch"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -4358,8 +4391,7 @@ fn type_mismatch() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4400,8 +4432,7 @@ fn type_mismatch() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4419,10 +4450,11 @@ fn type_mismatch() { ], "collapsing works as well, and we allow to see the typechange" ); + Ok(()) } #[test] -fn type_mismatch_ignore_case() { +fn type_mismatch_ignore_case() -> TestResult { let root = fixture("type-mismatch-icase"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -4445,8 +4477,7 @@ fn type_mismatch_ignore_case() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4485,8 +4516,7 @@ fn type_mismatch_ignore_case() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4503,10 +4533,11 @@ fn type_mismatch_ignore_case() { ], "this is the same as in the non-icase version, which means that icase lookup works" ); + Ok(()) } #[test] -fn type_mismatch_ignore_case_clash_dir_is_file() { +fn type_mismatch_ignore_case_clash_dir_is_file() -> TestResult { let root = fixture("type-mismatch-icase-clash-dir-is-file"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -4529,8 +4560,7 @@ fn type_mismatch_ignore_case_clash_dir_is_file() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4544,10 +4574,11 @@ fn type_mismatch_ignore_case_clash_dir_is_file() { [entry("d", Tracked, File)], "file `d` exists on disk and it is found as well. This is just because we prefer finding files over dirs, coincidence" ); + Ok(()) } #[test] -fn type_mismatch_ignore_case_clash_file_is_dir() { +fn type_mismatch_ignore_case_clash_file_is_dir() -> TestResult { let root = fixture("type-mismatch-icase-clash-file-is-dir"); let ((out, _root), entries) = try_collect_filtered_opts_collect( &root, @@ -4570,8 +4601,7 @@ fn type_mismatch_ignore_case_clash_file_is_dir() { fresh_index: false, ..Default::default() }, - ) - .expect("success"); + )?; assert_eq!( out, walk::Outcome { @@ -4586,10 +4616,11 @@ fn type_mismatch_ignore_case_clash_file_is_dir() { "`D` exists on disk as directory, and we manage to to find it in in the index, hence no collapsing happens.\ If there was no special handling for this, it would have found the file (`d` in the index, icase), which would have been wrong." ); + Ok(()) } #[test] -fn top_level_slash_with_negations() -> Result { +fn top_level_slash_with_negations() -> TestResult { for repo_name in ["slash-in-root-and-negated", "star-in-root-and-negated"] { let root = fixture(repo_name); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); @@ -4646,7 +4677,7 @@ fn top_level_slash_with_negations() -> Result { } #[test] -fn subdir_slash_with_negations() -> Result { +fn subdir_slash_with_negations() -> TestResult { for repo_name in ["slash-in-subdir-and-negated", "star-in-subdir-and-negated"] { let root = fixture(repo_name); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); @@ -4703,7 +4734,7 @@ fn subdir_slash_with_negations() -> Result { } #[test] -fn one_ignored_submodule() -> Result { +fn one_ignored_submodule() -> TestResult { let root = fixture("one-ignored-submodule"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -4747,7 +4778,7 @@ fn one_ignored_submodule() -> Result { } #[test] -fn ignored_sub_repo() -> Result { +fn ignored_sub_repo() -> TestResult { let root = fixture("with-sub-repo"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -4806,7 +4837,7 @@ fn ignored_sub_repo() -> Result { } #[test] -fn in_repo_worktree() -> Result { +fn in_repo_worktree() -> TestResult { let root = fixture("in-repo-worktree"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( @@ -4862,7 +4893,7 @@ fn in_repo_worktree() -> Result { } #[test] -fn in_repo_hidden_worktree() -> Result { +fn in_repo_hidden_worktree() -> TestResult { let root = fixture("in-repo-hidden-worktree"); let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); assert_eq!( diff --git a/gix-dir/tests/dir_cwd.rs b/gix-dir/tests/dir_cwd.rs index 99ff187e76b..4d00d3d38dd 100644 --- a/gix-dir/tests/dir_cwd.rs +++ b/gix-dir/tests/dir_cwd.rs @@ -11,7 +11,7 @@ use crate::walk_utils::{collect, entryps, fixture, options}; pub mod walk_utils; #[test] -fn prefixes_work_as_expected() -> gix_testtools::Result { +fn prefixes_work_as_expected() -> gix_testtools::TestResult { let root = fixture("only-untracked"); let _cwd = gix_testtools::set_current_dir(root.join("d"))?; let troot = Path::new("..").join("d"); diff --git a/gix-discover/src/parse.rs b/gix-discover/src/parse.rs index e963585c0dd..9edec254a1b 100644 --- a/gix-discover/src/parse.rs +++ b/gix-discover/src/parse.rs @@ -15,7 +15,7 @@ pub fn gitdir(input: &[u8]) -> Result<PathBuf> { if path.is_empty() { bail!(validation("Format should be 'gitdir: <path>', but got").with_input(input)); } - Ok(gix_path::try_from_bstr(path) + Ok(gix_path::from_bstr(path) .or_raise(|| validation("Couldn't decode input as UTF8").with_input(input))? .into_owned()) } diff --git a/gix-discover/src/path.rs b/gix-discover/src/path.rs index b3ecbcb94fd..39fbb08af3f 100644 --- a/gix-discover/src/path.rs +++ b/gix-discover/src/path.rs @@ -92,7 +92,11 @@ pub fn repository_kind(git_dir: &Path) -> Option<RepositoryKind> { /// /// Empty or whitespace-only path files are invalid. pub fn from_plain_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> { - read_plain_file_content(path).map(|res| res.map(gix_path::from_bstring)) + read_plain_file_content(path).map(|res| { + res.and_then(|path| { + gix_path::from_bstring(path).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err)) + }) + }) } /// Reads a plain path from a file like [`from_plain_file()`], resolving relative paths against @@ -103,7 +107,8 @@ pub fn from_plain_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf pub fn from_plain_file_relative_to_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> { read_plain_file_content(path).map(|res| { res.and_then(|buf| { - let plain_path = gix_path::from_bstring(buf); + let plain_path = + gix_path::from_bstring(buf).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; if !plain_path.is_relative() { return Ok(plain_path); } diff --git a/gix-discover/src/repository.rs b/gix-discover/src/repository.rs index 76f6b7fc9b7..88664ba2e1f 100644 --- a/gix-discover/src/repository.rs +++ b/gix-discover/src/repository.rs @@ -64,7 +64,7 @@ mod trust_tests { use gix_sec::Trust; #[test] - fn known_candidate_trust_skips_only_identical_git_directory_spellings() -> gix_error::TestResult { + fn known_candidate_trust_skips_only_identical_git_directory_spellings() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_ownership_repos.sh")?; let cwd = std::env::current_dir()?; for name in ["worktree/.git", "bare"] { @@ -91,7 +91,7 @@ mod trust_tests { } #[test] - fn resolved_paths_constrain_trust_without_upgrading_the_candidate() -> gix_error::TestResult { + fn resolved_paths_constrain_trust_without_upgrading_the_candidate() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_linked_ownership_repo.sh")?; let cwd = std::env::current_dir()?; let candidate = root.join("linked/.git"); diff --git a/gix-discover/src/upwards/mod.rs b/gix-discover/src/upwards/mod.rs index bd4d6052307..af5de38891b 100644 --- a/gix-discover/src/upwards/mod.rs +++ b/gix-discover/src/upwards/mod.rs @@ -432,7 +432,7 @@ pub(crate) mod function { use super::{Options, Trust, TrustPolicy, discover_opts_with_trust}; #[test] - fn ownership_checks_skip_identical_candidate_and_git_directory() -> gix_error::TestResult { + fn ownership_checks_skip_identical_candidate_and_git_directory() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_ownership_repos.sh")?; let root = std::env::current_dir()?.join(root); for (name, bare) in [("worktree", false), ("bare", true)] { @@ -465,7 +465,7 @@ pub(crate) mod function { } #[test] - fn ownership_of_every_linked_repository_path_limits_trust() -> gix_testtools::Result { + fn ownership_of_every_linked_repository_path_limits_trust() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_linked_ownership_repo.sh")?; let main = root.join("main"); let worktree = root.join("linked"); diff --git a/gix-discover/src/upwards/types.rs b/gix-discover/src/upwards/types.rs index 8206bc6c1ff..3ca208b58fe 100644 --- a/gix-discover/src/upwards/types.rs +++ b/gix-discover/src/upwards/types.rs @@ -212,13 +212,13 @@ mod tests { #[test] #[cfg(unix)] - fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { + fn parse_ceiling_dirs_from_environment_format() -> gix_testtools::TestResult { use std::{fs, os::unix::fs::symlink}; use super::*; // Setup filesystem - let dir = tempfile::tempdir().expect("success creating temp dir"); + let dir = tempfile::tempdir()?; let direct_path = dir.path().join("direct"); let symlink_path = dir.path().join("symlink"); fs::create_dir(&direct_path)?; @@ -230,28 +230,25 @@ mod tests { let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str())); assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded"); - assert_eq!( - ceiling_dirs[0], - symlink_path.canonicalize().expect("symlink path exists"), - "Symlinks are resolved" - ); + assert_eq!(ceiling_dirs[0], symlink_path.canonicalize()?, "Symlinks are resolved"); assert_eq!( ceiling_dirs[1], symlink_path, "Symlink are not resolved after empty item" ); - dir.close() + dir.close()?; + Ok(()) } #[test] #[cfg(windows)] - fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { + fn parse_ceiling_dirs_from_environment_format() -> gix_testtools::TestResult { use std::{fs, os::windows::fs::symlink_dir}; use super::*; // Setup filesystem - let dir = tempfile::tempdir().expect("success creating temp dir"); + let dir = tempfile::tempdir()?; let direct_path = dir.path().join("direct"); let symlink_path = dir.path().join("symlink"); fs::create_dir(&direct_path)?; @@ -269,6 +266,7 @@ mod tests { "Symlink are not resolved after empty item" ); - dir.close() + dir.close()?; + Ok(()) } } diff --git a/gix-discover/tests/discover/is_git.rs b/gix-discover/tests/discover/is_git.rs index 60be812716b..b5c2d5380c6 100644 --- a/gix-discover/tests/discover/is_git.rs +++ b/gix-discover/tests/discover/is_git.rs @@ -1,9 +1,9 @@ -use crate::Result; use crate::upwards::repo_path; +use gix_testtools::TestResult; #[cfg(target_os = "macos")] #[test] -fn verify_on_exfat() -> Result<()> { +fn verify_on_exfat() -> TestResult { use std::process::Command; use gix_discover::repository::Kind; @@ -42,7 +42,7 @@ fn verify_on_exfat() -> Result<()> { } #[test] -fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> Result { +fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> TestResult { for name in ["bare-no-config-after-init.git", "bare-no-config.git"] { let repo = repo_path()?.join(name); let kind = gix_discover::is_git(&repo)?; @@ -52,7 +52,7 @@ fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> Result { } #[test] -fn bare_repo_with_index_file_looks_still_looks_like_bare() -> Result { +fn bare_repo_with_index_file_looks_still_looks_like_bare() -> TestResult { let repo = repo_path()?.join("bare-with-index.git"); let kind = gix_discover::is_git(&repo)?; assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare); @@ -60,7 +60,7 @@ fn bare_repo_with_index_file_looks_still_looks_like_bare() -> Result { } #[test] -fn non_bare_repo_without_workdir() -> Result { +fn non_bare_repo_without_workdir() -> TestResult { let repo = repo_path()?.join("non-bare-without-worktree"); let kind = gix_discover::is_git(&repo)?; assert_eq!( @@ -72,7 +72,7 @@ fn non_bare_repo_without_workdir() -> Result { } #[test] -fn non_bare_repo_without_workdir_with_index() -> Result { +fn non_bare_repo_without_workdir_with_index() -> TestResult { let repo = repo_path()?.join("non-bare-without-worktree-with-index"); let kind = gix_discover::is_git(&repo)?; assert_eq!( @@ -84,7 +84,7 @@ fn non_bare_repo_without_workdir_with_index() -> Result { } #[test] -fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() -> Result { +fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() -> TestResult { for repo_name in ["bare-with-index-bare", "bare-with-index-no-config-bare"] { let repo = repo_path()?.join(repo_name); let kind = gix_discover::is_git(&repo)?; @@ -94,7 +94,7 @@ fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() -> } #[test] -fn no_bare_repo_without_index_file_looks_like_worktree() -> Result { +fn no_bare_repo_without_index_file_looks_like_worktree() -> TestResult { let repo = repo_path()?.join("non-bare-without-index").join(".git"); let kind = gix_discover::is_git(&repo)?; assert_eq!(kind, gix_discover::repository::Kind::WorkTree { linked_git_dir: None }); @@ -102,7 +102,7 @@ fn no_bare_repo_without_index_file_looks_like_worktree() -> Result { } #[test] -fn non_bare_repo_with_git_extension_is_not_a_worktree() -> Result { +fn non_bare_repo_with_git_extension_is_not_a_worktree() -> TestResult { let worktree = repo_path()?.join("repo.git"); let err = gix_discover::is_git(&worktree).unwrap_err(); insta::assert_debug_snapshot!(err, "repo.git isn't a .git directory after all", @"Missing HEAD at \".git/HEAD\""); @@ -114,7 +114,7 @@ fn non_bare_repo_with_git_extension_is_not_a_worktree() -> Result { } #[test] -fn missing_configuration_file_is_not_a_dealbreaker_in_nonbare_repo() -> Result { +fn missing_configuration_file_is_not_a_dealbreaker_in_nonbare_repo() -> TestResult { for name in ["worktree-no-config-after-init/.git", "worktree-no-config/.git"] { let repo = repo_path()?.join(name); let kind = gix_discover::is_git(&repo)?; @@ -124,7 +124,7 @@ fn missing_configuration_file_is_not_a_dealbreaker_in_nonbare_repo() -> Result { } #[test] -fn split_worktree_using_configuration() -> Result { +fn split_worktree_using_configuration() -> TestResult { for name in [ "repo-with-worktree-in-config", "repo-with-worktree-in-config-unborn", @@ -145,7 +145,7 @@ fn split_worktree_using_configuration() -> Result { } #[test] -fn reftable() -> Result { +fn reftable() -> TestResult { let Some(root) = gix_testtools::scripted_fixture_read_only_with_git_version("make_reftable_repo.sh", |version| { version >= (2, 44, 0) })? diff --git a/gix-discover/tests/discover/main.rs b/gix-discover/tests/discover/main.rs index 350171876b1..ae430f7b4b4 100644 --- a/gix-discover/tests/discover/main.rs +++ b/gix-discover/tests/discover/main.rs @@ -1,5 +1,3 @@ -pub use gix_testtools::Result; - mod is_git; mod parse; mod path; diff --git a/gix-discover/tests/discover/parse.rs b/gix-discover/tests/discover/parse.rs index 65480951b73..7c982c779db 100644 --- a/gix-discover/tests/discover/parse.rs +++ b/gix-discover/tests/discover/parse.rs @@ -1,10 +1,10 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use gix_discover::parse; #[test] -fn valid() -> Result { +fn valid() -> TestResult { assert_eq!(parse::gitdir(b"gitdir: a")?, Path::new("a")); assert_eq!(parse::gitdir(b"gitdir: relative/path")?, Path::new("relative/path")); assert_eq!(parse::gitdir(b"gitdir: ./relative/path")?, Path::new("./relative/path")); diff --git a/gix-discover/tests/discover/path.rs b/gix-discover/tests/discover/path.rs index 25c4cfd07f8..accd0fd8e45 100644 --- a/gix-discover/tests/discover/path.rs +++ b/gix-discover/tests/discover/path.rs @@ -1,5 +1,5 @@ mod from_git_dir_file { - use crate::Result; + use gix_testtools::{Result, TestResult}; use std::{ io::Write, path::{Path, PathBuf}, @@ -9,7 +9,7 @@ mod from_git_dir_file { #[cfg(not(windows))] #[test] - fn absolute_path_unix() -> Result { + fn absolute_path_unix() -> TestResult { let (path, _) = write_and_read(b"gitdir: /absolute/path/.git")?; assert_eq!(path, Path::new("/absolute/path/.git")); Ok(()) @@ -17,7 +17,7 @@ mod from_git_dir_file { #[cfg(windows)] #[test] - fn absolute_path_windows() -> Result { + fn absolute_path_windows() -> TestResult { let (path, _) = write_and_read(b"gitdir: C:/absolute/path/.git")?; assert_eq!(path, Path::new("C:/absolute/path/.git")); @@ -27,7 +27,7 @@ mod from_git_dir_file { } #[test] - fn relative_path_is_made_absolute_relative_to_containing_dir() -> Result { + fn relative_path_is_made_absolute_relative_to_containing_dir() -> TestResult { let (path, gitdir_file) = write_and_read(b"gitdir: relative/path")?; assert_eq!(path, gitdir_file.parent().unwrap().join(Path::new("relative/path"))); Ok(()) @@ -46,19 +46,19 @@ mod from_git_dir_file { } mod from_plain_file_relative_to_file { - use crate::Result; use crate::path::plain_file_with_content; + use gix_testtools::{Result, TestResult}; use std::path::{Path, PathBuf}; #[test] - fn relative_path_is_made_absolute_relative_to_containing_dir() -> Result { + fn relative_path_is_made_absolute_relative_to_containing_dir() -> TestResult { let (path, plain_file) = write_and_read(b"relative/path\n")?; assert_eq!(path, plain_file.parent().unwrap().join(Path::new("relative/path"))); Ok(()) } #[test] - fn empty_or_whitespace_only_path_is_invalid() -> Result { + fn empty_or_whitespace_only_path_is_invalid() -> TestResult { let mut error_snapshots = Vec::new(); for content in [b"".as_slice(), b" \n".as_slice()] { let file = plain_file_with_content(content)?; @@ -102,11 +102,11 @@ mod from_plain_file_relative_to_file { } mod from_plain_file { - use crate::Result; use crate::path::plain_file_with_content; + use gix_testtools::TestResult; #[test] - fn empty_or_whitespace_only_path_is_invalid() -> Result { + fn empty_or_whitespace_only_path_is_invalid() -> TestResult { let mut error_snapshots = Vec::new(); for content in [b"".as_slice(), b" \n".as_slice()] { let file = plain_file_with_content(content)?; diff --git a/gix-discover/tests/discover/upwards/ceiling_dirs.rs b/gix-discover/tests/discover/upwards/ceiling_dirs.rs index 4c3eb1381e1..bea87ab1d0e 100644 --- a/gix-discover/tests/discover/upwards/ceiling_dirs.rs +++ b/gix-discover/tests/discover/upwards/ceiling_dirs.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use gix_discover::upwards::Options; @@ -16,7 +16,7 @@ fn assert_repo_is_current_workdir(path: gix_discover::repository::Path, work_dir } #[test] -fn git_dir_candidate_within_ceiling_allows_discovery() -> Result { +fn git_dir_candidate_within_ceiling_allows_discovery() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); let (repo_path, _trust) = gix_discover::upwards_opts( @@ -25,15 +25,14 @@ fn git_dir_candidate_within_ceiling_allows_discovery() -> Result { ceiling_dirs: vec![work_dir.clone()], ..Default::default() }, - ) - .expect("ceiling dir should allow us to discover the repo"); + )?; assert_repo_is_current_workdir(repo_path, &work_dir); Ok(()) } #[test] -fn ceiling_dir_is_ignored_if_we_are_standing_on_the_ceiling_and_no_match_is_required() -> Result { +fn ceiling_dir_is_ignored_if_we_are_standing_on_the_ceiling_and_no_match_is_required() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); // the ceiling dir is equal to the input dir, which itself doesn't contain a repository. @@ -46,15 +45,14 @@ fn ceiling_dir_is_ignored_if_we_are_standing_on_the_ceiling_and_no_match_is_requ match_ceiling_dir_or_error: false, ..Default::default() }, - ) - .expect("ceiling dir should be skipped"); + )?; assert_repo_is_current_workdir(repo_path, &work_dir); Ok(()) } #[test] -fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() -> Result { +fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); let err = gix_discover::upwards_opts( @@ -76,7 +74,7 @@ fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() } #[test] -fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> Result { +fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); @@ -104,7 +102,7 @@ fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> Result { } #[test] -fn no_matching_ceiling_dir_error_can_be_suppressed() -> Result { +fn no_matching_ceiling_dir_error_can_be_suppressed() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); let (repo_path, _trust) = gix_discover::upwards_opts( @@ -119,15 +117,14 @@ fn no_matching_ceiling_dir_error_can_be_suppressed() -> Result { ], ..Default::default() }, - ) - .expect("ceiling dir should allow us to discover the repo"); + )?; assert_repo_is_current_workdir(repo_path, &work_dir); Ok(()) } #[test] -fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> Result { +fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir"); let err = gix_discover::upwards_opts( @@ -145,7 +142,7 @@ fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> Result { } #[test] -fn ceiling_dirs_are_not_processed_differently_than_the_git_dir_candidate() -> Result { +fn ceiling_dirs_are_not_processed_differently_than_the_git_dir_candidate() -> TestResult { let work_dir = repo_path()?; let dir = work_dir.join("some/very/deeply/nested/subdir/../../../../../.."); let (repo_path, _trust) = gix_discover::upwards_opts( @@ -155,8 +152,7 @@ fn ceiling_dirs_are_not_processed_differently_than_the_git_dir_candidate() -> Re ceiling_dirs: vec![Path::new("./some").into()], ..Default::default() }, - ) - .expect("the repo can be discovered because the relative ceiling doesn't _look_ like it has something to do with the git dir candidate"); + )?; assert_ne!( &repo_path.as_ref().canonicalize()?, @@ -168,7 +164,7 @@ fn ceiling_dirs_are_not_processed_differently_than_the_git_dir_candidate() -> Re } #[test] -fn no_matching_ceiling_dirs_errors_by_default() -> Result { +fn no_matching_ceiling_dirs_errors_by_default() -> TestResult { let relative_work_dir = repo_path()?; let dir = relative_work_dir.join("some"); let res = gix_discover::upwards_opts( @@ -189,7 +185,7 @@ fn no_matching_ceiling_dirs_errors_by_default() -> Result { } #[test] -fn ceilings_are_adjusted_to_match_search_dir() -> Result { +fn ceilings_are_adjusted_to_match_search_dir() -> TestResult { let relative_work_dir = repo_path()?; let cwd = std::env::current_dir()?; let absolute_ceiling_dir = gix_path::realpath_opts(&relative_work_dir, &cwd, 8)?; @@ -219,7 +215,7 @@ fn ceilings_are_adjusted_to_match_search_dir() -> Result { #[test] #[cfg(unix)] -fn ceiling_dirs_limit_the_physical_symlink_target() -> Result { +fn ceiling_dirs_limit_the_physical_symlink_target() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let search_dir = root.join("lexical-parent/link/real-dir"); let err = gix_discover::upwards_opts( diff --git a/gix-discover/tests/discover/upwards/mod.rs b/gix-discover/tests/discover/upwards/mod.rs index f40d050c880..a81f6881267 100644 --- a/gix-discover/tests/discover/upwards/mod.rs +++ b/gix-discover/tests/discover/upwards/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::{Result, TestResult}; use std::path::PathBuf; use gix_discover::repository::Kind; @@ -87,7 +87,7 @@ fn discovery_error_variants_are_intrinsically_classified() { } #[test] -fn optional_repository_recovery_excludes_io_and_untrusted_candidates() -> Result { +fn optional_repository_recovery_excludes_io_and_untrusted_candidates() -> TestResult { use gix_discover::upwards::{Error, Options, TrustPolicy}; use gix_error::ErrorExt; @@ -157,7 +157,7 @@ fn optional_repository_recovery_excludes_io_and_untrusted_candidates() -> Result // macOS filesystems require valid UTF-8 names. #[cfg(all(unix, not(target_os = "macos")))] #[test] -fn discovery_errors_preserve_non_utf8_paths() -> Result { +fn discovery_errors_preserve_non_utf8_paths() -> TestResult { use std::os::unix::ffi::OsStrExt; let root = gix_testtools::tempfile::tempdir()?; @@ -182,7 +182,7 @@ fn discovery_errors_preserve_non_utf8_paths() -> Result { } #[test] -fn can_override_computed_trust() -> Result { +fn can_override_computed_trust() -> TestResult { let dir = repo_path()?.join("some/very/deeply/nested/subdir"); let overridden_trust = match expected_trust() { gix_sec::Trust::Full => gix_sec::Trust::Reduced, @@ -231,7 +231,7 @@ fn can_override_computed_trust() -> Result { } #[test] -fn from_bare_git_dir() -> Result { +fn from_bare_git_dir() -> TestResult { let dir = repo_path()?.join("bare.git"); let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned"); @@ -241,7 +241,7 @@ fn from_bare_git_dir() -> Result { } #[test] -fn from_bare_with_index() -> Result { +fn from_bare_with_index() -> TestResult { let dir = repo_path()?.join("bare-with-index.git"); let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned"); @@ -251,7 +251,7 @@ fn from_bare_with_index() -> Result { } #[test] -fn from_non_bare_without_index() -> Result { +fn from_non_bare_without_index() -> TestResult { let dir = repo_path()?.join("non-bare-without-index"); let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!(path.as_ref(), dir, "now we refer to a worktree"); @@ -261,7 +261,7 @@ fn from_non_bare_without_index() -> Result { } #[test] -fn from_non_bare_repo_with_git_extension() -> Result { +fn from_non_bare_repo_with_git_extension() -> TestResult { let dir = repo_path()?.join("repo.git"); let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!( @@ -275,7 +275,7 @@ fn from_non_bare_repo_with_git_extension() -> Result { } #[test] -fn from_bare_git_dir_without_config_file() -> Result { +fn from_bare_git_dir_without_config_file() -> TestResult { for name in ["bare-no-config.git", "bare-no-config-after-init.git"] { let dir = repo_path()?.join(name); let (path, trust) = gix_discover::upwards(&dir)?; @@ -287,7 +287,7 @@ fn from_bare_git_dir_without_config_file() -> Result { } #[test] -fn from_inside_bare_git_dir() -> Result { +fn from_inside_bare_git_dir() -> TestResult { let git_dir = repo_path()?.join("bare.git"); let dir = git_dir.join("objects"); let (path, trust) = gix_discover::upwards(&dir)?; @@ -302,7 +302,7 @@ fn from_inside_bare_git_dir() -> Result { } #[test] -fn from_git_dir() -> Result { +fn from_git_dir() -> TestResult { let dir = repo_path()?.join(".git"); let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None }); @@ -316,7 +316,7 @@ fn from_git_dir() -> Result { } #[test] -fn from_working_dir() -> Result { +fn from_working_dir() -> TestResult { let dir = repo_path()?; let (path, trust) = gix_discover::upwards(&dir)?; assert_eq!(path.as_ref(), dir, "a working tree dir yields the git dir"); @@ -326,7 +326,7 @@ fn from_working_dir() -> Result { } #[test] -fn from_working_dir_no_config() -> Result { +fn from_working_dir_no_config() -> TestResult { for name in ["worktree-no-config-after-init", "worktree-no-config"] { let dir = repo_path()?.join(name); let (path, trust) = gix_discover::upwards(&dir)?; @@ -338,7 +338,7 @@ fn from_working_dir_no_config() -> Result { } #[test] -fn from_nested_dir() -> Result { +fn from_nested_dir() -> TestResult { let working_dir = repo_path()?; let dir = working_dir.join("some/very/deeply/nested/subdir"); let (path, trust) = gix_discover::upwards(&dir)?; @@ -349,7 +349,7 @@ fn from_nested_dir() -> Result { } #[test] -fn an_invalid_dot_git_directory_does_not_skip_ancestors() -> Result { +fn an_invalid_dot_git_directory_does_not_skip_ancestors() -> TestResult { let repo = gix_path::realpath(repo_path()?)?; let start = repo.join("non-repo/.git"); @@ -364,7 +364,7 @@ fn an_invalid_dot_git_directory_does_not_skip_ancestors() -> Result { #[test] #[cfg(unix)] -fn from_symlinked_nested_dir_follows_target_ancestors() -> Result { +fn from_symlinked_nested_dir_follows_target_ancestors() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let link = root.join("lexical-parent/link"); @@ -385,7 +385,7 @@ fn from_symlinked_nested_dir_follows_target_ancestors() -> Result { #[test] #[cfg(unix)] -fn symlink_is_resolved_before_parent_component() -> Result { +fn symlink_is_resolved_before_parent_component() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; for path in [ @@ -406,7 +406,7 @@ fn symlink_is_resolved_before_parent_component() -> Result { #[test] #[cfg(unix)] -fn relative_symlinks_use_the_configured_current_dir() -> Result { +fn relative_symlinks_use_the_configured_current_dir() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let root = std::env::current_dir()?.join(root); @@ -435,7 +435,7 @@ fn relative_symlinks_use_the_configured_current_dir() -> Result { } #[test] -fn from_dir_with_dot_dot() -> Result { +fn from_dir_with_dot_dot() -> TestResult { let outer = gix_testtools::scripted_fixture_read_only("make_nested_repos.sh")?; let outer = std::env::current_dir()?.join(outer); let inner = outer.join("inner"); @@ -463,7 +463,7 @@ fn from_dir_with_dot_dot() -> Result { } #[test] -fn from_nested_dir_inside_a_git_dir() -> Result { +fn from_nested_dir_inside_a_git_dir() -> TestResult { let working_dir = repo_path()?; let dir = working_dir.join(".git").join("objects"); let (path, trust) = gix_discover::upwards(&dir)?; @@ -474,20 +474,20 @@ fn from_nested_dir_inside_a_git_dir() -> Result { } #[test] -fn from_non_existing_worktree() { - let top_level_repo = repo_path().unwrap(); - let (path, _trust) = gix_discover::upwards(&top_level_repo.join("worktrees/b-private-dir-deleted")).unwrap(); +fn from_non_existing_worktree() -> TestResult { + let top_level_repo = repo_path()?; + let (path, _trust) = gix_discover::upwards(&top_level_repo.join("worktrees/b-private-dir-deleted"))?; assert_eq!(path, gix_discover::repository::Path::WorkTree(top_level_repo.clone())); - let (path, _trust) = - gix_discover::upwards(&top_level_repo.join("worktrees/from-bare/d-private-dir-deleted")).unwrap(); + let (path, _trust) = gix_discover::upwards(&top_level_repo.join("worktrees/from-bare/d-private-dir-deleted"))?; assert_eq!(path, gix_discover::repository::Path::WorkTree(top_level_repo)); + Ok(()) } #[test] -fn from_existing_worktree_inside_dot_git() { - let top_level_repo = repo_path().unwrap(); - let (path, _trust) = gix_discover::upwards(&top_level_repo.join(".git/worktrees/a")).unwrap(); +fn from_existing_worktree_inside_dot_git() -> TestResult { + let top_level_repo = repo_path()?; + let (path, _trust) = gix_discover::upwards(&top_level_repo.join(".git/worktrees/a"))?; let suffix = std::path::Path::new(top_level_repo.file_name().unwrap()) .join("worktrees") .join("a"); @@ -495,6 +495,7 @@ fn from_existing_worktree_inside_dot_git() { matches!(path, gix_discover::repository::Path::LinkedWorkTree { work_dir, .. } if work_dir.ends_with(suffix)), "we can handle to start from within a (somewhat partial) worktree git dir" ); + Ok(()) } #[test] @@ -511,7 +512,7 @@ fn from_non_existing_worktree_inside_dot_git() { } #[test] -fn from_existing_worktree() -> Result { +fn from_existing_worktree() -> TestResult { let top_level_repo = repo_path()?; for (discover_path, expected_worktree_path, expected_git_dir) in [ (top_level_repo.join("worktrees/a"), "worktrees/a", ".git/worktrees/a"), @@ -527,7 +528,7 @@ fn from_existing_worktree() -> Result { assert_eq!(trust, expected_trust()); let (git_dir, worktree) = path.into_repository_and_work_tree_directories(); assert_eq!( - git_dir.strip_prefix(gix_path::realpath(&top_level_repo).unwrap()), + git_dir.strip_prefix(gix_path::realpath(&top_level_repo)?), Ok(std::path::Path::new(expected_git_dir)), "we don't skip over worktrees and discover their git dir (gitdir is absolute in file)" ); @@ -542,7 +543,7 @@ fn from_existing_worktree() -> Result { } #[test] -fn from_existing_worktree_with_relative_linking_files() -> Result { +fn from_existing_worktree_with_relative_linking_files() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?; let main = fixture.join("main"); let linked = fixture.join("linked"); @@ -579,7 +580,7 @@ fn from_existing_worktree_with_relative_linking_files() -> Result { #[test] #[cfg(unix)] -fn from_symlinked_worktree_with_relative_linking_files() -> Result { +fn from_symlinked_worktree_with_relative_linking_files() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?; let main = fixture.join("actual/main"); let linked_symlink = fixture.join("linked-symlink"); @@ -603,7 +604,7 @@ fn from_symlinked_worktree_with_relative_linking_files() -> Result { #[cfg(target_os = "macos")] #[test] -fn cross_fs() -> Result { +fn cross_fs() -> TestResult { use std::process::Command; use gix_discover::upwards::Options; @@ -671,8 +672,7 @@ fn cross_fs() -> Result { cross_fs: true, ..Default::default() }, - ) - .expect("the cross-fs option should allow us to discover the repo"); + )?; assert_eq!( repo_path @@ -687,9 +687,9 @@ fn cross_fs() -> Result { } #[test] -fn do_not_shorten_absolute_paths() -> Result { - let top_level_repo = repo_path()?.canonicalize().expect("repo path exists"); - let (repo_path, _trust) = gix_discover::upwards(&top_level_repo).expect("we can discover the repo"); +fn do_not_shorten_absolute_paths() -> TestResult { + let top_level_repo = repo_path()?.canonicalize()?; + let (repo_path, _trust) = gix_discover::upwards(&top_level_repo)?; match repo_path { gix_discover::repository::Path::WorkTree(work_dir) => { @@ -703,7 +703,7 @@ fn do_not_shorten_absolute_paths() -> Result { #[cfg(unix)] #[test] -fn preserves_symlinked_ancestor_of_absolute_paths() -> Result { +fn preserves_symlinked_ancestor_of_absolute_paths() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let root = std::env::current_dir()?.join(root); let worktree = root.join("linked-parent/repo"); @@ -737,8 +737,8 @@ fn preserves_symlinked_ancestor_of_absolute_paths() -> Result { } mod dot_git_only { - use crate::Result; use crate::upwards::repo_path; + use gix_testtools::TestResult; fn find_dot_git(base: impl AsRef<std::path::Path>) -> gix_discover::repository::Path { gix_discover::upwards_opts( @@ -762,7 +762,7 @@ mod dot_git_only { } #[test] - fn succeeds_in_worktree_dir() -> Result { + fn succeeds_in_worktree_dir() -> TestResult { let top_level_repo = repo_path()?; for base in [ top_level_repo.join("some/very/deeply/nested/subdir"), @@ -775,7 +775,7 @@ mod dot_git_only { } #[test] - fn succeeds_from_within_dot_git_dir() -> Result { + fn succeeds_from_within_dot_git_dir() -> TestResult { let top_level_repo = repo_path()?; for inside_git_dir in [top_level_repo.join(".git"), top_level_repo.join(".git").join("refs")] { let repo_path = find_dot_git(inside_git_dir); @@ -785,7 +785,7 @@ mod dot_git_only { } #[test] - fn bare_repos_are_ignored() -> Result { + fn bare_repos_are_ignored() -> TestResult { let top_level_repo = repo_path()?; for bare_dir in [ top_level_repo.join("bare.git"), @@ -799,10 +799,10 @@ mod dot_git_only { } mod submodules { - use crate::Result; + use gix_testtools::TestResult; #[test] - fn by_their_worktree_checkout() -> Result { + fn by_their_worktree_checkout() -> TestResult { let dir = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?; let parent = dir.join("with-submodules"); let modules = parent.join(".git").join("modules"); @@ -825,7 +825,7 @@ mod submodules { } #[test] - fn by_their_module_git_dir() -> Result { + fn by_their_module_git_dir() -> TestResult { let dir = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?; let modules = dir.join("with-submodules").join(".git").join("modules"); for module in ["m1", "dir/m1"] { diff --git a/gix-discover/tests/isolated.rs b/gix-discover/tests/isolated.rs index 3d1ea50aae7..20d55d00cb2 100644 --- a/gix-discover/tests/isolated.rs +++ b/gix-discover/tests/isolated.rs @@ -5,7 +5,7 @@ use serial_test::serial; #[test] #[serial] -fn in_cwd_upwards_from_nested_dir() -> gix_testtools::Result { +fn in_cwd_upwards_from_nested_dir() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo)?; @@ -23,7 +23,7 @@ fn in_cwd_upwards_from_nested_dir() -> gix_testtools::Result { #[test] #[serial] #[cfg(unix)] -fn upwards_from_relative_symlink_paths() -> gix_testtools::Result { +fn upwards_from_relative_symlink_paths() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo.join("lexical-parent"))?; @@ -43,7 +43,7 @@ fn upwards_from_relative_symlink_paths() -> gix_testtools::Result { #[test] #[serial] #[cfg(unix)] -fn relative_paths_retain_a_symlinked_ancestor() -> gix_testtools::Result { +fn relative_paths_retain_a_symlinked_ancestor() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_symlinked_nested_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo)?; @@ -56,7 +56,7 @@ fn relative_paths_retain_a_symlinked_ancestor() -> gix_testtools::Result { #[test] #[serial] -fn upwards_bare_repo_with_index() -> gix_testtools::Result { +fn upwards_bare_repo_with_index() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo.join("bare-with-index.git"))?; @@ -71,7 +71,7 @@ fn upwards_bare_repo_with_index() -> gix_testtools::Result { #[test] #[serial] -fn in_cwd_upwards_bare_repo_without_index() -> gix_testtools::Result { +fn in_cwd_upwards_bare_repo_without_index() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo.join("bare.git"))?; @@ -82,7 +82,7 @@ fn in_cwd_upwards_bare_repo_without_index() -> gix_testtools::Result { #[test] #[serial] -fn in_cwd_upwards_nonbare_repo_without_index() -> gix_testtools::Result { +fn in_cwd_upwards_nonbare_repo_without_index() -> gix_testtools::TestResult { let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; let _keep = gix_testtools::set_current_dir(repo.join("non-bare-without-index"))?; @@ -96,7 +96,7 @@ fn in_cwd_upwards_nonbare_repo_without_index() -> gix_testtools::Result { #[test] #[serial] -fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Result { +fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; @@ -119,12 +119,10 @@ fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Re ceiling_dirs: vec![ceiling_dir], ..Default::default() }, - ) - .expect("ceiling dir should allow us to discover the repo"); + )?; assert_repo_is_current_workdir(repo_path, Path::new("..")); - let (repo_path, _trust) = - gix_discover::upwards_opts(search_dir, Default::default()).expect("without ceiling dir we see the same"); + let (repo_path, _trust) = gix_discover::upwards_opts(search_dir, Default::default())?; assert_repo_is_current_workdir(repo_path, Path::new("..")); let (repo_path, _trust) = gix_discover::upwards_opts( @@ -133,8 +131,7 @@ fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Re ceiling_dirs: vec![PathBuf::from("..")], ..Default::default() }, - ) - .expect("purely relative ceiling dirs work as well"); + )?; assert_repo_is_current_workdir(repo_path, Path::new("..")); let err = gix_discover::upwards_opts( @@ -170,11 +167,11 @@ fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Re #[test] #[serial] -fn unc_paths_are_handled_on_windows() -> gix_testtools::Result { - let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh").unwrap(); +fn unc_paths_are_handled_on_windows() -> gix_testtools::TestResult { + let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; - let _keep = gix_testtools::set_current_dir(repo.join("some/very/deeply/nested/subdir")).unwrap(); - let cwd = std::env::current_dir().unwrap(); + let _keep = gix_testtools::set_current_dir(repo.join("some/very/deeply/nested/subdir"))?; + let cwd = std::env::current_dir()?; let parent = cwd.parent().unwrap(); // all discoveries should fail, as they'll hit `parent` before finding a git repository. @@ -189,7 +186,7 @@ fn unc_paths_are_handled_on_windows() -> gix_testtools::Result { ); assert!(res.is_err(), "{res:?}"); - let parent = parent.canonicalize().unwrap(); + let parent = parent.canonicalize()?; // dir: normal, ceiling: extended let res = gix_discover::upwards_opts( &cwd, @@ -201,7 +198,7 @@ fn unc_paths_are_handled_on_windows() -> gix_testtools::Result { ); assert!(res.is_err(), "{res:?}"); - let cwd = cwd.canonicalize().unwrap(); + let cwd = cwd.canonicalize()?; let parent = cwd.parent().unwrap(); // dir: extended, ceiling: normal @@ -215,7 +212,7 @@ fn unc_paths_are_handled_on_windows() -> gix_testtools::Result { ); assert!(res.is_err(), "{res:?}"); - let parent = parent.canonicalize().unwrap(); + let parent = parent.canonicalize()?; // dir: extended, ceiling: extended let res = gix_discover::upwards_opts( &cwd, diff --git a/gix-features/tests/parallel/in_order_iter.rs b/gix-features/tests/parallel/in_order_iter.rs index a51fc64b692..6378d59bc30 100644 --- a/gix-features/tests/parallel/in_order_iter.rs +++ b/gix-features/tests/parallel/in_order_iter.rs @@ -3,17 +3,17 @@ use std::convert::Infallible; use gix_features::parallel::InOrderIter; #[test] -fn in_order_stays_in_order() { +fn in_order_stays_in_order() -> gix_testtools::TestResult { assert_eq!( InOrderIter::from(vec![Ok::<_, Infallible>((0usize, 'a')), Ok((1, 'b')), Ok((2, 'c'))].into_iter()) - .collect::<Result<Vec<_>, _>>() - .expect("infallible"), + .collect::<Result<Vec<_>, _>>()?, vec!['a', 'b', 'c'] ); + Ok(()) } #[test] -fn out_of_order_items_are_held_until_the_sequence_is_complete() { +fn out_of_order_items_are_held_until_the_sequence_is_complete() -> gix_testtools::TestResult { assert_eq!( InOrderIter::from( vec![ @@ -24,10 +24,10 @@ fn out_of_order_items_are_held_until_the_sequence_is_complete() { ] .into_iter() ) - .collect::<Result<Vec<_>, _>>() - .expect("infallible"), + .collect::<Result<Vec<_>, _>>()?, vec!['a', 'b', 'c', 'd'] ); + Ok(()) } #[test] diff --git a/gix-features/tests/pipe.rs b/gix-features/tests/pipe.rs index 74384b48dff..7c08fe8d001 100644 --- a/gix-features/tests/pipe.rs +++ b/gix-features/tests/pipe.rs @@ -4,7 +4,7 @@ mod io { use gix_features::io; #[test] - fn threaded_read_to_end() { + fn threaded_read_to_end() -> gix_testtools::TestResult { let (mut writer, mut reader) = gix_features::io::pipe::unidirectional(0); let message = "Hello, world!"; @@ -15,9 +15,10 @@ mod io { }); let mut received = String::new(); - reader.read_to_string(&mut received).unwrap(); + reader.read_to_string(&mut received)?; assert_eq!(&received, message); + Ok(()) } #[test] @@ -33,38 +34,34 @@ mod io { assert_eq!(err.kind(), ErrorKind::BrokenPipe); } #[test] - fn line_reading_one_by_one() { + fn line_reading_one_by_one() -> gix_testtools::TestResult { let (mut writer, mut reader) = io::pipe::unidirectional(2); - writer.write_all(b"a\n").expect("success"); - writer.write_all(b"b\nc").expect("success"); + writer.write_all(b"a\n")?; + writer.write_all(b"b\nc")?; drop(writer); let mut buf = String::new(); for expected in &["a\n", "b\n", "c"] { buf.clear(); - assert_eq!(reader.read_line(&mut buf).expect("success"), expected.len()); + assert_eq!(reader.read_line(&mut buf)?, expected.len()); assert_eq!(buf, *expected); } + Ok(()) } #[test] - fn line_reading() { + fn line_reading() -> gix_testtools::TestResult { let (mut writer, reader) = io::pipe::unidirectional(2); - writer.write_all(b"a\n").expect("success"); - writer.write_all(b"b\nc\n").expect("success"); + writer.write_all(b"a\n")?; + writer.write_all(b"b\nc\n")?; drop(writer); - assert_eq!( - reader.lines().map_while(Result::ok).collect::<Vec<_>>(), - vec!["a", "b", "c"] - ); + assert_eq!(reader.lines().collect::<Result<Vec<_>, _>>()?, vec!["a", "b", "c"]); + Ok(()) } #[test] - fn writer_can_inject_errors() { + fn writer_can_inject_errors() -> gix_testtools::TestResult { let (writer, mut reader) = io::pipe::unidirectional(1); - writer - .channel - .send(Err(std::io::Error::other("the error"))) - .expect("send success"); + writer.channel.send(Err(std::io::Error::other("the error")))?; let mut buf = [0]; insta::assert_debug_snapshot!(reader.read(&mut buf).expect_err("using Read trait, errors are propagated"), "using Read trait, errors are propagated", @r#" Custom { @@ -73,29 +70,26 @@ mod io { } "#); - writer - .channel - .send(Err(std::io::Error::other("the error"))) - .expect("send success"); + writer.channel.send(Err(std::io::Error::other("the error")))?; insta::assert_debug_snapshot!(reader.fill_buf().expect_err("using BufRead trait, errors are propagated"), "using BufRead trait, errors are propagated", @r#" Custom { kind: Other, error: "the error", } "#); + Ok(()) } #[test] - fn continue_on_empty_writes() { + fn continue_on_empty_writes() -> gix_testtools::TestResult { let (mut writer, mut reader) = io::pipe::unidirectional(2); - writer.write_all(&[]).expect("write successful and non-blocking"); + writer.write_all(&[])?; let input = b"hello"; - writer - .write_all(input) - .expect("second write works as well as there is capacity"); + writer.write_all(input)?; let mut buf = vec![0u8; input.len()]; - assert_eq!(reader.read(&mut buf).expect("read succeeds"), input.len()); + assert_eq!(reader.read(&mut buf)?, input.len()); assert_eq!(buf, &input[..]); + Ok(()) } #[test] diff --git a/gix-filter/src/driver/init.rs b/gix-filter/src/driver/init.rs index da3b4148adb..7a898300751 100644 --- a/gix-filter/src/driver/init.rs +++ b/gix-filter/src/driver/init.rs @@ -73,13 +73,13 @@ impl State { } fn spawn_driver(cmd: BString, context: &gix_command::Context) -> Result<(std::process::Child, std::process::Command)> { - let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstr(cmd).into_owned()) + let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstr(cmd)?.into_owned()) .command_may_be_shell_script() .with_context(context.clone()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) - .into(); + .try_into()?; gix_trace::debug!(cmd = ?cmd, "launching filter driver"); use gix_error::ResultExt; let child = cmd diff --git a/gix-filter/src/pipeline/convert.rs b/gix-filter/src/pipeline/convert.rs index e631e0c0809..0eb3e9e6839 100644 --- a/gix-filter/src/pipeline/convert.rs +++ b/gix-filter/src/pipeline/convert.rs @@ -55,7 +55,7 @@ impl Pipeline { { use gix_error::{ResultExt, message}; - let bstr_rela_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)); + let bstr_rela_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)?); let Configuration { driver, digest, diff --git a/gix-filter/tests/filter/driver.rs b/gix-filter/tests/filter/driver.rs index 0a8ef903a6f..f92002256d3 100644 --- a/gix-filter/tests/filter/driver.rs +++ b/gix-filter/tests/filter/driver.rs @@ -1,11 +1,11 @@ use bstr::{BStr, BString}; mod baseline { - use crate::Result; use crate::driver::driver_path; + use gix_testtools::TestResult; #[test] - fn our_implementation_used_by_git() -> Result { + fn our_implementation_used_by_git() -> TestResult { let exe = driver_path().to_string(); gix_testtools::scripted_fixture_read_only_with_args_single_archive("baseline.sh", [exe])?; Ok(()) @@ -204,7 +204,7 @@ mod shutdown { } #[test] - fn explicit_shutdown_waits_for_processes() -> Result { + fn explicit_shutdown_waits_for_processes() -> gix_testtools::TestResult { let mut state = state_with_waiting_process()?; let start = std::time::Instant::now(); @@ -231,7 +231,7 @@ mod shutdown { } #[test] - fn unsuccessful_process_exit_can_be_turned_into_an_error() -> Result { + fn unsuccessful_process_exit_can_be_turned_into_an_error() -> gix_testtools::TestResult { let mut state = gix_filter::driver::State::default(); let mut driver = driver_with_process(); driver @@ -262,7 +262,7 @@ mod shutdown { } #[test] - fn drop_waits_for_processes() -> Result { + fn drop_waits_for_processes() -> gix_testtools::TestResult { let state = state_with_waiting_process()?; let start = std::time::Instant::now(); @@ -276,7 +276,7 @@ mod shutdown { } pub(crate) mod apply { - use crate::Result; + use gix_testtools::TestResult; use std::{io::Read, sync::LazyLock}; use crate::driver::{driver_path, shutdown::extract_client}; @@ -308,7 +308,7 @@ pub(crate) mod apply { } #[test] - fn missing_driver_means_no_filter_is_applied() -> Result { + fn missing_driver_means_no_filter_is_applied() -> TestResult { let mut state = gix_filter::driver::State::default(); let mut driver = driver_no_process(); driver.smudge = None; @@ -338,7 +338,7 @@ pub(crate) mod apply { } #[test] - fn a_crashing_process_can_restart_it() -> Result { + fn a_crashing_process_can_restart_it() -> TestResult { let mut state = gix_filter::driver::State::default(); let driver = driver_with_process(); let err = match state.apply( @@ -378,8 +378,7 @@ pub(crate) mod apply { &mut std::io::empty(), Operation::Smudge, context_from_path("fine"), - ) - .expect("process restarts fine") + )? .expect("filter applied"); let mut buf = Vec::new(); filtered.read_to_end(&mut buf)?; @@ -388,7 +387,7 @@ pub(crate) mod apply { } #[test] - fn process_status_abort_disables_capability() -> Result { + fn process_status_abort_disables_capability() -> TestResult { let mut state = gix_filter::driver::State::default(); let driver = driver_with_process(); let client = extract_client(state.maybe_launch_process(&driver, Operation::Clean, "does not matter".into())?); @@ -427,7 +426,7 @@ pub(crate) mod apply { } #[test] - fn process_status_strange_shuts_down_process() -> Result { + fn process_status_strange_shuts_down_process() -> TestResult { let mut state = gix_filter::driver::State::default(); let driver = driver_with_process(); let client = extract_client(state.maybe_launch_process(&driver, Operation::Clean, "does not matter".into())?); @@ -461,7 +460,7 @@ pub(crate) mod apply { } #[test] - fn smudge_and_clean_failure_is_translated_to_observable_error_for_required_drivers() -> Result { + fn smudge_and_clean_failure_is_translated_to_observable_error_for_required_drivers() -> TestResult { let mut state = gix_filter::driver::State::default(); let driver = driver_no_process(); assert!(driver.required); @@ -501,7 +500,7 @@ pub(crate) mod apply { } #[test] - fn smudge_and_clean_failure_falls_back_to_input_if_required_is_false() -> Result { + fn smudge_and_clean_failure_falls_back_to_input_if_required_is_false() -> TestResult { let mut state = gix_filter::driver::State::default(); let mut driver = driver_no_process(); driver.required = false; @@ -526,7 +525,7 @@ pub(crate) mod apply { } #[test] - fn successful_non_required_driver_can_close_stdin_early() -> Result { + fn successful_non_required_driver_can_close_stdin_early() -> TestResult { let mut state = gix_filter::driver::State::default(); let mut driver = driver_no_process(); driver.required = false; @@ -556,7 +555,7 @@ pub(crate) mod apply { } #[test] - fn smudge_and_clean_series() -> Result { + fn smudge_and_clean_series() -> TestResult { let mut state = gix_filter::driver::State::default(); for mut driver in [driver_no_process(), driver_with_process()] { assert!( @@ -610,7 +609,7 @@ pub(crate) mod apply { } #[test] - fn smudge_and_clean_delayed() -> Result { + fn smudge_and_clean_delayed() -> TestResult { let mut state = gix_filter::driver::State::default(); let driver = driver_with_process(); let input = "hello\nthere\n"; @@ -679,7 +678,7 @@ pub(crate) mod apply { } #[test] - fn delaying_without_permission_is_corruption() -> gix_error::TestResult { + fn delaying_without_permission_is_corruption() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -713,7 +712,7 @@ pub(crate) mod apply { } #[test] - fn large_file_with_cat_filter_does_not_hang() -> Result { + fn large_file_with_cat_filter_does_not_hang() -> TestResult { // This test reproduces issue #2080 where using `cat` as a filter with a large file // causes a deadlock. The pipe buffer is typically 64KB on Linux, so we use files // larger than that to ensure the buffer fills up. @@ -765,7 +764,7 @@ pub(crate) mod apply { } #[test] - fn large_file_with_cat_filter_early_drop() -> Result { + fn large_file_with_cat_filter_early_drop() -> TestResult { // Test that dropping the reader early doesn't cause issues (thread cleanup) let mut state = gix_filter::driver::State::default(); diff --git a/gix-filter/tests/filter/eol/convert_to_git.rs b/gix-filter/tests/filter/eol/convert_to_git.rs index 286cfa7c282..871fc04a2e1 100644 --- a/gix-filter/tests/filter/eol/convert_to_git.rs +++ b/gix-filter/tests/filter/eol/convert_to_git.rs @@ -1,11 +1,11 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use bstr::{ByteSlice, ByteVec}; use gix_filter::{eol, eol::AttributesDigest}; #[test] -fn with_binary_attribute_is_never_converted() { +fn with_binary_attribute_is_never_converted() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"hi\r\nho", @@ -13,13 +13,13 @@ fn with_binary_attribute_is_never_converted() { &mut buf, &mut no_call, Default::default(), - ) - .expect("no error"); + )?; assert!(!changed, "the user marked it as binary so it's never being touched"); + Ok(()) } #[test] -fn no_crlf_means_no_work() -> Result { +fn no_crlf_means_no_work() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"hi", @@ -27,8 +27,7 @@ fn no_crlf_means_no_work() -> Result { &mut buf, &mut no_call, Default::default(), - ) - .expect("no error"); + )?; assert!(!changed); let changed = eol::convert_to_git( @@ -37,14 +36,13 @@ fn no_crlf_means_no_work() -> Result { &mut buf, &mut no_object_in_index, Default::default(), - ) - .expect("no error"); + )?; assert!(!changed, "in auto-mode, the object is queried in the index as well."); Ok(()) } #[test] -fn detected_as_binary() -> Result { +fn detected_as_binary() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"hi\0zero makes it binary", @@ -52,8 +50,7 @@ fn detected_as_binary() -> Result { &mut buf, &mut no_call, Default::default(), - ) - .expect("no error"); + )?; assert!( !changed, "in auto-mode, we have a heuristic to see if the buffer is binary" @@ -62,7 +59,7 @@ fn detected_as_binary() -> Result { } #[test] -fn trailing_dos_eof_marker_is_not_detected_as_binary() -> Result { +fn trailing_dos_eof_marker_is_not_detected_as_binary() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"a\r\nb\r\n\x1a", @@ -70,8 +67,7 @@ fn trailing_dos_eof_marker_is_not_detected_as_binary() -> Result { &mut buf, &mut no_object_in_index, Default::default(), - ) - .expect("no error"); + )?; assert!(changed, "the DOS EOF marker doesn't stand in the way of conversion"); assert_eq!(buf.as_bstr(), "a\nb\n\x1a"); @@ -81,14 +77,13 @@ fn trailing_dos_eof_marker_is_not_detected_as_binary() -> Result { &mut buf, &mut no_object_in_index, Default::default(), - ) - .expect("no error"); + )?; assert!(!changed, "a second marker isn't discounted, so this is binary"); Ok(()) } #[test] -fn fast_conversion_by_stripping_cr() -> Result { +fn fast_conversion_by_stripping_cr() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"a\r\nb\r\nc", @@ -96,15 +91,14 @@ fn fast_conversion_by_stripping_cr() -> Result { &mut buf, &mut no_call, Default::default(), - ) - .expect("no error"); + )?; assert!(changed); assert_eq!(buf.as_bstr(), "a\nb\nc", "here carriage returns can just be stripped"); Ok(()) } #[test] -fn slower_conversion_due_to_lone_cr() -> Result { +fn slower_conversion_due_to_lone_cr() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_git( b"\r\ra\r\nb\r\nc", @@ -112,8 +106,7 @@ fn slower_conversion_due_to_lone_cr() -> Result { &mut buf, &mut no_call, Default::default(), - ) - .expect("no error"); + )?; assert!(changed); assert_eq!( buf.as_bstr(), @@ -124,7 +117,7 @@ fn slower_conversion_due_to_lone_cr() -> Result { } #[test] -fn crlf_in_index_prevents_conversion_to_lf() -> Result { +fn crlf_in_index_prevents_conversion_to_lf() -> TestResult { let mut buf = Vec::new(); let mut called = false; let changed = eol::convert_to_git( @@ -138,8 +131,7 @@ fn crlf_in_index_prevents_conversion_to_lf() -> Result { Ok(Some(())) }, Default::default(), - ) - .expect("no error"); + )?; assert!(called, "in auto mode, the index is queried as well"); assert!( !changed, @@ -149,7 +141,7 @@ fn crlf_in_index_prevents_conversion_to_lf() -> Result { } #[test] -fn round_trip_check() -> Result { +fn round_trip_check() -> TestResult { let mut error_snapshots = Vec::new(); let mut buf = Vec::new(); for (input, digest) in [ @@ -205,7 +197,7 @@ fn round_trip_check() -> Result { } #[test] -fn index_object_errors_keep_their_native_cause() -> gix_error::TestResult { +fn index_object_errors_keep_their_native_cause() -> TestResult { use gix_error::ErrorExt; let err = eol::convert_to_git( diff --git a/gix-filter/tests/filter/eol/convert_to_worktree.rs b/gix-filter/tests/filter/eol/convert_to_worktree.rs index 1a6cd270886..c8a8f35dd12 100644 --- a/gix-filter/tests/filter/eol/convert_to_worktree.rs +++ b/gix-filter/tests/filter/eol/convert_to_worktree.rs @@ -1,12 +1,12 @@ -use crate::Result; use bstr::ByteSlice; use gix_filter::{ eol, eol::{AttributesDigest, Configuration, Mode}, }; +use gix_testtools::TestResult; #[test] -fn no_conversion_if_attribute_digest_does_not_allow_it() -> Result { +fn no_conversion_if_attribute_digest_does_not_allow_it() -> TestResult { let mut buf = Vec::new(); for digest in [ AttributesDigest::Binary, @@ -20,7 +20,7 @@ fn no_conversion_if_attribute_digest_does_not_allow_it() -> Result { } #[test] -fn no_conversion_if_configuration_does_not_allow_it() -> Result { +fn no_conversion_if_configuration_does_not_allow_it() -> TestResult { let mut buf = Vec::new(); for digest in [AttributesDigest::Text, AttributesDigest::TextAuto] { for config in [ @@ -41,7 +41,7 @@ fn no_conversion_if_configuration_does_not_allow_it() -> Result { } #[test] -fn no_conversion_if_nothing_to_do() -> Result { +fn no_conversion_if_nothing_to_do() -> TestResult { let mut buf = Vec::new(); for (input, digest, msg) in [ ( @@ -72,7 +72,7 @@ fn no_conversion_if_nothing_to_do() -> Result { } #[test] -fn each_nl_is_replaced_with_crnl() -> Result { +fn each_nl_is_replaced_with_crnl() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_worktree( b"hi\n\nho\nend", @@ -89,7 +89,7 @@ fn each_nl_is_replaced_with_crnl() -> Result { } #[test] -fn trailing_dos_eof_marker_is_not_detected_as_binary() -> Result { +fn trailing_dos_eof_marker_is_not_detected_as_binary() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_worktree( b"a\nb\n\x1a", @@ -111,7 +111,7 @@ fn trailing_dos_eof_marker_is_not_detected_as_binary() -> Result { } #[test] -fn existing_crnl_are_not_replaced_for_safety_nor_are_lone_cr() -> Result { +fn existing_crnl_are_not_replaced_for_safety_nor_are_lone_cr() -> TestResult { let mut buf = Vec::new(); let changed = eol::convert_to_worktree( b"hi\r\n\nho\r\nend\r", diff --git a/gix-filter/tests/filter/ident.rs b/gix-filter/tests/filter/ident.rs index 575a52c6447..12b263d88f8 100644 --- a/gix-filter/tests/filter/ident.rs +++ b/gix-filter/tests/filter/ident.rs @@ -1,9 +1,9 @@ mod undo { - use crate::Result; use bstr::{B, ByteSlice}; + use gix_testtools::TestResult; #[test] - fn no_id_changes_nothing() -> Result { + fn no_id_changes_nothing() -> TestResult { let mut buf = Vec::new(); let changed = gix_filter::ident::undo(B("hello"), &mut buf)?; assert!(!changed, "the buffer is not touched"); @@ -12,7 +12,7 @@ mod undo { } #[test] - fn empty() -> Result { + fn empty() -> TestResult { let mut buf = Vec::new(); assert!( !gix_filter::ident::undo(B(""), &mut buf)?, @@ -22,7 +22,7 @@ mod undo { } #[test] - fn nothing_if_newline_between_dollars() -> Result { + fn nothing_if_newline_between_dollars() -> TestResult { let mut buf = Vec::new(); assert!(!gix_filter::ident::undo(B(" $Id: \n$"), &mut buf)?); assert_eq!(buf.len(), 0); @@ -30,7 +30,7 @@ mod undo { } #[test] - fn nothing_if_it_is_not_id() -> Result { + fn nothing_if_it_is_not_id() -> TestResult { let mut buf = Vec::new(); assert!( !gix_filter::ident::undo(B(" $id: something$"), &mut buf)?, @@ -41,7 +41,7 @@ mod undo { } #[test] - fn anything_between_dollar_id_dollar() -> Result { + fn anything_between_dollar_id_dollar() -> TestResult { let mut buf = Vec::new(); assert!(gix_filter::ident::undo(B(" $Id: something$\nhello"), &mut buf)?); assert_eq!(buf.as_bstr(), " $Id$\nhello"); @@ -49,7 +49,7 @@ mod undo { } #[test] - fn multiple() -> Result { + fn multiple() -> TestResult { let mut buf = Vec::new(); assert!(gix_filter::ident::undo( B("$Id: a\n$ $Id: something$\nhello$Id: hex$\nlast $Id:other$\n$Id: \n$"), @@ -67,12 +67,12 @@ mod undo { } mod apply { - use crate::Result; use bstr::{B, ByteSlice}; use gix_filter::ident; + use gix_testtools::TestResult; #[test] - fn no_change() -> Result { + fn no_change() -> TestResult { let mut buf = Vec::new(); for input_no_match in [ "", @@ -88,7 +88,7 @@ mod apply { } #[test] - fn simple() -> Result { + fn simple() -> TestResult { let mut buf = Vec::new(); assert!( ident::apply(B("$Id$"), gix_testtools::object_hash(), &mut buf)?, @@ -119,7 +119,7 @@ mod apply { } #[test] - fn round_trips() -> Result { + fn round_trips() -> TestResult { let mut buf = Vec::new(); for input in [ "hi\n$Id$\nho\n\t$Id$$Id$$Id$", diff --git a/gix-filter/tests/filter/pipeline/convert_to_git.rs b/gix-filter/tests/filter/pipeline/convert_to_git.rs index 2fc54c0fa22..8672a5a8298 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_git.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_git.rs @@ -6,7 +6,7 @@ use gix_filter::{eol, pipeline::CrlfRoundTripCheck}; use crate::{driver::apply::driver_with_process, pipeline::pipeline}; #[test] -fn no_driver_but_filter_with_autocrlf() -> gix_testtools::Result { +fn no_driver_but_filter_with_autocrlf() -> gix_testtools::TestResult { let (_cache, mut pipe) = pipeline("no-filter", || { ( vec![], @@ -38,7 +38,7 @@ fn no_driver_but_filter_with_autocrlf() -> gix_testtools::Result { } #[test] -fn all_stages_mean_streaming_is_impossible() -> gix_testtools::Result { +fn all_stages_mean_streaming_is_impossible() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("all-filters", || { ( vec![driver_with_process()], @@ -73,7 +73,7 @@ fn all_stages_mean_streaming_is_impossible() -> gix_testtools::Result { } #[test] -fn only_driver_means_streaming_is_possible() -> gix_testtools::Result { +fn only_driver_means_streaming_is_possible() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("driver-only", || { ( vec![driver_with_process()], @@ -113,7 +113,7 @@ fn only_driver_means_streaming_is_possible() -> gix_testtools::Result { } #[test] -fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::Result { +fn no_filter_means_reader_is_returned_unchanged() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("no-filters", || { (vec![], Vec::new(), CrlfRoundTripCheck::Fail, Default::default()) })?; diff --git a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs index ed6168ca9f9..0c653f84f18 100644 --- a/gix-filter/tests/filter/pipeline/convert_to_worktree.rs +++ b/gix-filter/tests/filter/pipeline/convert_to_worktree.rs @@ -6,7 +6,7 @@ use gix_filter::pipeline::{CrlfRoundTripCheck, convert::to_worktree}; use crate::{driver::apply::driver_with_process, pipeline::pipeline}; #[test] -fn all_stages() -> gix_testtools::Result { +fn all_stages() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("all-filters", || { ( vec![driver_with_process()], @@ -52,7 +52,7 @@ fn all_stages() -> gix_testtools::Result { } #[test] -fn all_stages_no_filter() -> gix_testtools::Result { +fn all_stages_no_filter() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("all-filters", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; @@ -91,7 +91,7 @@ fn all_stages_no_filter() -> gix_testtools::Result { } #[test] -fn no_filter() -> gix_testtools::Result { +fn no_filter() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("no-filters", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; @@ -119,7 +119,7 @@ fn no_filter() -> gix_testtools::Result { } #[test] -fn unknown_encoding_is_ignored_after_other_conversions() -> gix_testtools::Result { +fn unknown_encoding_is_ignored_after_other_conversions() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("unknown-encoding", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; @@ -146,7 +146,7 @@ fn unknown_encoding_is_ignored_after_other_conversions() -> gix_testtools::Resul } #[test] -fn encoding_failure_is_ignored_after_other_conversions() -> gix_testtools::Result { +fn encoding_failure_is_ignored_after_other_conversions() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("all-filters", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; @@ -173,7 +173,7 @@ fn encoding_failure_is_ignored_after_other_conversions() -> gix_testtools::Resul } #[test] -fn encoding_failure_can_be_an_error() -> gix_testtools::Result { +fn encoding_failure_can_be_an_error() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("all-filters", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; @@ -199,7 +199,7 @@ fn encoding_failure_can_be_an_error() -> gix_testtools::Result { } #[test] -fn unknown_encoding_can_be_an_error() -> gix_testtools::Result { +fn unknown_encoding_can_be_an_error() -> gix_testtools::TestResult { let (mut cache, mut pipe) = pipeline("unknown-encoding", || { (vec![], Vec::new(), CrlfRoundTripCheck::Skip, Default::default()) })?; diff --git a/gix-filter/tests/filter/pipeline/mod.rs b/gix-filter/tests/filter/pipeline/mod.rs index c856c20e226..a5c92fbc423 100644 --- a/gix-filter/tests/filter/pipeline/mod.rs +++ b/gix-filter/tests/filter/pipeline/mod.rs @@ -1,13 +1,13 @@ -use crate::Result; use bstr::ByteSlice; use gix_attributes::glob::pattern::Case; use gix_filter::{eol, pipeline::convert::to_worktree}; +use gix_testtools::TestResult; mod convert_to_git; mod convert_to_worktree; #[test] -fn default_options() -> Result { +fn default_options() -> TestResult { let mut filters = gix_filter::Pipeline::new(Default::default(), gix_testtools::object_hash(), Default::default()); let out = filters.convert_to_worktree( b"hi", diff --git a/gix-filter/tests/filter/worktree.rs b/gix-filter/tests/filter/worktree.rs index d1112a71323..f724d5954cb 100644 --- a/gix-filter/tests/filter/worktree.rs +++ b/gix-filter/tests/filter/worktree.rs @@ -73,12 +73,12 @@ mod encoding { } mod encode_to_git { - use crate::Result; use bstr::ByteSlice; use gix_filter::{worktree, worktree::encode_to_git::RoundTripCheck}; + use gix_testtools::TestResult; #[test] - fn simple() -> Result { + fn simple() -> TestResult { let input = &b"hello"[..]; for round_trip in [RoundTripCheck::Skip, RoundTripCheck::Fail] { let mut buf = Vec::new(); @@ -90,12 +90,12 @@ mod encode_to_git { } mod encode_to_worktree { - use crate::Result; use bstr::ByteSlice; use gix_filter::{worktree, worktree::encode_to_git::RoundTripCheck}; + use gix_testtools::TestResult; #[test] - fn shift_jis() -> Result { + fn shift_jis() -> TestResult { let input = "ハローワールド"; let mut buf = Vec::new(); worktree::encode_to_worktree(input.as_bytes(), encoding_rs::SHIFT_JIS, &mut buf)?; diff --git a/gix-fs/src/stack.rs b/gix-fs/src/stack.rs index 1c95209f06c..99a4683b849 100644 --- a/gix-fs/src/stack.rs +++ b/gix-fs/src/stack.rs @@ -62,7 +62,7 @@ fn bytes_component_to_os_str<'a>(component: &'a [u8], path: &BStr) -> Option<Res if component.is_empty() { return None; } - let component = match gix_path::try_from_byte_slice(component.as_bstr()) { + let component = match gix_path::from_byte_slice(component.as_bstr()) { Ok(c) => c, Err(err) => return Some(Err(err)), }; diff --git a/gix-fs/tests/current_dir.rs b/gix-fs/tests/current_dir.rs index d464b783dfe..e12494444aa 100644 --- a/gix-fs/tests/current_dir.rs +++ b/gix-fs/tests/current_dir.rs @@ -1,5 +1,5 @@ #[test] -fn precompose_unicode() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> { +fn precompose_unicode() -> gix_testtools::TestResult { let tmp = tempfile::TempDir::new()?; let decomposed = "a\u{308}"; diff --git a/gix-fs/tests/fs/capabilities.rs b/gix-fs/tests/fs/capabilities.rs index 7729b4b83e1..8339d67ec5b 100644 --- a/gix-fs/tests/fs/capabilities.rs +++ b/gix-fs/tests/fs/capabilities.rs @@ -1,12 +1,11 @@ #[test] -fn probe() { - let dir = tempfile::tempdir().unwrap(); +fn probe() -> gix_testtools::TestResult { + let dir = tempfile::tempdir()?; let config_path = dir.path().join("config"); - std::fs::File::create(&config_path).unwrap(); + std::fs::File::create(&config_path)?; let caps = gix_fs::Capabilities::probe(dir.path()); - let entries: Vec<_> = std::fs::read_dir(dir.path()) - .unwrap() + let entries: Vec<_> = std::fs::read_dir(dir.path())? .filter_map(Result::ok) .filter(|e| e.file_name().to_str() != Some("config")) .map(|e| e.path()) @@ -24,23 +23,23 @@ fn probe() { let actual = gix_fs::Capabilities::probe_dir(dir.path()); assert_eq!(actual, caps, "Both probes arrive at the same result"); - std::fs::remove_file(config_path).expect("to be present"); + std::fs::remove_file(config_path)?; let actual = gix_fs::Capabilities::probe_dir(dir.path()); assert_eq!(actual, caps, "Even if config file doesn't exist, it works"); + Ok(()) } #[test] -fn probe_git_dir_without_config_uses_head() { - let dir = tempfile::tempdir().unwrap(); - std::fs::File::create(dir.path().join("HEAD")).unwrap(); +fn probe_git_dir_without_config_uses_head() -> gix_testtools::TestResult { + let dir = tempfile::tempdir()?; + std::fs::File::create(dir.path().join("HEAD"))?; let actual = gix_fs::Capabilities::probe(dir.path()); let expected_ignore_case = std::fs::metadata(dir.path().join("hEaD")).is_ok(); assert_eq!(actual.ignore_case, expected_ignore_case); - let entries: Vec<_> = std::fs::read_dir(dir.path()) - .unwrap() + let entries: Vec<_> = std::fs::read_dir(dir.path())? .filter_map(Result::ok) .filter(|e| e.file_name().to_str() != Some("HEAD")) .map(|e| e.path()) @@ -50,6 +49,7 @@ fn probe_git_dir_without_config_uses_head() { 0, "there should be no left-over files after probing, found {entries:?}" ); + Ok(()) } #[test] diff --git a/gix-fs/tests/fs/dir/create.rs b/gix-fs/tests/fs/dir/create.rs index de5ed8d90fb..fb65322eb00 100644 --- a/gix-fs/tests/fs/dir/create.rs +++ b/gix-fs/tests/fs/dir/create.rs @@ -1,10 +1,10 @@ mod all { - use crate::Result; + use crate::TestResult; use gix_fs::dir::create; #[test] #[cfg(unix)] - fn shared_permissions_apply_only_to_new_directories() -> crate::Result { + fn shared_permissions_apply_only_to_new_directories() -> TestResult { use std::{fs, os::unix::fs::PermissionsExt}; let dir = tempfile::tempdir()?; @@ -37,7 +37,7 @@ mod all { } #[test] - fn a_deeply_nested_directory() -> Result { + fn a_deeply_nested_directory() -> TestResult { let dir = tempfile::tempdir()?; let target = &dir.path().join("1").join("2").join("3").join("4").join("5").join("6"); let dir = create::all(target, Default::default(), 0)?; @@ -46,7 +46,7 @@ mod all { } } mod iter { - use crate::Result; + use crate::TestResult; pub use std::io::ErrorKind::*; use gix_fs::dir::{ @@ -55,35 +55,29 @@ mod iter { }; #[test] - fn an_existing_directory_causes_immediate_success() -> Result { + fn an_existing_directory_causes_immediate_success() -> TestResult { let dir = tempfile::tempdir()?; let mut it = create::Iter::new(dir.path(), 0); - assert_eq!( - it.next().expect("item").expect("success"), - dir.path(), - "first iteration is immediately successful" - ); + let created = it.next().expect("item").expect("success"); + assert_eq!(created, dir.path(), "first iteration is immediately successful"); assert!(it.next().is_none(), "iterator exhausted afterwards"); Ok(()) } #[test] - fn a_single_directory_can_be_created_too() -> Result { + fn a_single_directory_can_be_created_too() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("new"); let mut it = create::Iter::new(&new_dir, 0); - assert_eq!( - it.next().expect("item").expect("success"), - &new_dir, - "first iteration is immediately successful" - ); + let created = it.next().expect("item").expect("success"); + assert_eq!(created, &new_dir, "first iteration is immediately successful"); assert!(it.next().is_none(), "iterator exhausted afterwards"); assert!(new_dir.is_dir(), "the directory exists"); Ok(()) } #[test] - fn multiple_intermediate_directories_are_created_automatically() -> Result { + fn multiple_intermediate_directories_are_created_automatically() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("s1").join("s2").join("new"); let mut it = create::Iter::new(&new_dir, 0); @@ -125,18 +119,15 @@ mod iter { new_dir.parent().unwrap(), "second subdir is created" ); - assert_eq!( - it.next().expect("item").expect("success"), - new_dir, - "target directory is created" - ); + let created = it.next().expect("item").expect("success"); + assert_eq!(created, new_dir, "target directory is created"); assert!(it.next().is_none(), "iterator depleted"); assert!(new_dir.is_dir(), "the directory exists"); Ok(()) } #[test] - fn multiple_intermediate_directories_are_created_up_to_retries_limit() -> Result { + fn multiple_intermediate_directories_are_created_up_to_retries_limit() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("s1").join("s2").join("new"); let limits = Retries { @@ -179,7 +170,7 @@ mod iter { } #[test] - fn an_existing_file_makes_directory_creation_fail_permanently() -> Result { + fn an_existing_file_makes_directory_creation_fail_permanently() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("also-file"); std::fs::write(&new_dir, [42])?; @@ -219,7 +210,7 @@ mod iter { Ok(()) } #[test] - fn racy_directory_creation_with_new_directory_being_deleted_not_enough_retries() -> Result { + fn racy_directory_creation_with_new_directory_being_deleted_not_enough_retries() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("a").join("new"); let parent_dir = new_dir.parent().unwrap(); @@ -276,7 +267,7 @@ mod iter { } #[test] - fn racy_directory_creation_with_new_directory_being_deleted() -> Result { + fn racy_directory_creation_with_new_directory_being_deleted() -> TestResult { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("a").join("new"); let parent_dir = new_dir.parent().unwrap(); diff --git a/gix-fs/tests/fs/dir/remove.rs b/gix-fs/tests/fs/dir/remove.rs index ae8c2884e2d..890b8eabd73 100644 --- a/gix-fs/tests/fs/dir/remove.rs +++ b/gix-fs/tests/fs/dir/remove.rs @@ -1,11 +1,11 @@ mod empty_upwards_until_boundary { - use crate::Result; + use crate::TestResult; use std::{io, path::Path}; use gix_fs::dir::remove; #[test] - fn boundary_must_contain_target_dir() -> Result { + fn boundary_must_contain_target_dir() -> TestResult { let dir = tempfile::tempdir()?; let (target, boundary) = (dir.path().join("a"), dir.path().join("b")); std::fs::create_dir(&target)?; @@ -26,7 +26,7 @@ mod empty_upwards_until_boundary { Ok(()) } #[test] - fn target_directory_non_existing_causes_existing_parents_not_to_be_deleted() -> Result { + fn target_directory_non_existing_causes_existing_parents_not_to_be_deleted() -> TestResult { let dir = tempfile::tempdir()?; let parent = dir.path().join("a"); std::fs::create_dir(&parent)?; @@ -40,7 +40,7 @@ mod empty_upwards_until_boundary { } #[test] - fn target_directory_being_a_file_immediately_fails() -> Result { + fn target_directory_being_a_file_immediately_fails() -> TestResult { let dir = tempfile::tempdir()?; let target = dir.path().join("actually-a-file"); std::fs::write(&target, [42])?; @@ -50,14 +50,14 @@ mod empty_upwards_until_boundary { Ok(()) } #[test] - fn boundary_being_the_target_dir_always_succeeds_and_we_do_nothing() -> Result { + fn boundary_being_the_target_dir_always_succeeds_and_we_do_nothing() -> TestResult { let dir = tempfile::tempdir()?; assert_eq!(remove::empty_upward_until_boundary(dir.path(), dir.path())?, dir.path()); assert!(dir.path().is_dir(), "it won't touch the boundary"); Ok(()) } #[test] - fn a_directory_which_doesnt_exist_to_start_with_is_ok() -> Result { + fn a_directory_which_doesnt_exist_to_start_with_is_ok() -> TestResult { let dir = tempfile::tempdir()?; let target = dir.path().join("does-not-exist"); assert_eq!(remove::empty_upward_until_boundary(&target, dir.path())?, target); @@ -65,14 +65,14 @@ mod empty_upwards_until_boundary { Ok(()) } #[test] - fn boundary_directory_doesnt_have_to_exist_either_if_the_target_doesnt() -> Result { + fn boundary_directory_doesnt_have_to_exist_either_if_the_target_doesnt() -> TestResult { let boundary = Path::new("/boundary"); let target = Path::new("/boundary/target"); assert_eq!(remove::empty_upward_until_boundary(target, boundary)?, target); Ok(()) } #[test] - fn nested_directory_deletion_works() -> Result { + fn nested_directory_deletion_works() -> TestResult { let dir = tempfile::tempdir()?; let nested = dir.path().join("a").join("b").join("to-delete"); std::fs::create_dir_all(&nested)?; @@ -89,14 +89,14 @@ mod empty_upwards_until_boundary { } mod empty_depth_first { - use crate::Result; + use crate::TestResult; use std::{ fs::{create_dir, create_dir_all}, path::Path, }; #[test] - fn non_empty_anywhere_and_deletion_fails() -> Result { + fn non_empty_anywhere_and_deletion_fails() -> TestResult { let dir = tempfile::TempDir::new()?; let touch = |base: &Path, name: &str| create_dir_all(base).and_then(|_| std::fs::write(base.join(name), b"")); @@ -134,11 +134,11 @@ mod empty_depth_first { /// We assume that all checks above also apply to the iterator, so won't repeat them here /// Test outside interference only mod iter { - use crate::Result; + use crate::TestResult; use gix_fs::dir::remove; #[test] - fn racy_directory_creation_during_deletion_always_wins_immediately() -> Result { + fn racy_directory_creation_during_deletion_always_wins_immediately() -> TestResult { let dir = tempfile::tempdir()?; let nested = dir.path().join("a").join("b").join("to-delete"); std::fs::create_dir_all(&nested)?; diff --git a/gix-fs/tests/fs/file.rs b/gix-fs/tests/fs/file.rs index 3e8914fd7f3..bb39ece2ee1 100644 --- a/gix-fs/tests/fs/file.rs +++ b/gix-fs/tests/fs/file.rs @@ -5,7 +5,7 @@ mod open_read_only_no_follow { use gix_fs::{FileOrSymlink, open_read_only_no_follow}; #[test] - fn regular_files_can_be_read() -> gix_testtools::Result { + fn regular_files_can_be_read() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let path = dir.path().join("file"); std::fs::write(&path, b"contents")?; @@ -23,7 +23,7 @@ mod open_read_only_no_follow { } #[test] - fn missing_paths_are_errors_not_symlinks() -> gix_testtools::Result { + fn missing_paths_are_errors_not_symlinks() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let err = open_read_only_no_follow(&dir.path().join("missing")) .expect_err("missing paths must not be classified as symlinks"); @@ -37,7 +37,7 @@ mod open_read_only_no_follow { #[test] #[cfg(any(unix, windows))] - fn symlinks_are_not_followed() -> gix_testtools::Result { + fn symlinks_are_not_followed() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("symlinks.sh")?; if !gix_testtools::fixture_has_symlinks(&dir)? { return Ok(()); diff --git a/gix-fs/tests/fs/main.rs b/gix-fs/tests/fs/main.rs index 6e73fb73afe..5c83d745a3d 100644 --- a/gix-fs/tests/fs/main.rs +++ b/gix-fs/tests/fs/main.rs @@ -1,4 +1,4 @@ -type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>; +use gix_testtools::TestResult; mod capabilities; mod dir; diff --git a/gix-fs/tests/fs/read_dir.rs b/gix-fs/tests/fs/read_dir.rs index d25aab86a94..40b16217d8e 100644 --- a/gix-fs/tests/fs/read_dir.rs +++ b/gix-fs/tests/fs/read_dir.rs @@ -1,7 +1,7 @@ -use crate::Result; +use crate::TestResult; #[test] -fn with_precomposed_unicode() -> Result { +fn with_precomposed_unicode() -> TestResult { let tmp = tempfile::tempdir()?; let decomposed = "a\u{308}"; diff --git a/gix-fs/tests/fs/snapshot.rs b/gix-fs/tests/fs/snapshot.rs index 406e1b76cb5..c6750194894 100644 --- a/gix-fs/tests/fs/snapshot.rs +++ b/gix-fs/tests/fs/snapshot.rs @@ -3,8 +3,8 @@ use std::path::Path; use gix_fs::SharedFileSnapshotMut; #[test] -fn journey() -> Result<(), Box<dyn std::error::Error>> { - let tmp = tempfile::tempdir().unwrap(); +fn journey() -> gix_testtools::TestResult { + let tmp = tempfile::tempdir()?; if !has_granular_times(tmp.path())? { return Ok(()); } diff --git a/gix-fs/tests/fs/stack.rs b/gix-fs/tests/fs/stack.rs index 7b266e1a053..d285bae345b 100644 --- a/gix-fs/tests/fs/stack.rs +++ b/gix-fs/tests/fs/stack.rs @@ -1,5 +1,5 @@ #![expect(clippy::join_absolute_paths)] -use crate::Result; +use crate::TestResult; use gix_error::classify; use std::path::{Path, PathBuf}; @@ -226,7 +226,7 @@ fn empty_paths_are_noop_if_no_path_was_pushed_before() { } #[test] -fn relative_components_are_invalid() { +fn relative_components_are_invalid() -> TestResult { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); @@ -242,8 +242,7 @@ fn relative_components_are_invalid() { } "#); - s.make_relative_path_current(p("a/./b"), &mut r) - .expect("dot is ignored"); + s.make_relative_path_current(p("a/./b"), &mut r)?; assert_eq!( r, Record { @@ -258,8 +257,7 @@ fn relative_components_are_invalid() { if cfg!(windows) { r".\a\b" } else { "./a/b" }, "dot is silently ignored" ); - s.make_relative_path_current(p("a//b/"), &mut r) - .expect("multiple-slashes are ignored"); + s.make_relative_path_current(p("a//b/"), &mut r)?; assert_eq!( r, Record { @@ -279,10 +277,11 @@ fn relative_components_are_invalid() { } "#); assert!(classify(&err).is_validation(), "peeked errors retain their cause too"); + Ok(()) } #[test] -fn absolute_paths_are_invalid() -> Result { +fn absolute_paths_are_invalid() -> TestResult { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); @@ -377,7 +376,7 @@ fn absolute_paths_are_invalid() -> Result { } #[test] -fn delegate_calls_are_consistent() -> Result { +fn delegate_calls_are_consistent() -> TestResult { let root = PathBuf::from("."); let mut s = Stack::new(root.clone()); @@ -604,7 +603,7 @@ fn delegate_calls_are_consistent() -> Result { } #[test] -fn failed_directory_to_leaf_transition_does_not_keep_directory_state() -> crate::Result { +fn failed_directory_to_leaf_transition_does_not_keep_directory_state() -> TestResult { let mut s = Stack::new(PathBuf::from(".")); let mut r = FailOnce::default(); s.make_relative_path_current("x/z/a", &mut r)?; @@ -635,7 +634,7 @@ fn failed_directory_to_leaf_transition_does_not_keep_directory_state() -> crate: } #[test] -fn failed_leaf_to_directory_transition_restores_leaf_state() -> crate::Result { +fn failed_leaf_to_directory_transition_restores_leaf_state() -> TestResult { let mut s = Stack::new(PathBuf::from(".")); let mut r = FailOnce { directory_to_fail_on: Some(PathBuf::from("x/z")), @@ -668,7 +667,7 @@ fn failed_leaf_to_directory_transition_restores_leaf_state() -> crate::Result { } #[test] -fn failed_child_push_after_leaf_to_directory_transition_restores_directory_state() -> crate::Result { +fn failed_child_push_after_leaf_to_directory_transition_restores_directory_state() -> TestResult { let mut error_snapshots = Vec::new(); for (path_to_fail_on, directory_to_fail_on) in [(Some("x/z/a"), None), (None, Some("x/z/a"))] { let mut s = Stack::new(PathBuf::from(".")); diff --git a/gix-fs/tests/fs/walkdir.rs b/gix-fs/tests/fs/walkdir.rs index ca06088a458..4b1cc3979d7 100644 --- a/gix-fs/tests/fs/walkdir.rs +++ b/gix-fs/tests/fs/walkdir.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, io::ErrorKind}; use gix_fs::{walkdir_new, walkdir_sorted_new}; #[test] -fn hidden_entries_and_depth_limits() -> gix_testtools::Result { +fn hidden_entries_and_depth_limits() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("hidden"); let mut names = walkdir_new(&dir, false) @@ -22,7 +22,7 @@ fn hidden_entries_and_depth_limits() -> gix_testtools::Result { } #[test] -fn sorted_walk_uses_git_directory_order() -> gix_testtools::Result { +fn sorted_walk_uses_git_directory_order() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("sorted"); let names = walkdir_sorted_new(&dir, 1, false) @@ -39,7 +39,7 @@ fn sorted_walk_uses_git_directory_order() -> gix_testtools::Result { } #[test] -fn unicode_precomposition_applies_to_names_and_paths() -> gix_testtools::Result { +fn unicode_precomposition_applies_to_names_and_paths() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?.join("unicode"); let decomposed = "a\u{308}"; let root = dir.join(decomposed); @@ -69,7 +69,7 @@ fn unicode_precomposition_applies_to_names_and_paths() -> gix_testtools::Result } #[test] -fn missing_roots_yield_io_errors() -> gix_testtools::Result { +fn missing_roots_yield_io_errors() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("walkdir.sh")?; let mut entries = walkdir_new(&dir.join("missing"), false).into_iter(); let err = entries diff --git a/gix-glob/src/search/pattern.rs b/gix-glob/src/search/pattern.rs index 26ff98b0e5c..1c331a0baf0 100644 --- a/gix-glob/src/search/pattern.rs +++ b/gix-glob/src/search/pattern.rs @@ -106,24 +106,37 @@ where /// If `root` is `Some(…)` it's used to see `source_file` as relative to itself, if `source_file` is absolute. /// If source is relative and should be treated as base, set `root` to `Some("")`. /// `parse` is a way to parse bytes to pattern. - pub fn from_bytes(bytes: &[u8], source_file: PathBuf, root: Option<&Path>, parse: T) -> Self { + /// Returns an error if a source-relative base cannot be represented as Git path bytes. + pub fn from_bytes(bytes: &[u8], source_file: PathBuf, root: Option<&Path>, parse: T) -> std::io::Result<Self> { let patterns = parse.bytes_to_patterns(bytes, source_file.as_path()); - let base = root - .and_then(|root| source_file.parent().expect("file").strip_prefix(root).ok()) - .and_then(|base| { - (!base.as_os_str().is_empty()).then(|| { - let mut base: BString = - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(base)).into_owned(); - + let base = if let Some(root) = root { + let parent = source_file.parent().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Pattern source must have a parent directory", + ) + })?; + parent + .strip_prefix(root) + .ok() + .filter(|base| !base.as_os_str().is_empty()) + .map(|base| { + let mut base = gix_path::to_unix_separators_on_windows( + gix_path::into_bstr(base).map_err(std::io::Error::other)?, + ) + .into_owned(); base.push_byte(b'/'); - base + Ok::<_, std::io::Error>(base) }) - }); - List { + .transpose()? + } else { + None + }; + Ok(List { patterns, source: Some(source_file), base, - } + }) } /// Create a pattern list from the `source` file, which may be located underneath `root`, while optionally @@ -139,8 +152,9 @@ where parse: T, ) -> std::io::Result<Option<Self>> { let source = source.into(); - Ok(read_in_full_ignore_missing(&source, follow_symlinks, buf)? - .then(|| Self::from_bytes(buf, source, root, parse))) + read_in_full_ignore_missing(&source, follow_symlinks, buf)? + .then(|| Self::from_bytes(buf, source, root, parse)) + .transpose() } } diff --git a/gix-glob/tests/glob/pattern/matching.rs b/gix-glob/tests/glob/pattern/matching.rs index f0805c26a2f..eff17ca96df 100644 --- a/gix-glob/tests/glob/pattern/matching.rs +++ b/gix-glob/tests/glob/pattern/matching.rs @@ -102,7 +102,7 @@ fn compare_baseline_with_ours() { } #[test] -fn compare_whitespace_baseline_with_ours() -> gix_testtools::Result { +fn compare_whitespace_baseline_with_ours() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("make_baseline.sh")?; for (input_file, case) in [ ("git-baseline.whitespace-false", Case::Sensitive), diff --git a/gix-glob/tests/glob/search/pattern.rs b/gix-glob/tests/glob/search/pattern.rs index e8287e26795..66f2dd457a2 100644 --- a/gix-glob/tests/glob/search/pattern.rs +++ b/gix-glob/tests/glob/search/pattern.rs @@ -21,9 +21,9 @@ mod list { } #[test] - fn from_bytes_base() { + fn from_bytes_base() -> gix_testtools::TestResult { { - let list = List::from_bytes(&[], "a/b/source".into(), None, Dummy); + let list = List::from_bytes(&[], "a/b/source".into(), None, Dummy)?; assert_eq!(list.base, None, "no root always means no-base, i.e. globals lists"); assert_eq!( list.source.as_deref(), @@ -33,8 +33,8 @@ mod list { } { - let cwd = std::env::current_dir().expect("cwd available"); - let list = List::from_bytes(&[], cwd.join("a/b/source"), Some(cwd.as_path()), Dummy); + let cwd = std::env::current_dir()?; + let list = List::from_bytes(&[], cwd.join("a/b/source"), Some(cwd.as_path()), Dummy)?; assert_eq!( list.base.as_ref().expect("set"), "a/b/", @@ -48,7 +48,7 @@ mod list { } { - let list = List::from_bytes(&[], "a/b/source".into(), Some(Path::new("c/")), Dummy); + let list = List::from_bytes(&[], "a/b/source".into(), Some(Path::new("c/")), Dummy)?; assert_eq!( list.base, None, "if root doesn't contain source, it silently skips it as base" @@ -59,11 +59,19 @@ mod list { "source is always verbatim" ); } + Ok(()) } #[test] - fn strip_base_handle_recompute_basename_pos() { - let list = List::from_bytes(&[], "a/b/source".into(), Some(Path::new("")), Dummy); + fn a_source_without_a_parent_returns_an_error() { + let err = List::from_bytes(&[], "".into(), Some(Path::new("")), Dummy) + .expect_err("a source-relative list needs a parent directory"); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + } + + #[test] + fn strip_base_handle_recompute_basename_pos() -> gix_testtools::TestResult { + let list = List::from_bytes(&[], "a/b/source".into(), Some(Path::new("")), Dummy)?; assert_eq!( list.base.as_ref().expect("set"), "a/b/", @@ -82,6 +90,7 @@ mod list { Some(("c/File".into(), Some(2))), "otherwise the basename is recomputed, case folding is effective" ); + Ok(()) } #[test] @@ -97,12 +106,12 @@ mod list { } #[test] - fn from_file_that_is_a_directory() -> gix_testtools::Result<()> { + fn from_file_that_is_a_directory() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let dir_path = tmp.path().join(".gitignore"); std::fs::create_dir(&dir_path)?; let mut buf = Vec::new(); - let list = List::from_file(dir_path, None, false, &mut buf, Dummy).expect("no io error"); + let list = List::from_file(dir_path, None, false, &mut buf, Dummy)?; assert!(list.is_none(), "directories are ignored just like Git does it"); Ok(()) diff --git a/gix-glob/tests/pattern_list.rs b/gix-glob/tests/pattern_list.rs new file mode 100644 index 00000000000..681265cd509 --- /dev/null +++ b/gix-glob/tests/pattern_list.rs @@ -0,0 +1,2 @@ +#[path = "glob/search/pattern.rs"] +mod pattern; diff --git a/gix-hash/tests/hash/change_id.rs b/gix-hash/tests/hash/change_id.rs index 7bc196a30b8..c2f4a14474b 100644 --- a/gix-hash/tests/hash/change_id.rs +++ b/gix-hash/tests/hash/change_id.rs @@ -3,7 +3,7 @@ use gix_hash::{ChangeId, ObjectId}; use crate::hex_to_id; #[test] -fn formats_and_parses_exactly_like_jujutsu() -> gix_testtools::Result { +fn formats_and_parses_exactly_like_jujutsu() -> gix_testtools::TestResult { let object_hex = "0123456789abcdef0123456789abcdef01234567"; let reverse_hex = "zyxwvutsrqponmlkzyxwvutsrqponmlkzyxwvuts"; let object_id = hex_to_id(object_hex); @@ -45,7 +45,7 @@ fn formats_and_parses_exactly_like_jujutsu() -> gix_testtools::Result { #[test] #[cfg(feature = "sha256")] -fn formats_sha256_with_the_same_jj_algorithm() -> gix_testtools::Result { +fn formats_sha256_with_the_same_jj_algorithm() -> gix_testtools::TestResult { let object_hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; let reverse_hex = "zyxwvutsrqponmlkzyxwvutsrqponmlkzyxwvutsrqponmlkzyxwvutsrqponmlk"; let change_id = ChangeId::from(hex_to_id(object_hex)); diff --git a/gix-hash/tests/hash/object_id.rs b/gix-hash/tests/hash/object_id.rs index 23d71266027..bdeb6ee4ec2 100644 --- a/gix-hash/tests/hash/object_id.rs +++ b/gix-hash/tests/hash/object_id.rs @@ -65,23 +65,22 @@ mod sha1 { } #[test] - fn empty_blob() { + fn empty_blob() -> gix_testtools::TestResult { let actual = ObjectId::empty_blob(Kind::Sha1); - assert_eq!(actual, hash_contents(b"blob 0\0").expect("empty blob to not collide"),); + assert_eq!(actual, hash_contents(b"blob 0\0")?,); assert_eq!(format!("{actual:?}"), "Sha1(e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)"); + Ok(()) } #[test] - fn empty_tree() { - assert_eq!( - ObjectId::empty_tree(Kind::Sha1), - hash_contents(b"tree 0\0").expect("empty tree to not collide"), - ); + fn empty_tree() -> gix_testtools::TestResult { + assert_eq!(ObjectId::empty_tree(Kind::Sha1), hash_contents(b"tree 0\0")?); + Ok(()) } /// Check the test vectors from RFC 3174. #[test] - fn rfc_3174() { + fn rfc_3174() -> gix_testtools::TestResult { let fixtures: &[(&[u8], &str)] = &[ (b"abc", "A9 99 3E 36 47 06 81 6A BA 3E 25 71 78 50 C2 6C 9C D0 D8 9D"), ( @@ -99,10 +98,11 @@ mod sha1 { ]; for (input, output) in fixtures { assert_eq!( - hash_contents(input).expect("RFC inputs to not collide"), - ObjectId::from_str(&output.to_lowercase().replace(' ', "")).expect("RFC digests to be valid"), + hash_contents(input)?, + ObjectId::from_str(&output.to_lowercase().replace(' ', ""))?, ); } + Ok(()) } /// Check the “SHA‐1 is a Shambles” chosen‐prefix collision. @@ -133,20 +133,19 @@ mod sha256 { } #[test] - fn empty_blob() { + fn empty_blob() -> gix_testtools::TestResult { let actual = ObjectId::empty_blob(Kind::Sha256); - assert_eq!(actual, hash_contents(b"blob 0\0").expect("empty blob to not collide"),); + assert_eq!(actual, hash_contents(b"blob 0\0")?,); assert_eq!( format!("{actual:?}"), "Sha256(473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813)" ); + Ok(()) } #[test] - fn empty_tree() { - assert_eq!( - ObjectId::empty_tree(Kind::Sha256), - hash_contents(b"tree 0\0").expect("empty tree to not collide"), - ); + fn empty_tree() -> gix_testtools::TestResult { + assert_eq!(ObjectId::empty_tree(Kind::Sha256), hash_contents(b"tree 0\0")?); + Ok(()) } } diff --git a/gix-hash/tests/hash/prefix.rs b/gix-hash/tests/hash/prefix.rs index 2656b6f603a..563617237c3 100644 --- a/gix-hash/tests/hash/prefix.rs +++ b/gix-hash/tests/hash/prefix.rs @@ -2,16 +2,16 @@ mod hex_output { use crate::hex_to_id; #[test] - fn writes_only_the_significant_hex_digits() { - let prefix = - gix_hash::Prefix::new(&hex_to_id("abcdefabcdefabcdefabcdefabcdefabcdefabcd"), 7).expect("valid prefix"); + fn writes_only_the_significant_hex_digits() -> gix_testtools::TestResult { + let prefix = gix_hash::Prefix::new(&hex_to_id("abcdefabcdefabcdefabcdefabcdefabcdefabcd"), 7)?; let mut buf = [0; 7]; assert_eq!(prefix.hex_to_buf(&mut buf), "abcdefa"); assert_eq!(prefix.to_string(), "abcdefa"); let mut written = Vec::new(); - prefix.write_hex_to(&mut written).expect("in-memory writes succeed"); + prefix.write_hex_to(&mut written)?; assert_eq!(written, b"abcdefa"); + Ok(()) } } @@ -314,7 +314,7 @@ mod reverse_hex { use gix_hash::{ChangeId, Prefix}; #[test] - fn matches_change_ids_at_odd_nibbles() -> gix_testtools::Result { + fn matches_change_ids_at_odd_nibbles() -> gix_testtools::TestResult { let full = "zzyxwvutsrqponmlkzyxwvutsrqponmlkzyxwvut"; let change_id = ChangeId::from_reverse_hex(full.as_bytes())?; let prefix = Prefix::from_reverse_hex_nonempty("zzy")?; @@ -326,7 +326,7 @@ mod reverse_hex { } #[test] - fn validates_reverse_hex_like_forward_hex() -> gix_testtools::Result { + fn validates_reverse_hex_like_forward_hex() -> gix_testtools::TestResult { insta::assert_debug_snapshot!(Prefix::from_reverse_hex("zzy") .expect_err("three digits are below the safe minimum"), "validates reverse hex like forward hex", @"The minimum hex length of a short object id is 4, got 3"); let prefix = Prefix::from_reverse_hex("ZZYX")?; diff --git a/gix-ignore/fuzz/fuzz_targets/ignore.rs b/gix-ignore/fuzz/fuzz_targets/ignore.rs index e68baa403bc..17ccc96413b 100644 --- a/gix-ignore/fuzz/fuzz_targets/ignore.rs +++ b/gix-ignore/fuzz/fuzz_targets/ignore.rs @@ -94,7 +94,9 @@ fn fuzz(input: &[u8]) { } let mut search = Search::default(); - search.add_patterns_buffer(&sane_input, "fuzz.gitignore", None, ignore); + search + .add_patterns_buffer(&sane_input, "fuzz.gitignore", None, ignore) + .expect("a global pattern list has no base path to convert"); let overrides: Vec<String> = sane_input .split(|b| *b == 0 || *b == b'\n') diff --git a/gix-ignore/src/search.rs b/gix-ignore/src/search.rs index 41c1e314136..89522cad7c0 100644 --- a/gix-ignore/src/search.rs +++ b/gix-ignore/src/search.rs @@ -89,7 +89,7 @@ impl Search { patterns: patterns .enumerate() .filter_map(|(seq_id, pattern)| { - let pattern = gix_path::try_into_bstr(PathBuf::from(pattern)).ok()?; + let pattern = gix_path::into_bstr(PathBuf::from(pattern)).ok()?; crate::parse(pattern.as_ref(), parse.support_precious) .next() .map(|(p, _seq_id, kind)| pattern::Mapping { @@ -117,9 +117,10 @@ impl Search { source: impl Into<PathBuf>, root: Option<&Path>, parse: Ignore, - ) { + ) -> std::io::Result<()> { self.patterns - .push(pattern::List::from_bytes(bytes, source.into(), root, parse)); + .push(pattern::List::from_bytes(bytes, source.into(), root, parse)?); + Ok(()) } } diff --git a/gix-ignore/tests/ignore/search.rs b/gix-ignore/tests/ignore/search.rs index d9aaeccfe54..9cd8ea46656 100644 --- a/gix-ignore/tests/ignore/search.rs +++ b/gix-ignore/tests/ignore/search.rs @@ -1,7 +1,7 @@ -use crate::Result; use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; use gix_ignore::search::Match; +use gix_testtools::TestResult; struct Expectations<'a> { lines: bstr::Lines<'a>, @@ -29,7 +29,7 @@ impl<'a> Iterator for Expectations<'a> { } #[test] -fn baseline_from_git_dir() -> Result { +fn baseline_from_git_dir() -> TestResult { for repo_name in [ "repo", "slash-and-excludes", @@ -85,7 +85,7 @@ fn baseline_from_git_dir() -> Result { let ignore_file = repo_dir.join("dir-with-ignore").join(".gitignore"); if ignore_file.is_file() { let buf = std::fs::read(&ignore_file)?; - group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into(), Default::default()); + group.add_patterns_buffer(&buf, ignore_file, repo_dir.as_path().into(), Default::default())?; } for (path, source_and_line) in (Expectations { diff --git a/gix-index/tests/index/file/access.rs b/gix-index/tests/index/file/access.rs index 7d0353bb9b5..4407200dd73 100644 --- a/gix-index/tests/index/file/access.rs +++ b/gix-index/tests/index/file/access.rs @@ -1,9 +1,9 @@ mod set_path { - use crate::Result; use crate::file::read; + use gix_testtools::TestResult; #[test] - fn future_writes_respect_the_newly_set_path() -> Result { + fn future_writes_respect_the_newly_set_path() -> TestResult { let mut file = read::file("v4_more_files_IEOT"); let tmp = gix_testtools::tempfile::TempDir::new()?; let new_index_path = tmp.path().join("new-index"); diff --git a/gix-index/tests/index/file/init.rs b/gix-index/tests/index/file/init.rs index 2b414c0d883..d88092b27a7 100644 --- a/gix-index/tests/index/file/init.rs +++ b/gix-index/tests/index/file/init.rs @@ -1,6 +1,6 @@ mod at { #[test] - fn shorter_than_checksum() -> gix_testtools::Result { + fn shorter_than_checksum() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let tmp = gix_testtools::tempfile::TempDir::new()?; let path = tmp.path().join("index"); @@ -37,18 +37,18 @@ mod at_or_new { use crate::Fixture::Generated; #[test] - fn opens_existing() { + fn opens_existing() -> gix_testtools::TestResult { gix_index::File::at_or_default( Generated("v4_more_files_IEOT").to_path(), gix_testtools::object_hash(), false, Default::default(), - ) - .expect("file exists and can be opened"); + )?; + Ok(()) } #[test] - fn missing_shared_index_is_an_error() -> gix_testtools::Result { + fn missing_shared_index_is_an_error() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let index_path = tmp.path().join("index"); // Keep the primary split index, but leave its shared index behind. @@ -67,14 +67,13 @@ mod at_or_new { } #[test] - fn create_empty_in_memory_state_if_file_does_not_exist() { + fn create_empty_in_memory_state_if_file_does_not_exist() -> gix_testtools::TestResult { let index = gix_index::File::at_or_default( "__definitely no file that exists ever__", gix_testtools::object_hash(), false, Default::default(), - ) - .expect("file is defaulting to a new one"); + )?; assert!(!index.path().is_file(), "the file wasn't created yet"); assert_eq!( index.object_hash(), @@ -82,6 +81,7 @@ mod at_or_new { "object hash is respected" ); assert_eq!(index.entries().len(), 0, "index is empty"); + Ok(()) } } @@ -91,7 +91,7 @@ mod from_state { use crate::Fixture::*; #[test] - fn writes_data_to_disk_and_is_a_valid_index() -> gix_testtools::Result { + fn writes_data_to_disk_and_is_a_valid_index() -> gix_testtools::TestResult { let fixtures = [ (Loose("extended-flags"), V3), (Generated("v2"), V2), diff --git a/gix-index/tests/index/file/write.rs b/gix-index/tests/index/file/write.rs index 3198bdbaf7d..a2045685485 100644 --- a/gix-index/tests/index/file/write.rs +++ b/gix-index/tests/index/file/write.rs @@ -1,12 +1,12 @@ -use crate::Result; use filetime::FileTime; use gix_index::{State, Version, entry, extension, write, write::Options}; +use gix_testtools::TestResult; use crate::Fixture::*; /// Round-trips should eventually be possible for all files we have, as we write them back exactly as they were read. #[test] -fn roundtrips() -> Result { +fn roundtrips() -> TestResult { let input = [ (Loose("extended-flags"), only_tree_ext()), (Loose("conflicting-file"), only_tree_ext()), @@ -49,7 +49,7 @@ fn roundtrips() -> Result { } #[test] -fn skip_hash() -> Result { +fn skip_hash() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let path = tmp.path().join("index"); let mut expected = Loose("conflicting-file").open(); @@ -90,7 +90,7 @@ fn skip_hash() -> Result { } #[test] -fn roundtrips_sparse_index() -> Result { +fn roundtrips_sparse_index() -> TestResult { // NOTE: I initially tried putting these fixtures into the main roundtrip test above, // but the call to `compare_raw_bytes` panics. It seems like git is using a different // ordering when it comes to writing the tree extension. Need to investigate more, hence @@ -190,7 +190,7 @@ fn state_comparisons_with_various_extension_configurations() { } #[test] -fn extended_flags_automatically_upgrade_the_version_to_avoid_data_loss() -> Result { +fn extended_flags_automatically_upgrade_the_version_to_avoid_data_loss() -> TestResult { let mut expected = Generated("v2").open(); assert_eq!(expected.version(), Version::V2); expected.entries_mut()[0].flags.insert(entry::Flags::EXTENDED); @@ -203,7 +203,7 @@ fn extended_flags_automatically_upgrade_the_version_to_avoid_data_loss() -> Resu } #[test] -fn remove_flag_is_respected() -> Result { +fn remove_flag_is_respected() -> TestResult { let mut index = Generated("v4_more_files_IEOT").open(); let total_entries = 10; assert_eq!(index.entries().len(), total_entries); diff --git a/gix-index/tests/index/fs.rs b/gix-index/tests/index/fs.rs index 1469bd15784..646a9960790 100644 --- a/gix-index/tests/index/fs.rs +++ b/gix-index/tests/index/fs.rs @@ -1,9 +1,9 @@ mod metadata { - use crate::Result; use gix_index::fs::Metadata; + use gix_testtools::TestResult; #[test] - fn from_path_no_follow() -> Result { + fn from_path_no_follow() -> TestResult { let root = crate::scripted_fixture_read_only("file_metadata.sh")?; // For now, don't assert on the values of the metadata as these depends on the filesystem, diff --git a/gix-index/tests/index/fuzzed.rs b/gix-index/tests/index/fuzzed.rs index b56e286f33b..ad5880f1b9b 100644 --- a/gix-index/tests/index/fuzzed.rs +++ b/gix-index/tests/index/fuzzed.rs @@ -62,29 +62,29 @@ fn malformed_tree_extension_is_ignored_instead_of_panicking() { } #[test] -fn malformed_fsmonitor_extension_is_ignored_instead_of_panicking() { +fn malformed_fsmonitor_extension_is_ignored_instead_of_panicking() -> gix_testtools::TestResult { let (state, _checksum) = decode_fuzzed(include_bytes!( "../../fuzz/artifacts/index_file/crash-6fe328e670c3ca54a4dac7a5c0dc1e51501cf1d9" - )) - .expect("fuzzed input should decode without panicking"); + ))?; assert!( state.fs_monitor().is_none(), "malformed optional extension must be ignored" ); + Ok(()) } #[test] -fn malformed_untracked_cache_extension_is_ignored_instead_of_panicking() { +fn malformed_untracked_cache_extension_is_ignored_instead_of_panicking() -> gix_testtools::TestResult { let (state, _checksum) = decode_fuzzed(include_bytes!( "../../fuzz/artifacts/index_file/crash-b3dc19d67c36fbc5fc4b4f5729df92911dd3a7d5" - )) - .expect("fuzzed input should decode without panicking"); + ))?; assert!( state.untracked().is_none(), "malformed optional extension must be ignored" ); + Ok(()) } #[test] diff --git a/gix-index/tests/index/init.rs b/gix-index/tests/index/init.rs index 3a19ffcaf96..677d06d8a9c 100644 --- a/gix-index/tests/index/init.rs +++ b/gix-index/tests/index/init.rs @@ -1,11 +1,11 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use crate::{odb_at, scripted_fixture_read_only}; use gix_index::State; #[test] -fn from_tree() -> Result { +fn from_tree() -> gix_testtools::TestResult { let fixtures = [ "make_index/v2.sh", "make_index/v2_more_files.sh", @@ -34,7 +34,7 @@ fn from_tree() -> Result { } #[test] -fn from_tree_validation() -> Result { +fn from_tree_validation() -> TestResult { let mut error_snapshots = Vec::new(); use gix_index::validate::path::component::Error; @@ -87,15 +87,13 @@ fn from_tree_validation() -> Result { } #[test] -fn from_tree_returns_file_directory_conflicts_until_fixed() -> Result { +fn from_tree_returns_file_directory_conflicts_until_fixed() -> gix_testtools::TestResult { let worktree_dir = scripted_fixture_read_only("make_symlink_prefix_reuse_advisory.sh")?; let tree_id = tree_id(&worktree_dir); let odb = odb_at(worktree_dir.join(".git").join("objects"))?; let actual_state = State::from_tree(&tree_id, &odb, Default::default())?; - actual_state - .verify_entries() - .expect("valid, even though invariants aren't met"); + actual_state.verify_entries()?; let paths: Vec<_> = actual_state .entries() diff --git a/gix-lock/src/acquire.rs b/gix-lock/src/acquire.rs index 5bfb274e0e8..5a527b8c2eb 100644 --- a/gix-lock/src/acquire.rs +++ b/gix-lock/src/acquire.rs @@ -304,7 +304,7 @@ mod tests { } #[test] - fn resource_is_resolved_on_each_lock_attempt() { + fn resource_is_resolved_on_each_lock_attempt() -> gix_testtools::TestResult { let resolutions = std::cell::Cell::new(0); let resolve = |_: &Path| { let current = resolutions.get(); @@ -324,11 +324,11 @@ mod tests { Ok(()) } }, - ) - .expect("the second target can be locked"); + )?; assert_eq!(resolutions.get(), 2, "the resource is resolved before every attempt"); assert_eq!(resource_path, Path::new("second"), "the locked target is retained"); assert_eq!(lock_path, Path::new("second.lock"), "the lock follows that target"); + Ok(()) } } diff --git a/gix-lock/tests/lock/file.rs b/gix-lock/tests/lock/file.rs index 3f152ee724a..d5835f611b9 100644 --- a/gix-lock/tests/lock/file.rs +++ b/gix-lock/tests/lock/file.rs @@ -5,7 +5,7 @@ mod close { use gix_lock::acquire::Fail; #[test] - fn acquire_close_commit_to_existing_file() -> gix_error::TestResult { + fn acquire_close_commit_to_existing_file() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::write(&resource, b"old state")?; @@ -31,7 +31,7 @@ mod commit { use gix_lock::acquire::Fail; #[test] - fn failure_to_commit_does_return_a_registered_marker() -> gix_error::TestResult { + fn failure_to_commit_does_return_a_registered_marker() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::create_dir(&resource)?; @@ -53,7 +53,7 @@ mod commit { } #[test] - fn failure_to_commit_does_return_a_registered_file() -> gix_error::TestResult { + fn failure_to_commit_does_return_a_registered_file() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-existing.ext"); std::fs::create_dir(&resource)?; @@ -96,7 +96,7 @@ mod acquire { } #[test] - fn lock_create_dir_write_commit() -> gix_error::TestResult { + fn lock_create_dir_write_commit() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("a").join("resource-nonexisting"); let resource_lock = resource.with_extension("lock"); @@ -128,7 +128,7 @@ mod acquire { #[test] #[cfg(unix)] - fn shared_permissions_reach_directories_created_by_files_and_markers() -> gix_error::TestResult { + fn shared_permissions_reach_directories_created_by_files_and_markers() -> gix_testtools::TestResult { use std::{fs, os::unix::fs::PermissionsExt}; let dir = tempfile::tempdir()?; @@ -172,7 +172,7 @@ mod acquire { } #[test] - fn lock_write_drop() -> gix_error::TestResult { + fn lock_write_drop() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("resource-nonexisting.ext"); { @@ -185,7 +185,7 @@ mod acquire { #[test] #[cfg(unix)] - fn lock_following_symlinks_writes_their_target() -> gix_error::TestResult { + fn lock_following_symlinks_writes_their_target() -> gix_testtools::TestResult { use std::os::unix::fs::symlink; let dir = tempfile::tempdir()?; @@ -209,7 +209,7 @@ mod acquire { #[test] #[cfg(unix)] - fn lock_permissions_can_be_adjusted_after_applying_the_umask() -> gix_error::TestResult { + fn lock_permissions_can_be_adjusted_after_applying_the_umask() -> gix_testtools::TestResult { use std::os::unix::fs::PermissionsExt; let dir = tempfile::tempdir()?; @@ -230,7 +230,7 @@ mod acquire { } #[test] - fn resource_path_does_not_parse_the_lock_file_name() -> gix_error::TestResult { + fn resource_path_does_not_parse_the_lock_file_name() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource_dir = dir.path().join("resource"); std::fs::create_dir(&resource_dir)?; @@ -246,7 +246,7 @@ mod acquire { } #[test] - fn lock_non_existing_dir_fails() -> gix_error::TestResult { + fn lock_non_existing_dir_fails() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("a").join("resource.ext"); let err = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None, 0) diff --git a/gix-lock/tests/lock/marker.rs b/gix-lock/tests/lock/marker.rs index 4f28221465b..f36e75e60c3 100644 --- a/gix-lock/tests/lock/marker.rs +++ b/gix-lock/tests/lock/marker.rs @@ -4,7 +4,7 @@ mod acquire { use gix_lock::acquire::Fail; #[test] - fn fail_mode_immediately_produces_a_descriptive_error() -> gix_error::TestResult { + fn fail_mode_immediately_produces_a_descriptive_error() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None, 0)?; @@ -32,7 +32,7 @@ mod acquire { } #[test] - fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> gix_error::TestResult { + fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let _guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None, 0)?; @@ -82,7 +82,7 @@ mod commit { } #[test] - fn fails_for_ordinary_marker_that_was_never_writable() -> gix_error::TestResult { + fn fails_for_ordinary_marker_that_was_never_writable() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let resource = dir.path().join("the-resource"); let mark = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None, 0)?; diff --git a/gix-mailmap/tests/mailmap/snapshot.rs b/gix-mailmap/tests/mailmap/snapshot.rs index 43b920fd120..36232aab7a7 100644 --- a/gix-mailmap/tests/mailmap/snapshot.rs +++ b/gix-mailmap/tests/mailmap/snapshot.rs @@ -215,17 +215,17 @@ fn invalid_entries_are_ignored() { } #[test] -fn partial_updates_match_git() -> gix_testtools::Result { - assert_matches_git("partial-updates") +fn partial_updates_match_git() -> gix_testtools::TestResult { + assert_matches_git("partial-updates").map_err(Into::into) } #[test] -fn case_folding_with_non_utf8_keys_matches_git() -> gix_testtools::Result { - assert_matches_git("case-folding-with-non-utf8-keys") +fn case_folding_with_non_utf8_keys_matches_git() -> gix_testtools::TestResult { + assert_matches_git("case-folding-with-non-utf8-keys").map_err(Into::into) } #[test] -fn large_reverse_ordered_mailmaps() -> gix_testtools::Result { +fn large_reverse_ordered_mailmaps() -> gix_testtools::TestResult { use std::fmt::Write; const COUNT: usize = 100_000; diff --git a/gix-merge/src/blob/pipeline.rs b/gix-merge/src/blob/pipeline.rs index 938edbe7ea6..d7268320621 100644 --- a/gix-merge/src/blob/pipeline.rs +++ b/gix-merge/src/blob/pipeline.rs @@ -159,7 +159,7 @@ impl Pipeline { Some(root) => { self.path.clear(); self.path.push(root); - self.path.push(gix_path::from_bstr(rela_path)); + self.path.push(gix_path::from_bstr(rela_path)?); let size_in_bytes = (self.options.large_file_threshold_bytes > 0) .then(|| { none_if_missing(self.path.metadata().map(|md| md.len())).or_raise(|| { @@ -180,7 +180,7 @@ impl Pipeline { Mode::ToGit | Mode::Renormalize => { let res = self.filter.convert_to_git( file, - gix_path::from_bstr(rela_path).as_ref(), + gix_path::from_bstr(rela_path)?.as_ref(), attributes, &mut |buf| { if convert == Mode::Renormalize || id.is_null() { @@ -277,7 +277,7 @@ impl Pipeline { let res = self.filter.convert_to_git( &**out, - &gix_path::from_bstr(rela_path), + &gix_path::from_bstr(rela_path)?, attributes, &mut |_buf| Ok(None), )?; diff --git a/gix-merge/src/blob/platform/merge.rs b/gix-merge/src/blob/platform/merge.rs index 983932163fb..bd2083f32d0 100644 --- a/gix-merge/src/blob/platform/merge.rs +++ b/gix-merge/src/blob/platform/merge.rs @@ -155,15 +155,15 @@ pub(super) mod inner { let token = if count > 1 { match token.first() { Some(&b'O') => { - cmd.push_str(gix_path::into_bstr(&base_path).as_ref()); + cmd.push_str(gix_path::into_bstr(&base_path)?.as_ref()); &token[1..] } Some(&b'A') => { - cmd.push_str(gix_path::into_bstr(&ours_path).as_ref()); + cmd.push_str(gix_path::into_bstr(&ours_path)?.as_ref()); &token[1..] } Some(&b'B') => { - cmd.push_str(gix_path::into_bstr(&theirs_path).as_ref()); + cmd.push_str(gix_path::into_bstr(&theirs_path)?.as_ref()); &token[1..] } Some(&b'L') => { @@ -205,13 +205,13 @@ pub(super) mod inner { } Ok(merge::Command { - cmd: gix_command::prepare(gix_path::from_bstring(cmd)) + cmd: gix_command::prepare(gix_path::from_bstring(cmd)?) .with_context(context) .command_may_be_shell_script() .stdin(Stdio::null()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) - .into(), + .try_into()?, current: ours_tmp, current_path: ours_path, ancestor: base_tmp, diff --git a/gix-merge/tests/merge/blob/builtin_driver.rs b/gix-merge/tests/merge/blob/builtin_driver.rs index 6d17c0efb8a..ee096f17836 100644 --- a/gix-merge/tests/merge/blob/builtin_driver.rs +++ b/gix-merge/tests/merge/blob/builtin_driver.rs @@ -26,13 +26,13 @@ fn binary() { } mod text { - use crate::Result; use arbitrary::Arbitrary; use bstr::ByteSlice; use gix_merge::blob::{ Resolution, builtin_driver, builtin_driver::text::{self, Conflict, ConflictStyle}, }; + use gix_testtools::TestResult; use pretty_assertions::assert_str_eq; use std::num::NonZero; @@ -202,7 +202,7 @@ mod text { } #[test] - fn run_baseline() -> Result { + fn run_baseline() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("text-baseline.sh")?; for (baseline, diverging, expected_percentage) in [ ("baseline.cases", DIVERGING, 10), diff --git a/gix-merge/tests/merge/blob/pipeline.rs b/gix-merge/tests/merge/blob/pipeline.rs index 6eebfca0436..51a7b8f7ed0 100644 --- a/gix-merge/tests/merge/blob/pipeline.rs +++ b/gix-merge/tests/merge/blob/pipeline.rs @@ -1,4 +1,3 @@ -use crate::Result; use bstr::ByteSlice; use gix_filter::{eol, eol::AutoCrlf}; use gix_merge::blob::{ @@ -6,13 +5,14 @@ use gix_merge::blob::{ pipeline::{self, Mode, WorktreeRoots}, }; use gix_object::tree::EntryKind; +use gix_testtools::TestResult; use crate::blob::util::{insert, object_db}; const ALL_MODES: [pipeline::Mode; 2] = [pipeline::Mode::ToGit, pipeline::Mode::Renormalize]; #[test] -fn without_transformation() -> Result { +fn without_transformation() -> TestResult { let mut error_snapshots = Vec::new(); for mode in ALL_MODES { let tmp = gix_testtools::tempfile::TempDir::new()?; @@ -124,7 +124,7 @@ fn without_transformation() -> Result { } #[test] -fn binary_below_large_file_threshold() -> Result { +fn binary_below_large_file_threshold() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = Pipeline::new( WorktreeRoots { @@ -174,7 +174,7 @@ fn binary_below_large_file_threshold() -> Result { } #[test] -fn above_large_file_threshold() -> Result { +fn above_large_file_threshold() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = gix_merge::blob::Pipeline::new( WorktreeRoots { @@ -235,7 +235,7 @@ fn above_large_file_threshold() -> Result { } #[test] -fn non_existing() -> Result { +fn non_existing() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let mut filter = Pipeline::new( WorktreeRoots { @@ -311,7 +311,7 @@ fn non_existing() -> Result { } #[test] -fn worktree_filter() -> Result { +fn worktree_filter() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let filter = gix_filter::Pipeline::new( Default::default(), diff --git a/gix-merge/tests/merge/blob/platform.rs b/gix-merge/tests/merge/blob/platform.rs index ad74b51d690..7ba19337f03 100644 --- a/gix-merge/tests/merge/blob/platform.rs +++ b/gix-merge/tests/merge/blob/platform.rs @@ -2,7 +2,7 @@ use gix_merge::blob::Platform; use gix_worktree::stack::state::attributes; mod merge { - use crate::Result; + use gix_testtools::TestResult; use std::{convert::Infallible, path::Path, process::Stdio}; use bstr::{BStr, ByteSlice}; @@ -23,7 +23,7 @@ mod merge { }; #[test] - fn builtin_text_uses_binary_if_needed() -> Result { + fn builtin_text_uses_binary_if_needed() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); platform.set_resource( gix_hash::Kind::Sha1.null(), @@ -73,7 +73,7 @@ mod merge { } #[test] - fn same_binaries_do_not_count_as_conflicted() -> Result { + fn same_binaries_do_not_count_as_conflicted() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); platform.set_resource( gix_hash::Kind::Sha1.null(), @@ -122,7 +122,7 @@ mod merge { } #[test] - fn builtin_with_conflict() -> Result { + fn builtin_with_conflict() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); let non_existing_ancestor_id = hex_to_id("ffffffffffffffffffffffffffffffffffffffff"); platform.set_resource( @@ -267,7 +267,7 @@ theirs } #[test] - fn external_driver_failure_is_unclassified() -> gix_error::TestResult { + fn external_driver_failure_is_unclassified() -> TestResult { for exit_code in [1, 2] { let mut platform = new_platform( [gix_merge::blob::Driver { @@ -313,7 +313,7 @@ theirs } #[test] - fn with_external() -> Result { + fn with_external() -> TestResult { let mut platform = new_platform( [gix_merge::blob::Driver { name: "b".into(), @@ -413,7 +413,7 @@ theirs #[test] #[cfg(not(windows))] // assertions aren't handling Windows paths, and there is no need. /// This test is a complex behavioural test for an external merge driver similar to `mergiraf`. - fn with_external_mergiraf_like_driver_uses_worktree_tempfiles_from_context() -> Result { + fn with_external_mergiraf_like_driver_uses_worktree_tempfiles_from_context() -> TestResult { let mut platform = new_platform( [gix_merge::blob::Driver { name: "b".into(), @@ -480,9 +480,9 @@ cat "%B" >> "%A""# ); } - let lines: Vec<_> = lines - .map(|line| line.to_str().expect("driver output is valid UTF-8")) - .collect(); + let lines = lines + .map(ByteSlice::to_str) + .collect::<std::result::Result<Vec<_>, _>>()?; assert_eq!( lines, [ @@ -503,7 +503,7 @@ cat "%B" >> "%A""# } #[test] - fn missing_buffers_are_empty_buffers() -> Result { + fn missing_buffers_are_empty_buffers() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); platform.set_resource( gix_hash::Kind::Sha1.null(), @@ -567,7 +567,7 @@ cat "%B" >> "%A""# } #[test] - fn one_buffer_too_large() -> Result { + fn one_buffer_too_large() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); platform.filter.options.large_file_threshold_bytes = 9; platform.set_resource( @@ -625,7 +625,7 @@ cat "%B" >> "%A""# } fn cleaned_driver_lines(buf: &[u8]) -> std::io::Result<impl Iterator<Item = &BStr>> { - let current_dir = gix_path::into_bstr(std::env::current_dir()?); + let current_dir = gix_path::into_bstr(std::env::current_dir()?).expect("fixture path is representable"); Ok(buf .lines() .map(move |line| line.strip_prefix(current_dir.as_bytes()).unwrap_or(line).as_bstr())) @@ -641,17 +641,17 @@ cat "%B" >> "%A""# } mod prepare_merge { - use crate::Result; use gix_merge::blob::{ BuiltinDriver, ResourceKind, builtin_driver, pipeline, platform::{DriverChoice, resource}, }; use gix_object::tree::EntryKind; + use gix_testtools::TestResult; use crate::blob::platform::new_platform; #[test] - fn ancestor_and_current_and_other_do_not_exist() -> Result { + fn ancestor_and_current_and_other_do_not_exist() -> TestResult { let mut platform = new_platform(None, pipeline::Mode::ToGit); platform.set_resource( gix_hash::Kind::Sha1.null(), @@ -676,9 +676,7 @@ mod prepare_merge { &gix_object::find::Never, )?; - let state = platform - .prepare_merge(&gix_object::find::Never, Default::default()) - .expect("no validation is done here, let the caller inspect"); + let state = platform.prepare_merge(&gix_object::find::Never, Default::default())?; assert_eq!(state.ancestor.data, resource::Data::Missing); assert_eq!(state.current.data, resource::Data::Missing); assert_eq!(state.other.data, resource::Data::Missing); @@ -686,7 +684,7 @@ mod prepare_merge { } #[test] - fn driver_selection() -> Result { + fn driver_selection() -> TestResult { let mut platform = new_platform( [ gix_merge::blob::Driver { diff --git a/gix-merge/tests/merge/tree/cartesian.rs b/gix-merge/tests/merge/tree/cartesian.rs index 876df0ff574..5fa3a181067 100644 --- a/gix-merge/tests/merge/tree/cartesian.rs +++ b/gix-merge/tests/merge/tree/cartesian.rs @@ -40,7 +40,7 @@ struct Outcome { /// See `tree-cartesian-baseline.sh` for the model and `cartesian-baseline.txt` for the metrics and /// all observed differences. Set `GIX_MERGE_UPDATE_CARTESIAN_BASELINE=1` to accept a new status quo. #[test] -fn records_status_quo_sha1() -> Result { +fn records_status_quo_sha1() -> gix_testtools::TestResult { if gix_testtools::object_hash() != gix_hash::Kind::Sha1 { return Ok(()); } @@ -858,7 +858,7 @@ fn records_status_quo_sha1() -> Result { /// load the repository behind a submodule today. /// TODO: Allow callers to provide a callback with enough submodule-repository context to perform reachability checks. #[test] -fn records_submodule_status_quo_sha1() -> Result { +fn records_submodule_status_quo_sha1() -> gix_testtools::TestResult { if gix_testtools::object_hash() != gix_hash::Kind::Sha1 { return Ok(()); } diff --git a/gix-merge/tests/merge/tree/mod.rs b/gix-merge/tests/merge/tree/mod.rs index 93083d3f272..29086478257 100644 --- a/gix-merge/tests/merge/tree/mod.rs +++ b/gix-merge/tests/merge/tree/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use gix_diff::Rewrites; @@ -40,7 +40,7 @@ fn assert_no_unknown_conflicts(outcome: &gix_merge::tree::Outcome<'_>, context: /// 5. Validate that all tests are still working, and adjust the expected number of cases /// in the assertion that would then fail. #[test] -fn run_baseline() -> Result { +fn run_baseline() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("tree-baseline.sh")?; let cases = std::fs::read_to_string(root.join("baseline.cases"))?; let mut actual_cases = 0; @@ -237,7 +237,7 @@ fn run_baseline() -> Result { } #[test] -fn unrelated_commits_require_explicit_permission() -> gix_error::TestResult { +fn unrelated_commits_require_explicit_permission() -> TestResult { let root = gix_testtools::tempfile::TempDir::new()?; let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, gix_testtools::object_hash()); let tree_id = objects.write_buf(gix_object::Kind::Tree, b"")?; diff --git a/gix-negotiate/tests/baseline/mod.rs b/gix-negotiate/tests/baseline/mod.rs index 6b571977591..5167d6d800b 100644 --- a/gix-negotiate/tests/baseline/mod.rs +++ b/gix-negotiate/tests/baseline/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::cell::RefCell; use gix_negotiate::Algorithm; @@ -6,7 +6,7 @@ use gix_object::{FindExt, bstr, bstr::ByteSlice}; use gix_ref::{file::ReferenceExt, store::WriteReflog}; #[test] -fn run() -> Result { +fn run() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_repos.sh")?; for case in [ "no_parents", @@ -74,7 +74,7 @@ fn run() -> Result { // } for tip in lookup_names(&["HEAD"]).into_iter().chain( refs.iter()? - .prefixed(b"refs/heads".try_into().unwrap())? + .prefixed(b"refs/heads".try_into()?)? .filter_map(std::result::Result::ok) .map(|r| r.target.into_id()), ) { @@ -114,7 +114,7 @@ fn run() -> Result { }; haves = lookup_names(&["c2side", "c5", "origin/main"]) .into_iter() - .chain(Some(gix_hash::ObjectId::from_hex(id.as_bytes()).unwrap())) + .chain(Some(gix_hash::ObjectId::from_hex(id.as_bytes())?)) .collect(); } } diff --git a/gix-negotiate/tests/negotiate.rs b/gix-negotiate/tests/negotiate.rs index 8526e92a724..468d1bbfd71 100644 --- a/gix-negotiate/tests/negotiate.rs +++ b/gix-negotiate/tests/negotiate.rs @@ -1,4 +1,4 @@ -use gix_testtools::{Result, size_ok}; +use gix_testtools::{TestResult, size_ok}; mod window_size { use gix_negotiate::window_size; diff --git a/gix-note/tests/note.rs b/gix-note/tests/note.rs index 95a8adb3703..7b05aac328a 100644 --- a/gix-note/tests/note.rs +++ b/gix-note/tests/note.rs @@ -1,17 +1,17 @@ -use gix_error::Result; +use gix_error::{Result, bail}; use std::{ cell::Cell, collections::BTreeSet, io::{self, Read}, }; -use gix_error::bail; use gix_hash::{Kind, ObjectId, oid}; use gix_object::{ FindExt, Tree, Write, bstr::{BString, ByteSlice}, tree::{Entry, EntryKind}, }; +use gix_testtools::TestResult; type ObjectDb = gix_odb::memory::Proxy<gix_object::find::Never>; @@ -112,27 +112,27 @@ impl gix_object::Write for CountingObjectDb { } #[test] -fn reads_notes_without_fanout() -> gix_testtools::Result { - assert_note_at_fanout(0) +fn reads_notes_without_fanout() -> gix_testtools::TestResult { + assert_note_at_fanout(0).map_err(Into::into) } #[test] -fn reads_notes_with_one_fanout_level() -> gix_testtools::Result { - assert_note_at_fanout(1) +fn reads_notes_with_one_fanout_level() -> gix_testtools::TestResult { + assert_note_at_fanout(1).map_err(Into::into) } #[test] -fn reads_notes_with_two_fanout_levels() -> gix_testtools::Result { - assert_note_at_fanout(2) +fn reads_notes_with_two_fanout_levels() -> gix_testtools::TestResult { + assert_note_at_fanout(2).map_err(Into::into) } #[test] -fn reads_notes_with_three_fanout_levels() -> gix_testtools::Result { - assert_note_at_fanout(3) +fn reads_notes_with_three_fanout_levels() -> gix_testtools::TestResult { + assert_note_at_fanout(3).map_err(Into::into) } #[test] -fn replacing_a_note_does_not_read_untouched_fanout_subtrees() -> gix_testtools::Result { +fn replacing_a_note_does_not_read_untouched_fanout_subtrees() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; @@ -198,7 +198,7 @@ fn replacing_a_note_does_not_read_untouched_fanout_subtrees() -> gix_testtools:: } #[test] -fn state_reuses_materialized_trees_across_operations() -> gix_testtools::Result { +fn state_reuses_materialized_trees_across_operations() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; @@ -259,7 +259,7 @@ fn state_reuses_materialized_trees_across_operations() -> gix_testtools::Result } #[test] -fn staged_edits_reuse_state_without_writing_trees() -> gix_testtools::Result { +fn staged_edits_reuse_state_without_writing_trees() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let note_blob_id = objects.write_buf(gix_object::Kind::Blob, b"note")?; @@ -356,7 +356,7 @@ fn staged_edits_reuse_state_without_writing_trees() -> gix_testtools::Result { } #[test] -fn state_recovers_after_failed_operations() -> gix_testtools::Result { +fn state_recovers_after_failed_operations() -> TestResult { let kind = gix_testtools::object_hash(); let objects = CountingObjectDb::new(kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; @@ -448,7 +448,7 @@ fn notes_tree(objects: &impl Write, annotated: &oid, note: ObjectId, fanout: usi } #[test] -fn ignores_entries_that_are_not_notes() -> gix_testtools::Result { +fn ignores_entries_that_are_not_notes() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, kind); let annotated = gix_object::compute_hash(kind, gix_object::Kind::Blob, b"annotated")?; @@ -470,7 +470,7 @@ fn ignores_entries_that_are_not_notes() -> gix_testtools::Result { } #[test] -fn mutations_rebalance_when_each_nibble_bucket_has_multiple_notes_and_preserve_non_notes() -> gix_testtools::Result { +fn mutations_rebalance_when_each_nibble_bucket_has_multiple_notes_and_preserve_non_notes() -> TestResult { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let unrelated = objects.write_buf(gix_object::Kind::Blob, b"keep")?; @@ -584,7 +584,7 @@ fn mutations_rebalance_when_each_nibble_bucket_has_multiple_notes_and_preserve_n } #[test] -fn mutation_fanout_matches_git_at_bucket_count_boundaries() -> gix_testtools::Result { +fn mutation_fanout_matches_git_at_bucket_count_boundaries() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_notes_fanout_repo.sh")?; let baseline = std::fs::read_to_string(fixture.join("fanout.baseline"))?; let mut lines = baseline.lines(); @@ -672,7 +672,7 @@ fn mutation_fanout_matches_git_at_bucket_count_boundaries() -> gix_testtools::Re } #[test] -fn edit_lifecycle_handles_empty_trees_replacements_and_no_op_removals() -> gix_testtools::Result { +fn edit_lifecycle_handles_empty_trees_replacements_and_no_op_removals() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let root = objects.write(&Tree { entries: Vec::new() })?; @@ -738,7 +738,7 @@ fn edit_lifecycle_handles_empty_trees_replacements_and_no_op_removals() -> gix_t } #[test] -fn mutations_create_and_collapse_mixed_deep_fanout() -> gix_testtools::Result { +fn mutations_create_and_collapse_mixed_deep_fanout() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let note = objects.write_buf(gix_object::Kind::Blob, b"note")?; @@ -803,7 +803,7 @@ fn mutations_create_and_collapse_mixed_deep_fanout() -> gix_testtools::Result { } #[test] -fn mutations_preserve_non_notes_at_root_and_below_hex_trees() -> gix_testtools::Result { +fn mutations_preserve_non_notes_at_root_and_below_hex_trees() -> gix_testtools::TestResult { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let payload = objects.write_buf(gix_object::Kind::Blob, b"keep")?; @@ -876,7 +876,7 @@ fn mutations_preserve_non_notes_at_root_and_below_hex_trees() -> gix_testtools:: } #[test] -fn mutations_reject_mixed_hash_kinds() -> gix_testtools::Result { +fn mutations_reject_mixed_hash_kinds() -> TestResult { let objects = ObjectDb::new(gix_object::find::Never, Kind::Sha1); let root = objects.write(&Tree { entries: Vec::new() })?; let sha1 = ObjectId::null(Kind::Sha1); @@ -918,7 +918,7 @@ fn mutations_reject_mixed_hash_kinds() -> gix_testtools::Result { #[test] #[cfg(feature = "sha256")] -fn edits_support_sha256_notes_trees() -> gix_testtools::Result { +fn edits_support_sha256_notes_trees() -> gix_testtools::TestResult { let kind = Kind::Sha256; let objects = ObjectDb::new(gix_object::find::Never, kind); let root = objects.write(&Tree { entries: Vec::new() })?; @@ -944,7 +944,7 @@ fn edits_support_sha256_notes_trees() -> gix_testtools::Result { } #[test] -fn mutations_reject_duplicate_mappings_across_layouts() -> gix_testtools::Result { +fn mutations_reject_duplicate_mappings_across_layouts() -> TestResult { let kind = gix_testtools::object_hash(); let objects = ObjectDb::new(gix_object::find::Never, kind); let annotated = ObjectId::null(kind); diff --git a/gix-object/src/signature/mod.rs b/gix-object/src/signature/mod.rs index 883568269b3..d7b0c7c7b6e 100644 --- a/gix-object/src/signature/mod.rs +++ b/gix-object/src/signature/mod.rs @@ -13,9 +13,9 @@ pub mod sign; pub mod verify; #[cfg(feature = "signature")] -fn ssh_path_argument(path: &std::path::Path) -> std::path::PathBuf { +fn ssh_path_argument(path: &std::path::Path) -> Result<std::path::PathBuf> { // The mixed `C:/…` form works with native Windows and Git for Windows' MSYS OpenSSH. - gix_path::from_bstring(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path)).into_owned()) + gix_path::from_bstring(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path)?).into_owned()) } /// A borrowed armored signature and its detected format. diff --git a/gix-object/src/signature/sign.rs b/gix-object/src/signature/sign.rs index 6ca69e8a63b..7cba78b48f2 100644 --- a/gix-object/src/signature/sign.rs +++ b/gix-object/src/signature/sign.rs @@ -138,7 +138,7 @@ fn sign_ssh(payload: &[u8], options: &Options) -> Result<BString> { // Unlike literal keys, resolved key paths can be passed directly to `ssh-keygen -f`. None => (options.signing_key.clone(), false), }; - let key = super::ssh_path_argument(std::path::Path::new(&key)); + let key = super::ssh_path_argument(std::path::Path::new(&key))?; let mut payload_file = secure_temporary_file()?; write_temporary(&mut payload_file, payload)?; let payload_path = temporary_path(&mut payload_file)?; @@ -150,7 +150,7 @@ fn sign_ssh(payload: &[u8], options: &Options) -> Result<BString> { command = command.arg("-U"); } let output = command - .arg(super::ssh_path_argument(&payload_path)) + .arg(super::ssh_path_argument(&payload_path)?) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -239,7 +239,7 @@ mod tests { use super::*; #[test] - fn missing_signing_program_is_reported_only_in_metadata() -> gix_error::TestResult { + fn missing_signing_program_is_reported_only_in_metadata() -> gix_testtools::TestResult { let directory = gix_testtools::tempfile::TempDir::new()?; let program = directory.path().join("missing signer"); for format in [Format::OpenPgp, Format::X509, Format::Ssh] { @@ -272,7 +272,7 @@ mod tests { } #[test] - fn programs_with_spaces_are_invoked_directly() { + fn programs_with_spaces_are_invoked_directly() -> gix_testtools::TestResult { let program = OsStr::new("a directory/signer"); let command: std::process::Command = command(&Options { format: Format::OpenPgp, @@ -281,7 +281,7 @@ mod tests { signing_key: "key".into(), environment: Vec::new(), }) - .into(); + .try_into()?; assert_eq!( command.get_program(), @@ -293,5 +293,6 @@ mod tests { [OsStr::new("argument with spaces")], "signer arguments remain separate from the program pathname" ); + Ok(()) } } diff --git a/gix-object/src/signature/verify.rs b/gix-object/src/signature/verify.rs index 4e02d3fa009..ce8c486ec7c 100644 --- a/gix-object/src/signature/verify.rs +++ b/gix-object/src/signature/verify.rs @@ -289,9 +289,11 @@ impl SignedData<'_> { .or_raise(|| message("Signature time could not be formatted for SSH verification"))?; let verify_time = format!("-Overify-time={verify_time}"); let mut signature_file = signature_file(signature)?; - let signature_path = super::ssh_path_argument(&signature_path(&mut signature_file)?); - let allowed_signers = super::ssh_path_argument(&allowed_signers); - let revocation_file = revocation_file.map(|path| super::ssh_path_argument(&path)); + let signature_path = super::ssh_path_argument(&signature_path(&mut signature_file)?)?; + let allowed_signers = super::ssh_path_argument(&allowed_signers)?; + let revocation_file = revocation_file + .map(|path| super::ssh_path_argument(&path)) + .transpose()?; // defensive, as we rely on English when parsing output. environment.extend([("LANG".into(), "C".into()), ("LC_ALL".into(), "C".into())]); let common = ( @@ -574,7 +576,6 @@ fn parse_ssh_output(output: BString, signer: Option<BString>, trust_level: Trust #[cfg(test)] mod tests { use super::*; - use gix_error::Result; #[test] fn parses_gpg_status() { @@ -652,7 +653,7 @@ mod tests { } #[test] - fn rejects_good_ssh_output_from_a_failed_verifier() -> Result { + fn rejects_good_ssh_output_from_a_failed_verifier() -> gix_testtools::TestResult { let signed = SignedData::new(b"payloadsignature", 7..16); let outcome = signed.verify( BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), @@ -676,9 +677,9 @@ mod tests { } #[test] - fn programs_with_spaces_are_invoked_directly() { + fn programs_with_spaces_are_invoked_directly() -> gix_testtools::TestResult { let program = OsStr::new("a directory/verifier"); - let command: std::process::Command = prepare(program, ["argument with spaces".into()], &[]).into(); + let command: std::process::Command = prepare(program, ["argument with spaces".into()], &[]).try_into()?; assert_eq!( command.get_program(), @@ -690,6 +691,7 @@ mod tests { [OsStr::new("argument with spaces")], "verifier arguments remain separate from the program pathname" ); + Ok(()) } #[test] diff --git a/gix-object/tests/object/commit/from_bytes.rs b/gix-object/tests/object/commit/from_bytes.rs index 62016fc05b8..2a2651e7925 100644 --- a/gix-object/tests/object/commit/from_bytes.rs +++ b/gix-object/tests/object/commit/from_bytes.rs @@ -11,10 +11,9 @@ use crate::{ }; #[test] -fn invalid_timestsamp() { +fn invalid_timestsamp() -> gix_testtools::TestResult { assert_eq!( - CommitRef::from_bytes(&fixture_name("commit", "invalid-timestamp.txt"), gix_hash::Kind::Sha1) - .expect("auto-correct invalid timestamp by discarding it (time is still valid UTC)"), + CommitRef::from_bytes(&fixture_name("commit", "invalid-timestamp.txt"), gix_hash::Kind::Sha1)?, CommitRef { tree: b"7989dfb2ec2f41914611a22fb30bbc2b3849df9a".as_bstr(), parents: [b"8845ae683e2688bc619baade49510c17e978518f".as_bstr()].into(), @@ -26,10 +25,11 @@ fn invalid_timestsamp() { }, "the offset of the actor is null, leaving the UTC time" ); + Ok(()) } #[test] -fn sha256_with_all_fields_and_signature() -> Result { +fn sha256_with_all_fields_and_signature() -> gix_testtools::TestResult { let input = b"tree 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef parent 1111111111111111111111111111111111111111111111111111111111111111 parent 2222222222222222222222222222222222222222222222222222222222222222 @@ -98,7 +98,7 @@ U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgZXhhbXBsZS1zaGEyNTY= } #[test] -fn uppercase_tree_id() -> Result { +fn uppercase_tree_id() -> gix_testtools::TestResult { let input = b"tree 7989DFB2EC2F41914611A22FB30BBC2B3849DF9A author Name <name@example.com> 1312735823 +0518 committer Name <name@example.com> 1312735823 +0518 @@ -111,7 +111,7 @@ message"; } #[test] -fn invalid_email_of_committer() -> Result { +fn invalid_email_of_committer() -> gix_testtools::TestResult { let actor = gix_actor::SignatureRef { name: b"Gregor Hartmann".as_bstr(), email: b"gh <Gregor Hartmann<gh@openoffice.org".as_bstr(), @@ -120,7 +120,7 @@ fn invalid_email_of_committer() -> Result { let mut buf = vec![]; let backing = fixture_name("commit", "invalid-actor.txt"); - let commit = CommitRef::from_bytes(&backing, gix_hash::Kind::Sha1).expect("ignore strangely formed actor format"); + let commit = CommitRef::from_bytes(&backing, gix_hash::Kind::Sha1)?; assert_eq!( commit, CommitRef { @@ -136,9 +136,9 @@ fn invalid_email_of_committer() -> Result { assert_eq!(commit.author()?, actor); assert_eq!(commit.committer()?, actor); - commit.write_to(&mut buf).expect("we can write invalid actors back"); + commit.write_to(&mut buf)?; assert_eq!( - CommitRef::from_bytes(&buf, gix_hash::Kind::Sha1).expect("this is the same commit and it can be parsed"), + CommitRef::from_bytes(&buf, gix_hash::Kind::Sha1)?, commit, "round-tripping works" ); @@ -147,7 +147,7 @@ fn invalid_email_of_committer() -> Result { } #[test] -fn unsigned() -> Result { +fn unsigned() -> gix_testtools::TestResult { let tree = fixture_oid_hex("1b2dfb4ac5e42080b682fc676e9738c94ce6d54d"); assert_eq!( CommitRef::from_bytes(&commit_fixture("unsigned.txt")?, crate::fixture_hash_kind())?, @@ -165,7 +165,7 @@ fn unsigned() -> Result { } #[test] -fn whitespace() -> Result { +fn whitespace() -> gix_testtools::TestResult { let tree = fixture_oid_hex("9bed6275068a0575243ba8409253e61af81ab2ff"); let parent = fixture_oid_hex("26b4df046d1776c123ac69d918f5aec247b58cc6"); assert_eq!( @@ -184,7 +184,7 @@ fn whitespace() -> Result { } #[test] -fn signed_singleline() -> Result { +fn signed_singleline() -> gix_testtools::TestResult { let tree = fixture_oid_hex("00fc39317701176e326974ce44f5bd545a32ec0b"); let parent = fixture_oid_hex("09d8d3a12e161a7f6afb522dbe8900a9c09bce06"); assert_eq!( @@ -203,7 +203,7 @@ fn signed_singleline() -> Result { } #[test] -fn mergetag() -> Result { +fn mergetag() -> gix_testtools::TestResult { let fixture = commit_fixture("mergetag.txt")?; let tree = fixture_oid_hex("1c61918031bf2c7fab9e17dde3c52a6a9884fcb5"); let parent_a = fixture_oid_hex("44ebe016df3aad96e3be8f95ec52397728dd7701"); @@ -230,7 +230,7 @@ fn mergetag() -> Result { } #[test] -fn signed() -> Result { +fn signed() -> gix_testtools::TestResult { let tree = fixture_oid_hex("00fc39317701176e326974ce44f5bd545a32ec0b"); let parent = fixture_oid_hex("09d8d3a12e161a7f6afb522dbe8900a9c09bce06"); assert_eq!( @@ -249,7 +249,7 @@ fn signed() -> Result { } #[test] -fn signed_with_encoding() -> Result { +fn signed_with_encoding() -> gix_testtools::TestResult { let tree = fixture_oid_hex("1973afa74d87b2bb73fa884aaaa8752aec43ea88"); let parent = fixture_oid_hex("79c51cc86923e2b8ca0ee5c4eb75e48027133f9a"); assert_eq!( @@ -268,7 +268,7 @@ fn signed_with_encoding() -> Result { } #[test] -fn with_encoding() -> Result { +fn with_encoding() -> gix_testtools::TestResult { let tree = fixture_oid_hex("4a1c03029e7407c0afe9fc0320b3258e188b115e"); let parent = fixture_oid_hex("7ca98aad461a5c302cb4c9e3acaaa6053cc67a62"); assert_eq!( @@ -287,7 +287,7 @@ fn with_encoding() -> Result { } #[test] -fn pre_epoch() -> Result { +fn pre_epoch() -> gix_testtools::TestResult { let tree = fixture_oid_hex("71cdd4015386b764b178005cad4c88966bc9d61a"); assert_eq!( CommitRef::from_bytes(&commit_fixture("pre-epoch.txt")?, crate::fixture_hash_kind())?, @@ -305,7 +305,7 @@ fn pre_epoch() -> Result { } #[test] -fn double_dash_special_time_offset() -> Result { +fn double_dash_special_time_offset() -> gix_testtools::TestResult { assert_eq!( CommitRef::from_bytes( &fixture_name("commit", "double-dash-date-offset.txt"), @@ -325,7 +325,7 @@ fn double_dash_special_time_offset() -> Result { } #[test] -fn with_trailer() -> Result { +fn with_trailer() -> gix_testtools::TestResult { let kim = SignatureRef { name: "Kim Altintop".into(), email: "kim@eagain.st".into(), @@ -413,7 +413,7 @@ instead of depending directly on the lower-level crates. } #[test] -fn merge() -> Result { +fn merge() -> gix_testtools::TestResult { let tree = fixture_oid_hex("0cf16ce8e229b59a761198975f0c0263229faf82"); let parent_a = fixture_oid_hex("6a6054db4ce3c1e4e6a37f8c4d7acb63a4d6ad71"); let parent_b = fixture_oid_hex("c91d592913d47ac4e4a76daf16fd649b276e211e"); @@ -433,7 +433,7 @@ fn merge() -> Result { } #[test] -fn newline_right_after_signature_multiline_header_sha1() -> Result { +fn newline_right_after_signature_multiline_header_sha1() -> gix_testtools::TestResult { let fixture = commit_fixture("signed-whitespace.txt")?; let commit = CommitRef::from_bytes(&fixture, crate::fixture_hash_kind())?; let pgp_sig = crate::commit::OTHER_SIGNATURE.as_bstr(); @@ -455,7 +455,7 @@ fn newline_right_after_signature_multiline_header_sha1() -> Result { #[test] #[cfg(feature = "sha256")] -fn sha256_commits_use_their_own_signature_header() -> Result { +fn sha256_commits_use_their_own_signature_header() -> gix_testtools::TestResult { let data = b"tree 0000000000000000000000000000000000000000000000000000000000000000\n\ author A <a@example.com> 0 +0000\n\ committer A <a@example.com> 0 +0000\n\ @@ -468,7 +468,7 @@ gpgsig-sha256 signature\n\ } #[test] -fn bogus_multi_gpgsig_header_sha1() -> Result { +fn bogus_multi_gpgsig_header_sha1() -> gix_testtools::TestResult { let fixture = commit_fixture("bogus-gpgsig-lines-in-git.git.txt")?; let commit = CommitRef::from_bytes(&fixture, crate::fixture_hash_kind())?; let pgp_sig = b"-----BEGIN PGP SIGNATURE-----".as_bstr(); diff --git a/gix-object/tests/object/commit/iter.rs b/gix-object/tests/object/commit/iter.rs index f8087c40164..e9e6d7598c8 100644 --- a/gix-object/tests/object/commit/iter.rs +++ b/gix-object/tests/object/commit/iter.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_object::{CommitRefIter, bstr::ByteSlice, commit::ref_iter::Token}; use crate::{ @@ -7,7 +6,7 @@ use crate::{ }; #[test] -fn newline_right_after_signature_multiline_header() -> Result { +fn newline_right_after_signature_multiline_header() -> gix_testtools::TestResult { let data = fixture_name("commit", "signed-whitespace.txt"); let tokens = CommitRefIter::from_bytes(&data, gix_hash::Kind::Sha1).collect::<std::result::Result<Vec<_>, _>>()?; assert_eq!(tokens.len(), 7, "mainly a parsing exercise"); @@ -21,7 +20,7 @@ fn newline_right_after_signature_multiline_header() -> Result { } #[test] -fn signed_with_encoding() -> Result { +fn signed_with_encoding() -> gix_testtools::TestResult { let input = fixture_name("commit", "signed-with-encoding.txt"); let iter = CommitRefIter::from_bytes(&input, gix_hash::Kind::Sha1); assert_eq!( @@ -51,7 +50,7 @@ fn signed_with_encoding() -> Result { } #[test] -fn whitespace() -> Result { +fn whitespace() -> gix_testtools::TestResult { assert_eq!( CommitRefIter::from_bytes(&fixture_name("commit", "whitespace.txt"), gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -75,7 +74,7 @@ fn whitespace() -> Result { } #[test] -fn unsigned() -> Result { +fn unsigned() -> gix_testtools::TestResult { assert_eq!( CommitRefIter::from_bytes(&fixture_name("commit", "unsigned.txt"), gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -96,7 +95,7 @@ fn unsigned() -> Result { } #[test] -fn signed_singleline() -> Result { +fn signed_singleline() -> gix_testtools::TestResult { assert_eq!( CommitRefIter::from_bytes(&fixture_name("commit", "signed-singleline.txt"), gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -127,7 +126,7 @@ fn signed_singleline() -> Result { } #[test] -fn error_handling() -> Result { +fn error_handling() -> gix_testtools::TestResult { let data = fixture_name("commit", "unsigned.txt"); let iter = CommitRefIter::from_bytes(&data[..data.len() / 2], gix_hash::Kind::Sha1); let tokens = iter.collect::<Vec<_>>(); @@ -139,7 +138,7 @@ fn error_handling() -> Result { } #[test] -fn mergetag() -> Result { +fn mergetag() -> gix_testtools::TestResult { let input = fixture_name("commit", "mergetag.txt"); let iter = CommitRefIter::from_bytes(&input, gix_hash::Kind::Sha1); assert_eq!( @@ -176,13 +175,12 @@ fn mergetag() -> Result { } mod method { - use crate::Result; use gix_object::CommitRefIter; use crate::{fixture_name, hex_to_id, signature}; #[test] - fn tree_id() -> Result { + fn tree_id() -> gix_testtools::TestResult { let input = fixture_name("commit", "unsigned.txt"); let iter = CommitRefIter::from_bytes(&input, gix_hash::Kind::Sha1); assert_eq!( @@ -198,7 +196,7 @@ mod method { } #[test] - fn signatures() -> Result { + fn signatures() -> gix_testtools::TestResult { let input = fixture_name("commit", "unsigned.txt"); let iter = CommitRefIter::from_bytes(&input, gix_hash::Kind::Sha1); assert_eq!( @@ -248,28 +246,28 @@ mod method { } #[test] - fn single_line() -> Result { - validate("signed-singleline.txt", b"magic:signature", 4..=4) + fn single_line() -> gix_testtools::TestResult { + Ok(validate("signed-singleline.txt", b"magic:signature", 4..=4)?) } #[test] - fn signed() -> Result { - validate("signed.txt", b"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEEdjYp/sh4j8NRKLX27gKdHl60AwAFAl7p9tgACgkQ7gKdHl60\nAwBpegf+KQciv9AOIN7+yPmowecGxBnSfpKWTDzFxnyGR8dq63SpWT8WEKG5mf3a\nG6iUqpsDWaMHlzihaMKRvgRpZxFRbjnNPFBj6F4RRqfE+5R7k6DRSLUV5PqnsdSH\nuccfIDWi1imhsm7AaP5trwl1t+83U2JhHqPcPVFLMODYwWeO6NLR/JCzGSTQRa8t\nRgaVMKI19O/fge5OT5Ua8D47VKEhsJX0LfmkP5RfZQ8JJvNd40TupqKRdlv0sAzP\nya7NXkSHXCavHNR6kA+KpWxn900UoGK8/IDlwU6MeOkpPVawb3NFMqnc7KJDaC2p\nSMzpuEG8LTrCx2YSpHNLqHyzvQ1CZA==\n=5ITV\n-----END PGP SIGNATURE-----\n", 4..=14) + fn signed() -> gix_testtools::TestResult { + Ok(validate("signed.txt", b"-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCAAdFiEEdjYp/sh4j8NRKLX27gKdHl60AwAFAl7p9tgACgkQ7gKdHl60\nAwBpegf+KQciv9AOIN7+yPmowecGxBnSfpKWTDzFxnyGR8dq63SpWT8WEKG5mf3a\nG6iUqpsDWaMHlzihaMKRvgRpZxFRbjnNPFBj6F4RRqfE+5R7k6DRSLUV5PqnsdSH\nuccfIDWi1imhsm7AaP5trwl1t+83U2JhHqPcPVFLMODYwWeO6NLR/JCzGSTQRa8t\nRgaVMKI19O/fge5OT5Ua8D47VKEhsJX0LfmkP5RfZQ8JJvNd40TupqKRdlv0sAzP\nya7NXkSHXCavHNR6kA+KpWxn900UoGK8/IDlwU6MeOkpPVawb3NFMqnc7KJDaC2p\nSMzpuEG8LTrCx2YSpHNLqHyzvQ1CZA==\n=5ITV\n-----END PGP SIGNATURE-----\n", 4..=14)?) } #[test] - fn with_encoding() -> Result { - validate("signed-with-encoding.txt", SIGNATURE, 5..=15) + fn with_encoding() -> gix_testtools::TestResult { + Ok(validate("signed-with-encoding.txt", SIGNATURE, 5..=15)?) } #[test] - fn msg_footer() -> Result { - validate("message-with-footer.txt", b"-----BEGIN PGP SIGNATURE-----\n\niHUEABYIAB0WIQSuZwcGWSQItmusNgR5URpSUCnwXQUCYT7xpAAKCRB5URpSUCnw\nXWB3AP9q323HlxnI8MyqszNOeYDwa7Y3yEZaUM2y/IRjz+z4YQEAq0yr1Syt3mrK\nOSFCqL2vDm3uStP+vF31f6FnzayhNg0=\n=Mhpp\n-----END PGP SIGNATURE-----\n", 4..=10) + fn msg_footer() -> gix_testtools::TestResult { + Ok(validate("message-with-footer.txt", b"-----BEGIN PGP SIGNATURE-----\n\niHUEABYIAB0WIQSuZwcGWSQItmusNgR5URpSUCnwXQUCYT7xpAAKCRB5URpSUCnw\nXWB3AP9q323HlxnI8MyqszNOeYDwa7Y3yEZaUM2y/IRjz+z4YQEAq0yr1Syt3mrK\nOSFCqL2vDm3uStP+vF31f6FnzayhNg0=\n=Mhpp\n-----END PGP SIGNATURE-----\n", 4..=10)?) } #[test] - fn whitespace() -> Result { - validate("signed-whitespace.txt", OTHER_SIGNATURE, 5..=15) + fn whitespace() -> gix_testtools::TestResult { + Ok(validate("signed-whitespace.txt", OTHER_SIGNATURE, 5..=15)?) } } } diff --git a/gix-object/tests/object/commit/mod.rs b/gix-object/tests/object/commit/mod.rs index 5c3b58ac49f..1c04e73c2b5 100644 --- a/gix-object/tests/object/commit/mod.rs +++ b/gix-object/tests/object/commit/mod.rs @@ -153,14 +153,14 @@ iyBBl69jASy41Ug/BlFJbw4+ItkShpXwkJKuBBV/JExChmvbxYWaS7QnyYC9UO0= "; mod method { - use crate::Result; + use gix_object::CommitRef; use pretty_assertions::assert_eq; use crate::{fixture_name, signature}; #[test] - fn tree() -> Result { + fn tree() -> gix_testtools::TestResult { let fixture = fixture_name("commit", "unsigned.txt"); let commit = CommitRef::from_bytes(&fixture, gix_hash::Kind::Sha1)?; assert_eq!(commit.tree(), "1b2dfb4ac5e42080b682fc676e9738c94ce6d54d"); @@ -169,7 +169,7 @@ mod method { } #[test] - fn author_and_committer_trims_signature() -> Result { + fn author_and_committer_trims_signature() -> gix_testtools::TestResult { let backing = fixture_name("commit", "email-with-space.txt"); let commit = CommitRef::from_bytes(&backing, gix_hash::Kind::Sha1)?; std::assert_eq!(commit.author()?, signature("1592437401 +0800")); diff --git a/gix-object/tests/object/commit/signature.rs b/gix-object/tests/object/commit/signature.rs index d8a3d15e2d1..3d5ffe16600 100644 --- a/gix-object/tests/object/commit/signature.rs +++ b/gix-object/tests/object/commit/signature.rs @@ -11,7 +11,7 @@ use gix_testtools::signature; use crate::Result; #[test] -fn ssh() -> Result { +fn ssh() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } @@ -32,7 +32,7 @@ fn ssh() -> Result { } #[test] -fn openpgp() -> Result { +fn openpgp() -> gix_testtools::TestResult { if !signature::program_available("gpg") { return Ok(()); } @@ -63,7 +63,7 @@ fn openpgp() -> Result { } #[test] -fn x509() -> Result { +fn x509() -> gix_testtools::TestResult { if !signature::program_available("gpgsm") { return Ok(()); } @@ -93,7 +93,7 @@ fn x509() -> Result { } #[test] -fn replaces_the_active_signature() -> Result { +fn replaces_the_active_signature() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } @@ -127,7 +127,7 @@ fn replaces_the_active_signature() -> Result { } #[test] -fn sha256_uses_its_git_signature_header() -> Result { +fn sha256_uses_its_git_signature_header() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } diff --git a/gix-object/tests/object/encode.rs b/gix-object/tests/object/encode.rs index 74fb518f51a..567d36d7a22 100644 --- a/gix-object/tests/object/encode.rs +++ b/gix-object/tests/object/encode.rs @@ -7,7 +7,7 @@ fn try_from_error() -> std::io::Error { macro_rules! round_trip_with_hash_kind { ($owned:ty, $borrowed:ty, $( $files:literal ), +) => { #[test] - fn round_trip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + fn round_trip() -> gix_testtools::TestResult { use std::convert::TryFrom; use std::io::Write; use crate::object_fixture; @@ -106,7 +106,7 @@ mod tree { use gix_object::{WriteTo, tree, tree::EntryKind}; #[test] - fn write_to_does_not_validate() { + fn write_to_does_not_validate() -> gix_testtools::TestResult { let hash_kind = crate::fixture_hash_kind(); let mut tree = gix_object::Tree::empty(); tree.entries.push(tree::Entry { @@ -119,8 +119,8 @@ mod tree { filename: "something\nwith\newlines\n".into(), oid: hash_kind.empty_tree(), }); - tree.write_to(&mut std::io::sink()) - .expect("write succeeds, no validation is performed"); + tree.write_to(&mut std::io::sink())?; + Ok(()) } #[test] @@ -157,7 +157,7 @@ mod blob { use crate::fixture_bytes; #[test] - fn round_trip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + fn round_trip() -> gix_testtools::TestResult { let input_name = "tree/everything.tree"; let input = fixture_bytes(input_name); // It doesn't matter which data we use - it's not interpreted. @@ -224,7 +224,7 @@ mod loose_header { use gix_object::{Kind, decode, encode}; #[test] - fn round_trip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + fn round_trip() -> gix_testtools::TestResult { for (kind, size, expected) in &[ (Kind::Tree, 1234, "tree 1234\0".as_bytes()), (Kind::Blob, 0, b"blob 0\0"), diff --git a/gix-object/tests/object/main.rs b/gix-object/tests/object/main.rs index f9c69c09ce7..0ee517b569e 100644 --- a/gix-object/tests/object/main.rs +++ b/gix-object/tests/object/main.rs @@ -11,21 +11,22 @@ mod tag; mod tree; #[test] -fn compute_hash() { +fn compute_hash() -> gix_testtools::TestResult { for hk in gix_hash::Kind::all() { assert_eq!( - gix_object::compute_hash(*hk, gix_object::Kind::Blob, &[]).expect("empty hash doesn’t collide"), + gix_object::compute_hash(*hk, gix_object::Kind::Blob, &[])?, gix_hash::ObjectId::empty_blob(*hk) ); assert_eq!( - gix_object::compute_hash(*hk, gix_object::Kind::Tree, &[]).expect("empty hash doesn’t collide"), + gix_object::compute_hash(*hk, gix_object::Kind::Tree, &[])?, gix_hash::ObjectId::empty_tree(*hk) ); } + Ok(()) } #[test] -fn compute_stream_hash() { +fn compute_stream_hash() -> gix_testtools::TestResult { for hk in gix_hash::Kind::all() { assert_eq!( gix_object::compute_stream_hash( @@ -35,8 +36,7 @@ fn compute_stream_hash() { 0, &mut gix_features::progress::Discard, &AtomicBool::default() - ) - .expect("in-memory works"), + )?, gix_hash::ObjectId::empty_blob(*hk) ); assert_eq!( @@ -47,15 +47,15 @@ fn compute_stream_hash() { 0, &mut gix_features::progress::Discard, &AtomicBool::default() - ) - .expect("in-memory works"), + )?, gix_hash::ObjectId::empty_tree(*hk) ); } + Ok(()) } #[test] -fn never_writes_to_nowhere_and_finds_nothing() -> Result { +fn never_writes_to_nowhere_and_finds_nothing() -> gix_testtools::TestResult { let db = gix_object::find::Never; let data = b"content"; let expected = gix_object::compute_hash(gix_hash::Kind::default(), gix_object::Kind::Blob, data)?; diff --git a/gix-object/tests/object/signature_format.rs b/gix-object/tests/object/signature_format.rs index 66cde7d6b55..1b86cde7bb5 100644 --- a/gix-object/tests/object/signature_format.rs +++ b/gix-object/tests/object/signature_format.rs @@ -1,7 +1,7 @@ use gix_object::{bstr::BStr, signature::Format}; #[test] -fn parse() -> gix_error::TestResult { +fn parse() -> gix_testtools::TestResult { for (value, expected) in [ ("openpgp", Format::OpenPgp), ("OpenPGP", Format::OpenPgp), diff --git a/gix-object/tests/object/tag/mod.rs b/gix-object/tests/object/tag/mod.rs index 5fa00281ce9..2e6b17aca14 100644 --- a/gix-object/tests/object/tag/mod.rs +++ b/gix-object/tests/object/tag/mod.rs @@ -1,4 +1,3 @@ -use crate::Result; use crate::signature; use gix_object::{Kind, TagRef, TagRefIter, bstr::ByteSlice}; @@ -132,7 +131,7 @@ message\n" } #[test] -fn sha256_with_all_fields_and_signature() -> Result { +fn sha256_with_all_fields_and_signature() -> gix_testtools::TestResult { let input = b"object abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 type commit tag v2.0.0-sha256 @@ -187,7 +186,7 @@ sha256-tag-signature } #[test] -fn target() -> Result { +fn target() -> gix_testtools::TestResult { let fixture = fixture_name("tag", "signed.txt"); let tag_ref = TagRef::from_bytes(&fixture, gix_hash::Kind::Sha1)?; assert_eq!(tag_ref.target(), "ffa700b4aca13b80cb6b98a078e7c96804f8e0ec"); @@ -212,7 +211,7 @@ fn target() -> Result { } #[test] -fn tagger() -> Result { +fn tagger() -> gix_testtools::TestResult { let fixture = fixture_name("tag", "tagger-with-whitespace.txt"); let tag = TagRef::from_bytes(&fixture, gix_hash::Kind::Sha1)?; std::assert_eq!(tag.tagger()?, Some(signature("1592381636 +0800"))); @@ -220,13 +219,12 @@ fn tagger() -> Result { } mod iter { - use crate::Result; use gix_object::{Kind, TagRefIter, bstr::ByteSlice, tag::ref_iter::Token}; use crate::{fixture_name, hex_to_id, signature}; #[test] - fn empty() -> Result { + fn empty() -> gix_testtools::TestResult { let tag = fixture_name("tag", "empty.txt"); let tag_iter = TagRefIter::from_bytes(&tag, gix_hash::Kind::Sha1); let target_id = hex_to_id("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc"); @@ -250,7 +248,7 @@ mod iter { } #[test] - fn no_tagger() -> Result { + fn no_tagger() -> gix_testtools::TestResult { assert_eq!( TagRefIter::from_bytes(&fixture_name("tag", "no-tagger.txt"), gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -287,7 +285,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn whitespace() -> Result { + fn whitespace() -> gix_testtools::TestResult { assert_eq!( TagRefIter::from_bytes(&fixture_name("tag", "whitespace.txt"), gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -308,7 +306,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn pgp_begin_marker_not_at_line_start_is_message() -> Result { + fn pgp_begin_marker_not_at_line_start_is_message() -> gix_testtools::TestResult { assert_eq!( TagRefIter::from_bytes(super::PGP_BEGIN_NOT_AT_LINE_START, gix_hash::Kind::Sha1) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -329,7 +327,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn error_handling() -> Result { + fn error_handling() -> gix_testtools::TestResult { let data = fixture_name("tag", "empty.txt"); let iter = TagRefIter::from_bytes(&data[..data.len() / 3], gix_hash::Kind::Sha1); let tokens = iter.collect::<Vec<_>>(); @@ -356,7 +354,7 @@ fn invalid() { } #[test] -fn uppercase_target_id() -> Result { +fn uppercase_target_id() -> gix_testtools::TestResult { let input = b"object FFA700B4ACA13B80CB6B98A078E7C96804F8E0EC type commit tag uppercase-target @@ -388,7 +386,7 @@ mod from_bytes { use crate::{fixture_name, fixture_oid, object_fixture}; #[test] - fn signed() -> Result { + fn signed() -> gix_testtools::TestResult { let fixture = tag_fixture("signed.txt")?; let target = fixture_oid_hex("ffa700b4aca13b80cb6b98a078e7c96804f8e0ec"); assert_eq!( @@ -425,7 +423,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn empty() -> Result { + fn empty() -> gix_testtools::TestResult { let fixture = tag_fixture("empty.txt")?; let target = fixture_oid_hex("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc"); let tag_ref = TagRef::from_bytes(&fixture, crate::fixture_hash_kind())?; @@ -445,7 +443,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn empty_missing_nl() -> Result { + fn empty_missing_nl() -> gix_testtools::TestResult { let fixture = tag_fixture("empty_missing_nl.txt")?; let target = fixture_oid_hex("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc"); let tag_ref = TagRef::from_bytes(&fixture, crate::fixture_hash_kind())?; @@ -465,7 +463,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn with_newlines() -> Result { + fn with_newlines() -> gix_testtools::TestResult { let target = fixture_oid_hex("ebdf205038b66108c0331aa590388431427493b7"); assert_eq!( TagRef::from_bytes(&tag_fixture("with-newlines.txt")?, crate::fixture_hash_kind())?, @@ -482,7 +480,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn no_tagger() -> Result { + fn no_tagger() -> gix_testtools::TestResult { let target = fixture_oid_hex("c39ae07f393806ccf406ef966e9a15afc43cc36a"); assert_eq!( TagRef::from_bytes(&tag_fixture("no-tagger.txt")?, crate::fixture_hash_kind())?, @@ -515,7 +513,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn pgp_begin_marker_not_at_line_start_is_message() -> Result { + fn pgp_begin_marker_not_at_line_start_is_message() -> gix_testtools::TestResult { let tag = TagRef::from_bytes(super::PGP_BEGIN_NOT_AT_LINE_START, gix_hash::Kind::Sha1)?; assert_eq!(tag.message, super::PGP_BEGIN_NOT_AT_LINE_START_MESSAGE.as_bstr()); assert_eq!(tag.signature, None, "it doesn't parse this as PGP signature"); @@ -524,7 +522,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn trailing_text_after_pgp_end_marker_is_signature() -> Result { + fn trailing_text_after_pgp_end_marker_is_signature() -> gix_testtools::TestResult { let tag = TagRef::from_bytes(super::PGP_SIGNATURE_WITH_TRAILING_TEXT, gix_hash::Kind::Sha1)?; assert_eq!(tag.message, b"message text".as_bstr()); assert_eq!( @@ -536,7 +534,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn pgp_begin_marker_without_end_marker_starts_signature() -> Result { + fn pgp_begin_marker_without_end_marker_starts_signature() -> gix_testtools::TestResult { let tag = TagRef::from_bytes(super::PGP_SIGNATURE_WITHOUT_END_MARKER, gix_hash::Kind::Sha1)?; assert_eq!(tag.message, b"message text".as_bstr()); assert_eq!( @@ -548,7 +546,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn pgp_begin_marker_at_body_start_is_signature() -> Result { + fn pgp_begin_marker_at_body_start_is_signature() -> gix_testtools::TestResult { let tag = TagRef::from_bytes(super::PGP_SIGNATURE_AT_BODY_START, gix_hash::Kind::Sha1)?; assert_eq!(tag.message, b"".as_bstr()); assert_eq!( @@ -560,7 +558,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn whitespace() -> Result { + fn whitespace() -> gix_testtools::TestResult { let target = fixture_oid_hex("01dd4e2a978a9f5bd773dae6da7aa4a5ac1cdbbc"); assert_eq!( TagRef::from_bytes(&tag_fixture("whitespace.txt")?, crate::fixture_hash_kind())?, @@ -577,7 +575,7 @@ KLMHist5yj0sw1E4hDTyQa0= } #[test] - fn tagger_without_timestamp() -> Result { + fn tagger_without_timestamp() -> gix_testtools::TestResult { assert_eq!( TagRef::from_bytes( &fixture_name("tag", "tagger-without-timestamp.txt"), diff --git a/gix-object/tests/object/tag/signature.rs b/gix-object/tests/object/tag/signature.rs index 263d9fca942..359a2cc8d14 100644 --- a/gix-object/tests/object/tag/signature.rs +++ b/gix-object/tests/object/tag/signature.rs @@ -12,7 +12,7 @@ use gix_testtools::signature; use crate::Result; #[test] -fn ssh_and_tag_ref_api() -> Result { +fn ssh_and_tag_ref_api() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } @@ -32,7 +32,7 @@ fn ssh_and_tag_ref_api() -> Result { } #[test] -fn openpgp() -> Result { +fn openpgp() -> gix_testtools::TestResult { if !signature::program_available("gpg") { return Ok(()); } @@ -66,7 +66,7 @@ fn openpgp() -> Result { } #[test] -fn x509() -> Result { +fn x509() -> gix_testtools::TestResult { if !signature::program_available("gpgsm") { return Ok(()); } @@ -93,7 +93,7 @@ fn x509() -> Result { } #[test] -fn replaces_the_active_signature_and_tampering_is_invalid() -> Result { +fn replaces_the_active_signature_and_tampering_is_invalid() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } @@ -136,7 +136,7 @@ fn replaces_the_active_signature_and_tampering_is_invalid() -> Result { #[test] #[cfg(feature = "sha256")] -fn native_sha256_has_one_in_body_signature_and_no_compatibility_header() -> Result { +fn native_sha256_has_one_in_body_signature_and_no_compatibility_header() -> gix_testtools::TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } diff --git a/gix-object/tests/object/tree/editor.rs b/gix-object/tests/object/tree/editor.rs index 41128118e25..ead890e9221 100644 --- a/gix-object/tests/object/tree/editor.rs +++ b/gix-object/tests/object/tree/editor.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_object::{ Tree, tree::{Entry, EntryKind}, @@ -13,7 +12,7 @@ fn null_id() -> gix_hash::ObjectId { } #[test] -fn from_empty_cursor() -> Result { +fn from_empty_cursor() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); @@ -142,7 +141,7 @@ fn from_empty_cursor() -> Result { Ok(()) } #[test] -fn from_existing_cursor() -> Result { +fn from_existing_cursor() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; @@ -223,7 +222,7 @@ fn from_existing_cursor() -> Result { Ok(()) } #[test] -fn from_empty_removal() -> Result { +fn from_empty_removal() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); @@ -309,7 +308,7 @@ fn from_empty_removal() -> Result { } #[test] -fn from_empty_remove_accepts_empty_segments_after_unreachable_paths() -> Result { +fn from_empty_remove_accepts_empty_segments_after_unreachable_paths() -> gix_testtools::TestResult { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); @@ -336,7 +335,7 @@ fn from_empty_remove_accepts_empty_segments_after_unreachable_paths() -> Result } #[test] -fn remove_if_leaf_preserves_trees_and_removes_files() -> Result { +fn remove_if_leaf_preserves_trees_and_removes_files() -> gix_testtools::TestResult { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); @@ -382,7 +381,7 @@ fn remove_if_leaf_preserves_trees_and_removes_files() -> Result { } #[test] -fn from_empty_remove_leaf_rejects_tree_entries() -> Result { +fn from_empty_remove_leaf_rejects_tree_entries() -> gix_testtools::TestResult { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); @@ -434,7 +433,7 @@ fn from_empty_remove_leaf_rejects_tree_entries() -> Result { } #[test] -fn from_existing_remove() -> Result { +fn from_existing_remove() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; @@ -498,15 +497,14 @@ fn from_existing_remove() -> Result { Ok(()) } #[test] -fn from_empty_invalid_write() -> Result { +fn from_empty_invalid_write() -> gix_testtools::TestResult { let (storage, mut write, _num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); let actual = edit .upsert(["a", "\n"], EntryKind::Blob, any_blob())? - .write(&mut write) - .expect("no validation is performed"); + .write(&mut write)?; insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @r#" Oid(1) └── a @@ -552,12 +550,12 @@ fn from_empty_invalid_write() -> Result { Ok(()) } #[test] -fn from_empty_add() -> Result { +fn from_empty_add() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new(storage.clone()); let mut edit = gix_object::tree::Editor::new(Tree::default(), &odb, hash_kind()); - let actual = edit.write(&mut write).expect("no changes are fine"); + let actual = edit.write(&mut write)?; assert_eq!(actual, empty_tree(), "empty stays empty"); assert_eq!(num_writes_and_clear(), 1, "the empty tree was written"); insta::assert_snapshot!(crate::normalize_tree_snapshot(&display_tree(actual, &storage)), @"Oid(1)"); @@ -568,10 +566,7 @@ fn from_empty_add() -> Result { "the 'root' can't be obtained, no entry exists for it, ever" ); - let actual = edit - .upsert(Some("hi"), EntryKind::Blob, null_id())? - .write(&mut write) - .expect("effectively no changes are fine"); + let actual = edit.upsert(Some("hi"), EntryKind::Blob, null_id())?.write(&mut write)?; assert_eq!( actual, empty_tree(), @@ -583,8 +578,7 @@ fn from_empty_add() -> Result { let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, null_id())? .upsert(["a", "b", "d", "e"], EntryKind::Blob, null_id())? - .write(&mut write) - .expect("effectively no changes are fine"); + .write(&mut write)?; assert_eq!( actual, empty_tree(), @@ -619,7 +613,7 @@ fn from_empty_add() -> Result { }), ); - let actual = edit.write(&mut write).expect("it's OK to write empty trees"); + let actual = edit.write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "one can write through trees, and empty trees are also fine", @@ -639,8 +633,7 @@ fn from_empty_add() -> Result { gix_hash::Kind::Sha1 => &b"850bf83c26003cb0541318718bc9217c4a5bde6d"[..], gix_hash::Kind::Sha256 => &b"76be2e1aa5ce87f85b81d707c3a5f91c37d09fd064e28e13442b657d419e15f4"[..], _ => unreachable!("tests only support sha1 and sha256 fixtures"), - }) - .expect("valid object id"); + })?; assert_eq!( edit.get(Some("a")), Some(&Entry { @@ -656,8 +649,7 @@ fn from_empty_add() -> Result { .upsert(["a", "b"], EntryKind::Blob, any_blob())? .upsert(["a", "b", "c"], EntryKind::BlobExecutable, any_blob())? .upsert(["x", "z"], EntryKind::Blob, any_blob())? - .write(&mut write) - .expect("writing made-up blobs is fine"); + .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "it's possible to write through previously added blobs", @@ -700,8 +692,7 @@ fn from_empty_add() -> Result { let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, any_blob())? .upsert(["a"], EntryKind::Blob, any_blob())? - .write(&mut write) - .expect("we can turn overwrite a newly added tree (at 'a/') with a blob"); + .write(&mut write)?; insta::assert_snapshot!( crate::normalize_tree_snapshot(&display_tree(actual, &storage)), "now a tree was once again changed into a blob", @@ -755,7 +746,7 @@ fn from_empty_add() -> Result { Ok(()) } #[test] -fn from_existing_add() -> Result { +fn from_existing_add() -> gix_testtools::TestResult { let (storage, mut write, num_writes_and_clear) = new_inmemory_writes(); let odb = StorageOdb::new_with_odb(storage.clone(), tree_odb()?); let root_tree_id = crate::generated_tree_root_id()?; @@ -764,7 +755,7 @@ fn from_existing_add() -> Result { let mut edit = gix_object::tree::Editor::new(root_tree.clone(), &odb, hash_kind()); assert!(edit.get(["bin"]).is_some(), "the root is immediately available"); - let actual = edit.write(&mut write).expect("no changes are fine"); + let actual = edit.write(&mut write)?; assert_eq!(actual, root_tree_id, "it rewrites the same tree"); assert_eq!(odb.access_count_and_clear(), 0); insta::assert_snapshot!(crate::normalize_tree_snapshot(&(display_tree_with_odb(actual, &storage, &odb))), @r#" @@ -786,8 +777,7 @@ fn from_existing_add() -> Result { let actual = edit .upsert(["file", "hi"], EntryKind::Blob, null_id())? - .write(&mut write) - .expect("effectively no changes are fine"); + .write(&mut write)?; assert_eq!( actual, root_tree_id, "null-ids are dropped automatically, they act as placeholders, ultimately the tree is not changed" @@ -803,8 +793,7 @@ fn from_existing_add() -> Result { let actual = edit .upsert(["a", "b", "c"], EntryKind::Blob, null_id())? .upsert(["a", "b", "d", "e"], EntryKind::Blob, null_id())? - .write(&mut write) - .expect("effectively no changes are fine"); + .write(&mut write)?; assert_eq!( actual, root_tree_id, "null-ids are dropped automatically, recursively, and empty intermediate trees are removed as well" @@ -822,8 +811,7 @@ fn from_existing_add() -> Result { .upsert(["bin", "b"], EntryKind::Tree, empty_tree())? .upsert(["bin", "b", "c"], EntryKind::Tree, empty_tree())? .upsert(["a", "b", "d", "e"], EntryKind::Tree, empty_tree())? - .write(&mut write) - .expect("it's OK to write empty leaf-trees"); + .write(&mut write)?; assert_eq!( odb.access_count_and_clear(), 0, diff --git a/gix-object/tests/object/tree/entries.rs b/gix-object/tests/object/tree/entries.rs index 743f3cba35b..762f76679a6 100644 --- a/gix-object/tests/object/tree/entries.rs +++ b/gix-object/tests/object/tree/entries.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_object::{Tree, TreeRef}; #[test] @@ -25,7 +24,7 @@ fn name_order_accounts_for_the_implicit_tree_separator() { } #[test] -fn sort_order_is_correct() -> Result { +fn sort_order_is_correct() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_trees.sh")?; let input = std::fs::read(root.join("tree.baseline"))?; diff --git a/gix-object/tests/object/tree/from_bytes.rs b/gix-object/tests/object/tree/from_bytes.rs index c7a61160afc..1d391833c51 100644 --- a/gix-object/tests/object/tree/from_bytes.rs +++ b/gix-object/tests/object/tree/from_bytes.rs @@ -1,10 +1,9 @@ -use crate::Result; use gix_object::{Tree, TreeRef, TreeRefIter, WriteTo, bstr::ByteSlice, tree, tree::EntryRef}; use crate::{fixture_oid, tree_fixture}; #[test] -fn empty() -> Result { +fn empty() -> gix_testtools::TestResult { let tree_ref = TreeRef::from_bytes(&[], gix_testtools::object_hash())?; assert_eq!( tree_ref, @@ -23,7 +22,7 @@ fn empty() -> Result { } #[test] -fn everything() -> Result { +fn everything() -> gix_testtools::TestResult { let fixture = tree_fixture("everything.tree")?; let hash_kind = crate::fixture_hash_kind(); let tree_ref = TreeRef::from_bytes(&fixture, hash_kind)?; @@ -95,7 +94,7 @@ fn fuzz_artifact_inputs_can_be_parsed_without_panicking() { } #[test] -fn special_trees() -> Result { +fn special_trees() -> gix_testtools::TestResult { let hash_kind = crate::fixture_hash_kind(); for (name, expected_entry_count) in [ ("maybe-special", 160), @@ -118,7 +117,7 @@ fn special_trees() -> Result { ); // Show we can roundtrip let mut buf: Vec<u8> = Default::default(); - actual.write_to(&mut buf).expect("Failed to write bytes to buffer"); + actual.write_to(&mut buf)?; assert_eq!(buf, fixture); } Ok(()) diff --git a/gix-object/tests/object/tree/iter.rs b/gix-object/tests/object/tree/iter.rs index 0d858a8c4a7..4a61f48da4f 100644 --- a/gix-object/tests/object/tree/iter.rs +++ b/gix-object/tests/object/tree/iter.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_object::{ TreeRefIter, bstr::ByteSlice, @@ -48,7 +47,7 @@ fn offset_to_next_entry() { } #[test] -fn everything() -> Result { +fn everything() -> gix_testtools::TestResult { assert_eq!( TreeRefIter::from_bytes(&tree_fixture("everything.tree")?, fixture_hash_kind()) .collect::<std::result::Result<Vec<_>, _>>()?, @@ -84,7 +83,7 @@ fn everything() -> Result { } #[test] -fn leading_space_in_tree_name() -> Result { +fn leading_space_in_tree_name() -> gix_testtools::TestResult { let oid = fixture_oid("4d5fcadc293a348e88f777dc0920f11e7d71441c"); let mut buf = b"40000 leading space\0".to_vec(); buf.extend_from_slice(oid.as_bytes()); @@ -101,14 +100,14 @@ fn leading_space_in_tree_name() -> Result { } mod lookup_entry { - use crate::Result; + use gix_object::tree::EntryKind; use utils::entry; use crate::fixture_hash_kind; #[test] - fn top_level_directory() -> Result { + fn top_level_directory() -> gix_testtools::TestResult { assert_eq!( utils::lookup_entry_by_path("bin")?, entry("bin", EntryKind::Blob, fixture_hash_kind().empty_blob()) @@ -117,7 +116,7 @@ mod lookup_entry { } #[test] - fn nested_file() -> Result { + fn nested_file() -> gix_testtools::TestResult { assert_eq!( utils::lookup_entry_by_path("file/a")?, entry("a", EntryKind::Blob, fixture_hash_kind().empty_blob()) @@ -126,7 +125,7 @@ mod lookup_entry { } #[test] - fn non_existing_nested_file() -> Result { + fn non_existing_nested_file() -> gix_testtools::TestResult { for path in ["file/does-not-exist", "non-existing", "file/a/through-file"] { let actual = utils::lookup_entry_by_path(path)?; assert_eq!(actual, None); diff --git a/gix-odb/src/alternate/parse.rs b/gix-odb/src/alternate/parse.rs index 00e94b88b69..a3089bd3484 100644 --- a/gix-odb/src/alternate/parse.rs +++ b/gix-odb/src/alternate/parse.rs @@ -35,7 +35,7 @@ pub fn parse(mut input: &[u8]) -> Result<Vec<PathBuf>> { continue; }; out.push( - gix_path::try_from_bstr(path) + gix_path::from_bstr(path) .or_raise(|| Message::new("Could not obtain an alternate object directory").with_input(original))? .into_owned(), ); diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index 036b9d264ca..fb99a156a1c 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -121,7 +121,7 @@ impl super::Store { break 'retry_with_next_slot_index; } Err(_err) => { - // TODO(odb-parallelism): consider to search fresh indices, + // TODO(odb-parallelism): consider to search fresh indices, // then retry all changed failed slots // before returning the first error. Persistent failures must not advance state_id. gix_features::trace::error!(err=?_err, "Failed to load index file - some objects may seem to not exist"); diff --git a/gix-odb/tests/odb/alternate.rs b/gix-odb/tests/odb/alternate.rs index 06c85fd34eb..15ae9841d98 100644 --- a/gix-odb/tests/odb/alternate.rs +++ b/gix-odb/tests/odb/alternate.rs @@ -1,4 +1,3 @@ -use crate::Result; use std::{ fs, io, path::{Path, PathBuf}, @@ -82,7 +81,7 @@ fn alternate_with_content( } #[test] -fn circular_alternates_are_detected_with_relative_paths() -> Result { +fn circular_alternates_are_detected_with_relative_paths() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let tmp = tmp.path().join("sub-dir"); std::fs::create_dir(&tmp)?; @@ -121,7 +120,7 @@ fn circular_alternates_are_detected_with_relative_paths() -> Result { } #[test] -fn alternates_reachable_on_multiple_paths_are_not_a_cycle() -> Result { +fn alternates_reachable_on_multiple_paths_are_not_a_cycle() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let tmp = tmp.path(); let (a, shared) = alternate(tmp.join("a"), tmp.join("shared"))?; @@ -146,7 +145,7 @@ fn alternates_reachable_on_multiple_paths_are_not_a_cycle() -> Result { } #[test] -fn cycles_between_alternates_also_listed_by_the_root_are_detected() -> Result { +fn cycles_between_alternates_also_listed_by_the_root_are_detected() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let tmp = tmp.path(); let (a, b) = alternate(tmp.join("a"), tmp.join("b"))?; @@ -176,7 +175,7 @@ fn cycles_between_alternates_also_listed_by_the_root_are_detected() -> Result { } #[test] -fn single_link_with_comment_before_path_and_ansi_c_escape() -> Result { +fn single_link_with_comment_before_path_and_ansi_c_escape() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let non_alternate = tmp.path().join("actual"); @@ -188,7 +187,7 @@ fn single_link_with_comment_before_path_and_ansi_c_escape() -> Result { } #[test] -fn no_alternate_in_first_objects_dir() -> Result { +fn no_alternate_in_first_objects_dir() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; assert!(alternate::resolve(tmp.path().to_owned(), &std::env::current_dir()?)?.is_empty()); Ok(()) diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index 401392386d6..7ab3e8e4aab 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -1,11 +1,10 @@ -use crate::Result; use std::io; use gix_error::{Class, ClassificationMarker, Error, Message, MetadataValue, classify}; use gix_object::{Kind, Write}; #[test] -fn write_failures_preserve_custom_sources_and_metadata() -> Result { +fn write_failures_preserve_custom_sources_and_metadata() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); #[derive(Debug)] struct ReadFailure(ClassificationMarker); @@ -124,7 +123,7 @@ fn write_failures_preserve_custom_sources_and_metadata() -> Result { } #[test] -fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> Result { +fn delta_lookup_distinguishes_missing_bases_from_recursion_limits() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use std::io::Write; @@ -503,12 +502,12 @@ fn multi_index_corrupt_reference_is_fallible(large_offset: bool) -> gix_error::T } #[test] -fn multi_index_invalid_pack_reference() -> gix_error::TestResult { +fn multi_index_invalid_pack_reference() -> gix_testtools::TestResult { multi_index_corrupt_reference_is_fallible(false) } #[test] -fn multi_index_invalid_large_offset_reference() -> gix_error::TestResult { +fn multi_index_invalid_large_offset_reference() -> gix_testtools::TestResult { multi_index_corrupt_reference_is_fallible(true) } @@ -663,17 +662,17 @@ fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::Tes } #[test] -fn multi_index_pack_generation_invalid_pack_reference() -> gix_error::TestResult { +fn multi_index_pack_generation_invalid_pack_reference() -> gix_testtools::TestResult { multi_index_pack_generation_is_fallible(false) } #[test] -fn multi_index_pack_generation_invalid_large_offset_reference() -> gix_error::TestResult { +fn multi_index_pack_generation_invalid_large_offset_reference() -> gix_testtools::TestResult { multi_index_pack_generation_is_fallible(true) } #[test] -fn pack_location_allocation_failures_are_resource_exhaustion() -> gix_error::TestResult { +fn pack_location_allocation_failures_are_resource_exhaustion() -> gix_testtools::TestResult { use gix_error::ResourceExhaustionKind; use gix_pack::{Find as _, data}; use std::io::Write as _; @@ -782,7 +781,7 @@ fn pack_location_allocation_failures_are_resource_exhaustion() -> gix_error::Tes } #[test] -fn pack_lookup_unavailable_is_not_an_error() -> gix_error::TestResult { +fn pack_lookup_unavailable_is_not_an_error() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let mut handle = crate::odb_at(dir.path())?.into_inner(); handle.prevent_pack_unload(); @@ -802,7 +801,7 @@ fn pack_lookup_unavailable_is_not_an_error() -> gix_error::TestResult { } #[test] -fn pack_location_preserves_native_load_errors() -> gix_error::TestResult { +fn pack_location_preserves_native_load_errors() -> gix_testtools::TestResult { use gix_pack::Find as _; let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; @@ -836,7 +835,7 @@ fn pack_location_preserves_native_load_errors() -> gix_error::TestResult { // offsets, object/header lookup and enumeration, including refresh_never and later valid/repaired // packs. Cover pack/alternate removal clearing failures without refresh loops. #[test] -fn pack_lookup_preserves_index_load_errors() -> gix_error::TestResult { +fn pack_lookup_preserves_index_load_errors() -> gix_testtools::TestResult { use gix_pack::Find as _; for lookup_location in [false, true] { @@ -876,7 +875,7 @@ fn pack_lookup_preserves_index_load_errors() -> gix_error::TestResult { } #[test] -fn multi_index_ref_delta_preserves_corrupt_base_lookup() -> gix_error::TestResult { +fn multi_index_ref_delta_preserves_corrupt_base_lookup() -> gix_testtools::TestResult { use gix_pack::data; use std::io::Write; @@ -976,7 +975,7 @@ fn multi_index_ref_delta_preserves_corrupt_base_lookup() -> gix_error::TestResul #[test] #[cfg(unix)] -fn disappearing_loose_objects_keep_retryable_diagnostics() -> Result { +fn disappearing_loose_objects_keep_retryable_diagnostics() -> gix_testtools::TestResult { let mut diagnostics = Vec::new(); use std::{os::unix::fs::symlink, sync::atomic::AtomicBool}; @@ -1076,7 +1075,7 @@ fn disappearing_loose_objects_keep_retryable_diagnostics() -> Result { } #[test] -fn in_pack_ref_delta_preserves_malformed_base_entry() -> gix_error::TestResult { +fn in_pack_ref_delta_preserves_malformed_base_entry() -> gix_testtools::TestResult { use gix_pack::data; use std::io::Write; diff --git a/gix-odb/tests/odb/memory.rs b/gix-odb/tests/odb/memory.rs index d3ed6955271..8ce9c356931 100644 --- a/gix-odb/tests/odb/memory.rs +++ b/gix-odb/tests/odb/memory.rs @@ -5,7 +5,7 @@ use gix_testtools::tempfile::TempDir; use crate::hex_to_id_for_hash; #[test] -fn without_memory() -> Result { +fn without_memory() -> gix_testtools::TestResult { use gix_odb::HeaderExt; let (mut odb, _tmp) = db_rw()?; let mut buf = Vec::new(); @@ -15,9 +15,9 @@ fn without_memory() -> Result { "21d3ba9a26b790a4858d67754ae05d04dfce4d0c", "95997c02e30a106c5413e7a68e7758c6b3c70e951f7471ee48d75c06edc7d234", ); - let tree = odb.find_tree(&existing, &mut buf).expect("present and valid"); + let tree = odb.find_tree(&existing, &mut buf)?; assert_eq!(tree.entries.len(), 1); - odb.header(existing).expect("header can be found just the same"); + odb.header(existing)?; assert!(odb.exists(&existing)); let mut tree = tree.to_owned(); @@ -34,14 +34,11 @@ fn without_memory() -> Result { "e0fcb04b8efaa91993f9880a111c25ae4f2ebd2db8737504e14548a194e31c84", ) ); - let actual = odb.header(new_tree_id).expect("header of new objects can be found"); + let actual = odb.header(new_tree_id)?; assert_eq!(actual.kind(), gix_object::Kind::Tree); assert_eq!(actual.size(), expected_new_tree_size()); - let new_tree = odb - .find_tree(&new_tree_id, &mut buf) - .expect("new tree is also available as object") - .to_owned(); + let new_tree = odb.find_tree(&new_tree_id, &mut buf)?.to_owned(); assert_eq!(new_tree, tree); assert!(!odb.exists(&gix_testtools::object_hash().null())); @@ -49,7 +46,7 @@ fn without_memory() -> Result { } #[test] -fn with_memory() -> Result { +fn with_memory() -> gix_testtools::TestResult { use gix_object::FindHeader; let mut odb = db()?; assert_eq!( @@ -62,7 +59,7 @@ fn with_memory() -> Result { "21d3ba9a26b790a4858d67754ae05d04dfce4d0c", "95997c02e30a106c5413e7a68e7758c6b3c70e951f7471ee48d75c06edc7d234", ); - let tree = odb.find_tree(&existing, &mut buf).expect("present and valid"); + let tree = odb.find_tree(&existing, &mut buf)?; assert!(odb.exists(&existing)); assert_eq!(tree.entries.len(), 1); odb.try_header(&existing)?.expect("header can be found just the same"); @@ -92,10 +89,7 @@ fn with_memory() -> Result { assert_eq!(actual.kind, gix_object::Kind::Tree); assert_eq!(actual.size, expected_new_tree_size()); - let new_tree = odb - .find_tree(&new_tree_id, &mut buf) - .expect("new tree is also available as object") - .to_owned(); + let new_tree = odb.find_tree(&new_tree_id, &mut buf)?.to_owned(); assert_eq!(new_tree, tree); let mem = odb.reset_object_memory().expect("memory is still available"); @@ -121,7 +115,7 @@ fn with_memory() -> Result { } #[test] -fn with_memory_trusts_known_id() -> Result { +fn with_memory_trusts_known_id() -> gix_testtools::TestResult { let odb = db()?; let kind = gix_object::Kind::Blob; let bytes = b"content"; @@ -151,7 +145,7 @@ fn with_memory_trusts_known_id() -> Result { } #[test] -fn without_memory_forwards_known_id_writes() -> Result { +fn without_memory_forwards_known_id_writes() -> gix_testtools::TestResult { let (mut odb, _tmp) = db_rw()?; odb.take_object_memory().expect("it starts out with memory set"); diff --git a/gix-odb/tests/odb/regression.rs b/gix-odb/tests/odb/regression.rs index 15fd3e4907d..dedb5ec2279 100644 --- a/gix-odb/tests/odb/regression.rs +++ b/gix-odb/tests/odb/regression.rs @@ -1,11 +1,11 @@ mod repo_with_small_packs { - use crate::Result; + use gix_object::Find; use crate::{db_small_packs, hex_to_id}; #[test] - fn all_packed_objects_can_be_found() -> Result { + fn all_packed_objects_can_be_found() -> gix_testtools::TestResult { let store = db_small_packs(); let mut buf = Vec::new(); assert!( @@ -19,7 +19,7 @@ mod repo_with_small_packs { #[test] #[cfg(feature = "parallel")] - fn multi_threaded_access_will_not_panic() -> Result { + fn multi_threaded_access_will_not_panic() -> gix_testtools::TestResult { for arg in ["no", "without-multi-index"] { let base = crate::scripted_fixture_read_only_with_args("make_repo_multi_index.sh", Some(arg))? .join(".git") @@ -59,7 +59,7 @@ mod repo_with_small_packs { "different ordering doesn't change the count" ); for handle in handles { - let actual = handle.join().expect("no panic").expect("no error in thread"); + let actual = handle.join().expect("no panic")?; assert_eq!(actual, expected); } } diff --git a/gix-odb/tests/odb/sink.rs b/gix-odb/tests/odb/sink.rs index 08c6840252a..abc474c5cfe 100644 --- a/gix-odb/tests/odb/sink.rs +++ b/gix-odb/tests/odb/sink.rs @@ -1,10 +1,9 @@ -use crate::Result; use gix_object::Write; use crate::store::loose::{locate_oid, object_ids}; #[test] -fn write() -> Result { +fn write() -> gix_testtools::TestResult { let mut buf = Vec::new(); for oid in object_ids() { let obj = locate_oid(oid, &mut buf); diff --git a/gix-odb/tests/odb/store/dynamic.rs b/gix-odb/tests/odb/store/dynamic.rs index 6e5d7d4a414..287118cb37f 100644 --- a/gix-odb/tests/odb/store/dynamic.rs +++ b/gix-odb/tests/odb/store/dynamic.rs @@ -72,7 +72,7 @@ fn db_with_all_object_sources() -> Result<(gix_odb::Handle, gix_testtools::tempf } #[test] -fn multi_index_access() -> Result { +fn multi_index_access() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; let expected = expected_pack_metrics(dir.path())?; let handle = crate::odb_at(dir.path().join(".git/objects"))?; @@ -179,7 +179,7 @@ fn multi_index_access() -> Result { } #[test] -fn multi_index_alloc_limit_bytes_falls_back_to_plain_indices() -> Result { +fn multi_index_alloc_limit_bytes_falls_back_to_plain_indices() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; let expected = expected_pack_metrics(dir.path())?; let handle = gix_odb::at_opts( @@ -212,7 +212,7 @@ fn multi_index_alloc_limit_bytes_falls_back_to_plain_indices() -> Result { } #[test] -fn multi_index_keep_open() -> Result { +fn multi_index_keep_open() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_writable("make_repo_multi_index.sh")?; let expected = expected_pack_metrics(dir.path())?; let (stable_handle, handle) = { @@ -278,7 +278,7 @@ fn multi_index_keep_open() -> Result { } #[test] -fn an_object_in_a_pack_moved_by_an_external_process_can_be_found_from_a_stale_handle() -> Result { +fn an_object_in_a_pack_moved_by_an_external_process_can_be_found_from_a_stale_handle() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let pack_dir = tmp.path().join("objects/pack"); std::fs::create_dir_all(&pack_dir)?; @@ -333,7 +333,7 @@ fn an_object_in_a_pack_moved_by_an_external_process_can_be_found_from_a_stale_ha } #[test] -fn write() -> Result { +fn write() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let mut handle = odb_at(dir.path())?; // It should refresh once even if the refresh mode is never, just to initialize the index @@ -351,7 +351,7 @@ fn write() -> Result { } #[test] -fn alternate_dbs_query() -> Result { +fn alternate_dbs_query() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_read_only("make_alternates_odb.sh")?; let handle = crate::odb_at(dir.join(".git/objects"))?; @@ -371,7 +371,7 @@ fn alternate_dbs_query() -> Result { } #[test] -fn object_replacement() -> Result { +fn object_replacement() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_read_only("make_replaced_history.sh")?; let handle = crate::odb_at(dir.join(".git/objects"))?; let mut buf = Vec::new(); @@ -675,7 +675,6 @@ fn packed_object_count_causes_all_indices_to_be_loaded() { } mod disambiguate_prefix { - use crate::Result; use std::cmp::Ordering; use gix_odb::store::prefix::disambiguate::Candidate; @@ -687,7 +686,7 @@ mod disambiguate_prefix { }; #[test] - fn unambiguous_hex_lengths_yield_prefixes_of_exactly_the_given_length() -> Result { + fn unambiguous_hex_lengths_yield_prefixes_of_exactly_the_given_length() -> gix_testtools::TestResult { let (mut handle, _tmp) = db_with_all_object_sources()?; handle.refresh.never(); @@ -769,7 +768,6 @@ mod disambiguate_prefix { } mod iter { - use crate::Result; use gix_odb::store::iter::Ordering; use crate::{ @@ -778,7 +776,7 @@ mod iter { }; #[test] - fn iteration_ordering_is_effective() -> Result { + fn iteration_ordering_is_effective() -> gix_testtools::TestResult { assert_eq!(all_orderings().len(), 2, "new orderings cause this test to be reviewed"); for (handle, _tmp) in [db_with_all_object_sources().map(|(a, b)| (a, Some(b)))?, (db(), None)] { let (mut a, mut b): (Vec<_>, Vec<_>) = ( @@ -805,7 +803,6 @@ mod iter { } mod lookup_prefix { - use crate::Result; use std::collections::HashSet; use maplit::hashset; @@ -856,7 +853,7 @@ mod lookup_prefix { } #[test] - fn iterable_objects_can_be_looked_up_with_varying_prefix_lengths() -> Result { + fn iterable_objects_can_be_looked_up_with_varying_prefix_lengths() -> gix_testtools::TestResult { let (mut handle, _tmp) = db_with_all_object_sources()?; handle.refresh.never(); @@ -933,7 +930,7 @@ fn missing_objects_triggers_everything_is_loaded() { } #[test] -fn iterate_over_a_bunch_of_loose_and_packed_objects() -> Result { +fn iterate_over_a_bunch_of_loose_and_packed_objects() -> gix_testtools::TestResult { let (db, _tmp) = db_with_all_object_sources()?; for order in all_orderings() { let iter = db.iter()?.with_ordering(order); @@ -951,7 +948,7 @@ fn iterate_over_a_bunch_of_loose_and_packed_objects() -> Result { } #[test] -fn auto_refresh_with_and_without_id_stability() -> Result { +fn auto_refresh_with_and_without_id_stability() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; assert!( gix_testtools::git_command(tmp.path()) diff --git a/gix-odb/tests/odb/store/linked.rs b/gix-odb/tests/odb/store/linked.rs index 6d09221329f..5407a63ff87 100644 --- a/gix-odb/tests/odb/store/linked.rs +++ b/gix-odb/tests/odb/store/linked.rs @@ -2,14 +2,14 @@ //! to be sure we don't loose coverage. This might, however, be overlapping with much more thorough //! tests o the general store itself, so they can possibly be removed at some point. mod iter { - use crate::Result; + use gix_odb::Header; use gix_pack::Find; use crate::db; #[test] - fn a_bunch_of_loose_and_packed_objects() -> Result { + fn a_bunch_of_loose_and_packed_objects() -> gix_testtools::TestResult { let db = db(); let iter = db.iter()?; assert_eq!( @@ -56,14 +56,14 @@ mod locate { } mod init { - use crate::Result; + use gix_hash::ObjectId; use gix_object::Exists; use crate::{alternate::alternate, db, odb_at}; #[test] - fn multiple_linked_repositories_via_alternates() -> Result { + fn multiple_linked_repositories_via_alternates() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let (object_path, _linked_object_path) = alternate(tmp.path().join("a"), tmp.path().join("b"))?; let db = odb_at(object_path.clone())?; @@ -76,7 +76,7 @@ mod init { } #[test] - fn a_db_without_alternates() -> Result { + fn a_db_without_alternates() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let db = odb_at(tmp.path())?; db.exists(&ObjectId::null(gix_testtools::object_hash())); // trigger load diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 18d39944e5b..7596c6e1238 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -53,11 +53,9 @@ pub fn locate_oid(id: gix_hash::ObjectId, buf: &mut Vec<u8>) -> gix_object::Data } #[test] -fn verify_integrity() { +fn verify_integrity() -> gix_testtools::TestResult { let db = ldb(); - let outcome = db - .verify_integrity(&mut progress::Discard, &AtomicBool::new(false)) - .expect("fixture objects pass integrity checks"); + let outcome = db.verify_integrity(&mut progress::Discard, &AtomicBool::new(false))?; assert_eq!(outcome.num_objects, 7, "all loose fixture objects were verified"); let err = db .verify_integrity(&mut progress::Discard, &AtomicBool::new(true)) @@ -105,17 +103,18 @@ fn verify_integrity() { }), "cancellation identifies the original I/O interruption" ); + Ok(()) } mod write { - use crate::Result; + use gix_object::Write; use gix_odb::loose; use crate::store::loose::{ldb_at, ldb_at_opts, locate_oid, object_ids}; #[test] - fn compression_level_is_respected() -> Result { + fn compression_level_is_respected() -> gix_testtools::TestResult { use gix_zlib::Compression; let data: Vec<u8> = (0..64 * 1024).map(|i| (i % 100) as u8).collect(); let mut sizes = Vec::new(); @@ -147,7 +146,7 @@ mod write { } #[test] - fn read_and_write() -> Result { + fn read_and_write() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let db = ldb_at(dir.path()); let mut buf = Vec::new(); @@ -186,7 +185,7 @@ mod write { #[test] #[cfg(unix)] - fn it_writes_objects_with_similar_permissions() -> Result { + fn it_writes_objects_with_similar_permissions() -> gix_testtools::TestResult { let object_hash = gix_testtools::object_hash(); let git_store = loose::Store::at( crate::scripted_fixture_read_only("repo_with_loose_objects.sh")?.join(".git/objects"), @@ -209,7 +208,7 @@ mod write { } #[test] - fn collisions_do_not_cause_failure() -> Result { + fn collisions_do_not_cause_failure() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; fn write_empty_trees(dir: &std::path::Path) { @@ -334,7 +333,7 @@ mod lookup_prefix { } mod find { - use crate::Result; + use gix_error::{Class, Message, MetadataValue, ResourceExhaustionKind}; use gix_object::{BlobRef, CommitRef, Kind, TagRef, TreeRef, bstr::ByteSlice, tree::EntryKind}; @@ -348,7 +347,7 @@ mod find { } #[test] - fn invalid_object_does_not_trigger_panics() -> Result { + fn invalid_object_does_not_trigger_panics() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let base = tmp.path().join("aa"); std::fs::create_dir(&base)?; @@ -389,7 +388,7 @@ mod find { } #[test] - fn completed_object_size_is_validated_before_allocation() -> Result { + fn completed_object_size_is_validated_before_allocation() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use std::io::Write; @@ -484,7 +483,7 @@ mod find { } #[test] - fn tag() -> Result { + fn tag() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("722fe60ad4f0276d5a8121970b5bb9dccdad4ef9", &mut buf); assert_eq!(o.kind, Kind::Tag); @@ -522,7 +521,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn commit() -> Result { + fn commit() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("ffa700b4aca13b80cb6b98a078e7c96804f8e0ec", &mut buf); assert_eq!(o.kind, Kind::Commit); @@ -542,7 +541,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn blob_data() -> Result { + fn blob_data() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("37d4e6c5c48ba0d245164c4e10d5f41140cab980", &mut buf); assert_eq!(o.data.as_bstr(), b"hi there\n".as_bstr()); @@ -550,7 +549,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn blob() -> Result { + fn blob() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("37d4e6c5c48ba0d245164c4e10d5f41140cab980", &mut buf); assert_eq!( @@ -570,7 +569,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn blob_big() -> Result { + fn blob_big() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("a706d7cd20fc8ce71489f34b50cf01011c104193", &mut buf); assert_eq!( @@ -582,7 +581,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn blob_big_respects_alloc_limit_bytes() -> Result { + fn blob_big_respects_alloc_limit_bytes() -> gix_testtools::TestResult { let id = hex_to_id("a706d7cd20fc8ce71489f34b50cf01011c104193"); let db = limited_ldb(1); let mut buf = Vec::new(); @@ -654,7 +653,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn unrepresentable_allocation_preserves_the_original_cause() -> Result { + fn unrepresentable_allocation_preserves_the_original_cause() -> gix_testtools::TestResult { use std::io::Write; let tmp = gix_testtools::tempfile::tempdir()?; @@ -772,7 +771,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn tree() -> Result { + fn tree() -> gix_testtools::TestResult { let mut buf = Vec::new(); let o = find("6ba2a0ded519f737fd5b8d5ccfb141125ef3176f", &mut buf); assert_eq!(o.kind, Kind::Tree); @@ -801,11 +800,11 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } mod header { - use crate::Result; + use crate::{hex_to_id, store::loose::ldb}; #[test] - fn existing() -> Result { + fn existing() -> gix_testtools::TestResult { let db = ldb(); assert_eq!( db.try_header(&hex_to_id("a706d7cd20fc8ce71489f34b50cf01011c104193"))? @@ -816,7 +815,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn non_existing() -> Result { + fn non_existing() -> gix_testtools::TestResult { let db = ldb(); assert_eq!( db.try_header(&hex_to_id("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))?, @@ -827,7 +826,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB } #[test] - fn all() -> Result { + fn all() -> gix_testtools::TestResult { let db = ldb(); let mut buf = Vec::new(); for id in db.iter() { diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index 93014a9c5ac..142de6696f3 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -671,7 +671,7 @@ mod tests { }; #[test] - fn traversal_buffer_growth_respects_alloc_limit() -> gix_error::TestResult { + fn traversal_buffer_growth_respects_alloc_limit() -> gix_testtools::TestResult { let mut out = Vec::new(); let mut inflate = gix_zlib::Inflate::default(); for size in [64, 65] { @@ -690,7 +690,7 @@ mod tests { } #[test] - fn traversal_rejects_unreachable_delta_cycles() -> gix_testtools::Result { + fn traversal_rejects_unreachable_delta_cycles() -> gix_testtools::TestResult { for has_root in [false, true] { for cycle_len in [1, 2, 3] { let mut pack = Vec::new(); @@ -730,7 +730,7 @@ mod tests { } #[test] - fn traversal_accepts_forward_delta_bases() -> gix_testtools::Result { + fn traversal_accepts_forward_delta_bases() -> gix_testtools::TestResult { let mut pack = Vec::new(); let child = append_entry( &mut pack, @@ -756,7 +756,7 @@ mod tests { } #[test] - fn traversal_accepts_reused_progress() -> gix_testtools::Result { + fn traversal_accepts_reused_progress() -> gix_testtools::TestResult { use progress::Count; let mut pack = Vec::new(); @@ -789,7 +789,7 @@ mod tests { } #[test] - fn traversal_resolves_children_lazily() { + fn traversal_resolves_children_lazily() -> gix_testtools::TestResult { let mut pack = Vec::new(); let root_offset = append_entry(&mut pack, data::entry::Header::Blob, 1, b"A"); let first_child = append_delta(&mut pack, root_offset, b'B'); @@ -797,16 +797,12 @@ mod tests { let first_leaf = append_delta(&mut pack, first_child, b'D'); let second_leaf = append_delta(&mut pack, second_child, b'E'); - let mut tree = Tree::with_capacity(5, None).expect("capacity is small"); - tree.add_root(root_offset, ()).expect("offsets are increasing"); - tree.add_child(root_offset, first_child, ()) - .expect("offsets are increasing"); - tree.add_child(root_offset, second_child, ()) - .expect("offsets are increasing"); - tree.add_child(first_child, first_leaf, ()) - .expect("offsets are increasing"); - tree.add_child(second_child, second_leaf, ()) - .expect("offsets are increasing"); + let mut tree = Tree::with_capacity(5, None)?; + tree.add_root(root_offset, ())?; + tree.add_child(root_offset, first_child, ())?; + tree.add_child(root_offset, second_child, ())?; + tree.add_child(first_child, first_leaf, ())?; + tree.add_child(second_child, second_leaf, ())?; let resolve_calls = AtomicUsize::new(0); let calls_at_first_child = AtomicUsize::new(usize::MAX); @@ -825,29 +821,28 @@ mod tests { } Ok(()) }, - ) - .expect("valid delta tree"); + )?; assert_eq!( calls_at_first_child.load(Ordering::Relaxed), 2, "the first child must be inspected before its siblings are materialized" ); + Ok(()) } #[test] - fn traversal_parallelizes_children_of_one_root() { + fn traversal_parallelizes_children_of_one_root() -> gix_testtools::TestResult { let mut pack = Vec::new(); let root_offset = append_entry(&mut pack, data::entry::Header::Blob, 1, b"A"); let child_offsets: Vec<_> = (b'B'..=b'I') .map(|byte| append_delta(&mut pack, root_offset, byte)) .collect(); - let mut tree = Tree::with_capacity(1 + child_offsets.len(), None).expect("capacity is small"); - tree.add_root(root_offset, ()).expect("offsets are increasing"); + let mut tree = Tree::with_capacity(1 + child_offsets.len(), None)?; + tree.add_root(root_offset, ())?; for child_offset in child_offsets { - tree.add_child(root_offset, child_offset, ()) - .expect("offsets are increasing"); + tree.add_child(root_offset, child_offset, ())?; } let active = AtomicUsize::new(0); @@ -867,30 +862,31 @@ mod tests { } Ok(()) }, - ) - .expect("valid delta tree"); + )?; let expected = if cfg!(feature = "parallel") { 1 } else { 0 }; assert!( max_active.load(Ordering::Relaxed) > expected, "idle workers must help with children of the last remaining root (if in parallel mode)" ); + Ok(()) } #[test] - fn traversal_rejects_declared_decompressed_size_over_alloc_limit() { + fn traversal_rejects_declared_decompressed_size_over_alloc_limit() -> gix_testtools::TestResult { let mut pack = Vec::new(); let root_offset = append_entry(&mut pack, data::entry::Header::Blob, 1, b""); - let mut tree = Tree::with_capacity(1, None).expect("capacity is small"); - tree.add_root(root_offset, ()).expect("offsets are increasing"); + let mut tree = Tree::with_capacity(1, None)?; + tree.add_root(root_offset, ())?; let err = traverse_with_limit(tree, &pack).expect_err("entry size exceeds the allocation cap"); insta::assert_debug_snapshot!(err, "declared decompressed sizes above the cap must be rejected before allocation", @"Entry too large to fit in memory"); + Ok(()) } #[test] - fn traversal_rejects_delta_base_size_over_alloc_limit() { + fn traversal_rejects_delta_base_size_over_alloc_limit() -> gix_testtools::TestResult { let mut pack = Vec::new(); let root_offset = append_entry(&mut pack, data::entry::Header::Blob, 0, b""); @@ -905,18 +901,18 @@ mod tests { &delta, ); - let mut tree = Tree::with_capacity(2, None).expect("capacity is small"); - tree.add_root(root_offset, ()).expect("offsets are increasing"); - tree.add_child(root_offset, child_offset, ()) - .expect("offsets are increasing"); + let mut tree = Tree::with_capacity(2, None)?; + tree.add_root(root_offset, ())?; + tree.add_child(root_offset, child_offset, ())?; let err = traverse_with_limit(tree, &pack).expect_err("delta base size exceeds the allocation cap"); insta::assert_debug_snapshot!(err, "delta base sizes above the cap must be rejected before comparing them with the decoded base", @"Entry too large to fit in memory"); + Ok(()) } #[test] - fn traversal_rejects_delta_result_size_over_alloc_limit() { + fn traversal_rejects_delta_result_size_over_alloc_limit() -> gix_testtools::TestResult { let mut pack = Vec::new(); let root_offset = append_entry(&mut pack, data::entry::Header::Blob, 0, b""); @@ -931,14 +927,14 @@ mod tests { &delta, ); - let mut tree = Tree::with_capacity(2, None).expect("capacity is small"); - tree.add_root(root_offset, ()).expect("offsets are increasing"); - tree.add_child(root_offset, child_offset, ()) - .expect("offsets are increasing"); + let mut tree = Tree::with_capacity(2, None)?; + tree.add_root(root_offset, ())?; + tree.add_child(root_offset, child_offset, ())?; let err = traverse_with_limit(tree, &pack).expect_err("delta result size exceeds the allocation cap"); insta::assert_debug_snapshot!(err, "delta result sizes above the cap must be rejected before resizing the output buffer", @"Entry too large to fit in memory"); + Ok(()) } fn traverse_with_limit(tree: Tree<()>, pack: &Vec<u8>) -> Result { diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index 18b419b2705..4cb0f9fc791 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -219,7 +219,7 @@ impl<T> Tree<T> { #[cfg(test)] mod tests { #[test] - fn unresolved_ref_delta_parent_is_corruption() -> gix_error::TestResult { + fn unresolved_ref_delta_parent_is_corruption() -> gix_testtools::TestResult { let mut tree = super::Tree::with_capacity(1, None)?; tree.add_child(24, 12, ())?; let err = tree @@ -239,7 +239,7 @@ mod tests { } #[test] - fn forward_ref_delta_parent_is_allowed() -> gix_error::TestResult { + fn forward_ref_delta_parent_is_allowed() -> gix_testtools::TestResult { let mut tree = super::Tree::with_capacity(2, None)?; tree.add_child(24, 12, ())?; tree.add_root(24, ())?; @@ -253,7 +253,7 @@ mod tests { } #[test] - fn pack_end_must_follow_the_last_entry() -> gix_error::TestResult { + fn pack_end_must_follow_the_last_entry() -> gix_testtools::TestResult { let mut tree = super::Tree::with_capacity(1, None)?; tree.add_root(12, ())?; let err = tree @@ -306,17 +306,17 @@ mod tests { use gix_testtools::fixture_path; #[test] - fn v1() -> gix_testtools::Result { - tree(INDEX_V1, PACK_FOR_INDEX_V1) + fn v1() -> gix_testtools::TestResult { + Ok(tree(INDEX_V1, PACK_FOR_INDEX_V1)?) } #[test] - fn v2() -> gix_testtools::Result { - tree(SMALL_PACK_INDEX, SMALL_PACK) + fn v2() -> gix_testtools::TestResult { + Ok(tree(SMALL_PACK_INDEX, SMALL_PACK)?) } #[test] - fn invalid_ofs_delta_base_distance_is_reported() -> gix_testtools::Result { + fn invalid_ofs_delta_base_distance_is_reported() -> gix_testtools::TestResult { let first_entry_offset = pack::data::header::SIZE as pack::data::Offset; let pack_file = gix_testtools::tempfile::NamedTempFile::new()?; let mut pack_data = pack::data::header::encode(pack::data::Version::V2, 1).to_vec(); @@ -341,7 +341,7 @@ mod tests { } #[test] - fn duplicate_and_overlapping_offsets_are_rejected() -> gix_testtools::Result { + fn duplicate_and_overlapping_offsets_are_rejected() -> gix_testtools::TestResult { let first = pack::data::header::SIZE as pack::data::Offset; let pack_file = gix_testtools::tempfile::NamedTempFile::new()?; let mut data = pack::data::header::encode(pack::data::Version::V2, 2).to_vec(); diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index 20e51139cfe..adcad6a8809 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -174,10 +174,9 @@ mod tests { use super::*; #[test] - fn accepts_non_canonical_pack_entry_header_encoding() { + fn accepts_non_canonical_pack_entry_header_encoding() -> gix_testtools::TestResult { let pack_offset = 42; - let entry = data::Entry::from_bytes(&[0xb3, 0x00], pack_offset, gix_hash::Kind::Sha1) - .expect("non-canonical size encodings are accepted by git"); + let entry = data::Entry::from_bytes(&[0xb3, 0x00], pack_offset, gix_hash::Kind::Sha1)?; assert_eq!(entry.header, data::entry::Header::Blob); assert_eq!( @@ -200,14 +199,14 @@ mod tests { ); assert_eq!(entry.pack_offset(), pack_offset); assert_eq!(entry.data_offset, pack_offset + 2); + Ok(()) } #[test] - fn non_canonical_pack_entry_header_keeps_ofs_delta_base_offsets_correct() { + fn non_canonical_pack_entry_header_keeps_ofs_delta_base_offsets_correct() -> gix_testtools::TestResult { let pack_offset = 100; let base_distance = 5; - let entry = data::Entry::from_bytes(&[0xe4, 0x00, base_distance], pack_offset, gix_hash::Kind::Sha1) - .expect("non-canonical ofs-delta size encodings are accepted by git"); + let entry = data::Entry::from_bytes(&[0xe4, 0x00, base_distance], pack_offset, gix_hash::Kind::Sha1)?; assert_eq!( entry.header, @@ -231,6 +230,7 @@ mod tests { Some(pack_offset - u64::from(base_distance)), "ofs-delta base distances are relative to the entry start, so preserving `pack_offset()` keeps the base lookup correct" ); + Ok(()) } #[test] diff --git a/gix-pack/src/multi_index/chunk.rs b/gix-pack/src/multi_index/chunk.rs index dbe2c1ac5e0..c50c16145c0 100644 --- a/gix-pack/src/multi_index/chunk.rs +++ b/gix-pack/src/multi_index/chunk.rs @@ -61,7 +61,7 @@ pub mod index_names { "Pack path requires more memory than allowed", )); } - let path = gix_path::try_from_byte_slice(path) + let path = gix_path::from_byte_slice(path) .or_raise(|| { corruption(format!( "Couldn't turn path \"{}\" into OS path due to encoding issues", diff --git a/gix-pack/tests/pack/bundle.rs b/gix-pack/tests/pack/bundle.rs index d524a1cd81c..c01628db903 100644 --- a/gix-pack/tests/pack/bundle.rs +++ b/gix-pack/tests/pack/bundle.rs @@ -1,5 +1,4 @@ mod locate { - use crate::Result; use bstr::ByteSlice; use gix_object::Kind; use gix_odb::pack; @@ -21,13 +20,12 @@ mod locate { } mod locate_and_verify { - use crate::Result; use gix_odb::pack; use crate::{PACKS_AND_INDICES, fixture_path}; #[test] - fn all() -> Result { + fn all() -> gix_testtools::TestResult { for (index_path, data_path) in PACKS_AND_INDICES { // both paths are equivalent pack::Bundle::at(fixture_path(index_path), gix_hash::Kind::Sha1)?; @@ -51,7 +49,7 @@ mod locate { } #[test] - fn blob() -> Result { + fn blob() -> gix_testtools::TestResult { let mut out = Vec::new(); let obj = locate("bd46bb3f5bb4ca5431770c4fde0735fb89d382f3", &mut out); @@ -67,7 +65,7 @@ mod locate { } #[test] - fn tree() -> Result { + fn tree() -> gix_testtools::TestResult { let mut out = Vec::new(); let obj = locate("e90926b07092bccb7bf7da445fae6ffdfacf3eae", &mut out); @@ -77,7 +75,7 @@ mod locate { } #[test] - fn commit() -> Result { + fn commit() -> gix_testtools::TestResult { let mut out = Vec::new(); let obj = locate("779c5451ba9fe210ffd1f55db202e55f51acecac", &mut out); @@ -120,7 +118,7 @@ mod write_to_directory { } #[test] - fn without_providing_one() -> Result { + fn without_providing_one() -> gix_testtools::TestResult { let res = write_pack(None::<&Path>, SMALL_PACK)?; assert_eq!(res, expected_outcome()?); assert_eq!( @@ -132,7 +130,7 @@ mod write_to_directory { } #[test] - fn given_a_directory() -> Result { + fn given_a_directory() -> gix_testtools::TestResult { let dir = TempDir::new()?; let mut res = write_pack(Some(&dir), SMALL_PACK)?; let (index_path, data_path, keep_path) = (res.index_path.take(), res.data_path.take(), res.keep_path.take()); @@ -167,7 +165,7 @@ mod write_to_directory { /// already has. `index-pack --fix-thin` makes such packs self-contained by appending /// those bases, leaving the original deltas as forward references. #[test] - fn in_pack_ref_deltas_with_forward_references() -> Result { + fn in_pack_ref_deltas_with_forward_references() -> gix_testtools::TestResult { for object_hash in [gix_hash::Kind::Sha1, gix_hash::Kind::Sha256] { for objects in [ &[b"A".as_slice(), b"B".as_slice()][..], @@ -224,7 +222,7 @@ mod write_to_directory { } #[test] - fn version_3_with_thin_pack_lookup() -> Result { + fn version_3_with_thin_pack_lookup() -> gix_testtools::TestResult { let object_hash = gix_hash::Kind::Sha1; let pack_data = ref_delta_pack( object_hash, @@ -252,7 +250,7 @@ mod write_to_directory { } #[test] - fn unresolved_ref_delta_base_is_reported() -> Result { + fn unresolved_ref_delta_base_is_reported() -> gix_testtools::TestResult { let object_hash = gix_hash::Kind::Sha1; let base_id = object_hash.null(); let delta = [0, 0]; @@ -285,7 +283,7 @@ mod write_to_directory { } #[test] - fn respects_alloc_limit_bytes() -> Result { + fn respects_alloc_limit_bytes() -> gix_testtools::TestResult { let pack_file = fs::File::open(fixture_path(SMALL_PACK))?; static SHOULD_INTERRUPT: AtomicBool = AtomicBool::new(false); diff --git a/gix-pack/tests/pack/data/file.rs b/gix-pack/tests/pack/data/file.rs index 5fada16a264..f8e7c24365c 100644 --- a/gix-pack/tests/pack/data/file.rs +++ b/gix-pack/tests/pack/data/file.rs @@ -33,7 +33,6 @@ fn unresolved_delta_base_is_not_found() { } mod method { - use crate::Result; use std::sync::atomic::AtomicBool; use gix_features::progress; @@ -47,7 +46,7 @@ mod method { } #[test] - fn verify_checksum() -> Result { + fn verify_checksum() -> gix_testtools::TestResult { let p = pack_at(SMALL_PACK); assert_eq!( p.verify_checksum(&mut progress::Discard, &AtomicBool::new(false))?, @@ -75,7 +74,7 @@ mod method { } #[test] - fn verify_checksum_from_memory() -> Result { + fn verify_checksum_from_memory() -> gix_testtools::TestResult { let p = pack_from_memory_at(SMALL_PACK); assert_eq!( p.verify_checksum(&mut progress::Discard, &AtomicBool::new(false))?, @@ -127,7 +126,7 @@ mod method { } #[test] - fn iter() -> Result { + fn iter() -> gix_testtools::TestResult { let pack = pack_at(SMALL_PACK); let it = pack.streaming_iter()?; assert_eq!(it.count(), pack.num_objects() as usize); diff --git a/gix-pack/tests/pack/data/fuzzed.rs b/gix-pack/tests/pack/data/fuzzed.rs index 61b7c600cdf..673dfd378d5 100644 --- a/gix-pack/tests/pack/data/fuzzed.rs +++ b/gix-pack/tests/pack/data/fuzzed.rs @@ -9,14 +9,11 @@ use gix_pack::data; const FIRST_ENTRY_OFFSET: data::Offset = data::header::SIZE as data::Offset; #[test] -fn artifact_inputs_can_be_opened_without_panicking() { +fn artifact_inputs_can_be_opened_without_panicking() -> gix_testtools::TestResult { for path in crate::fuzz_artifact_paths("data_file") { - _ = data::File::from_data( - std::fs::read(&path).expect("artifact is readable"), - path, - gix_hash::Kind::Sha1, - ); + _ = data::File::from_data(std::fs::read(&path)?, path, gix_hash::Kind::Sha1); } + Ok(()) } /// Reproducer for the truncated ref-delta metadata fuzz case: malformed entry data must not panic @@ -36,7 +33,7 @@ fn truncated_ref_delta_metadata_is_reported_without_panicking() { /// Reproducer for the oversized pack header fuzz case: malformed entry headers with too many /// continuation bytes must not panic while decoding the first object entry. #[test] -fn oversized_pack_entry_header_is_reported_without_panicking() { +fn oversized_pack_entry_header_is_reported_without_panicking() -> gix_testtools::TestResult { let data = [ b'P', b'A', b'C', b'K', 0, 0, 0, 2, 0, 0, 0, 1, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, @@ -45,8 +42,7 @@ fn oversized_pack_entry_header_is_reported_without_panicking() { data.as_slice(), PathBuf::from("fuzzed-header.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid"); + )?; let result = catch_unwind(AssertUnwindSafe(|| file.entry(FIRST_ENTRY_OFFSET))); assert!( @@ -55,10 +51,11 @@ fn oversized_pack_entry_header_is_reported_without_panicking() { .is_err(), "forged pack entry headers should be rejected as corrupt pack data" ); + Ok(()) } #[test] -fn non_canonical_pack_entry_header_is_accepted() { +fn non_canonical_pack_entry_header_is_accepted() -> gix_testtools::TestResult { fn deflate(bytes: &[u8]) -> Vec<u8> { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::BEST_SPEED); out.write_all(bytes).expect("writing to deflater succeeds"); @@ -75,11 +72,8 @@ fn non_canonical_pack_entry_header_is_accepted() { bytes.as_slice(), PathBuf::from("non-canonical-header.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid"); - let entry = file - .entry(FIRST_ENTRY_OFFSET) - .expect("git-compatible non-canonical entry header is accepted"); + )?; + let entry = file.entry(FIRST_ENTRY_OFFSET)?; assert_eq!(entry.header_size(), 2, "the actual header size is retained"); assert_eq!( entry.pack_offset(), @@ -88,36 +82,32 @@ fn non_canonical_pack_entry_header_is_accepted() { ); let mut out = Vec::new(); - let outcome = file - .decode_entry( - entry, - &mut out, - &mut Default::default(), - &|_, _| Ok(None), - &mut gix_pack::cache::Never, - ) - .expect("non-canonical entry decodes like a canonical git object"); + let outcome = file.decode_entry( + entry, + &mut out, + &mut Default::default(), + &|_, _| Ok(None), + &mut gix_pack::cache::Never, + )?; assert_eq!(out, b"20\n"); assert_eq!(outcome.kind, gix_object::Kind::Blob); assert_eq!(outcome.object_size, 3); + Ok(()) } /// Reproducer for the large-allocation fuzz case: attacker-controlled object sizes must not cause /// `decode_entry()` to attempt multi-gigabyte allocations. #[test] -fn oversized_declared_object_size_is_reported_without_panicking() { +fn oversized_declared_object_size_is_reported_without_panicking() -> gix_testtools::TestResult { let mut bytes = data::header::encode(data::Version::V2, 1).to_vec(); - data::entry::Header::Blob - .write_to((i32::MAX as u64) + 1, &mut bytes) - .expect("header write succeeds"); + data::entry::Header::Blob.write_to((i32::MAX as u64) + 1, &mut bytes)?; bytes.extend_from_slice(&[0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]); bytes.extend_from_slice(&[0; 20]); - let file = data::File::from_data(bytes.as_slice(), PathBuf::from("fuzzed-oom.pack"), gix_hash::Kind::Sha1) - .expect("pack header is syntactically valid") + let file = data::File::from_data(bytes.as_slice(), PathBuf::from("fuzzed-oom.pack"), gix_hash::Kind::Sha1)? .with_alloc_limit_bytes(Some(4_000_000)); - let entry = file.entry(FIRST_ENTRY_OFFSET).expect("entry metadata is parseable"); + let entry = file.entry(FIRST_ENTRY_OFFSET)?; let result = catch_unwind(AssertUnwindSafe(|| { file.decode_entry( @@ -135,12 +125,13 @@ fn oversized_declared_object_size_is_reported_without_panicking() { .is_err(), "oversized declared object sizes should be rejected" ); + Ok(()) } /// Reproducer for the allocation-limit fuzz case: with a configured cap, attacker-controlled /// object sizes must fail with `OutOfMemory` instead of attempting the allocation or panicking. #[test] -fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { +fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() -> gix_testtools::TestResult { fn deflate(bytes: &[u8]) -> Vec<u8> { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::BEST_SPEED); out.write_all(bytes).expect("writing to deflater succeeds"); @@ -150,9 +141,7 @@ fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { let object = [0u8; 65]; let mut bytes = data::header::encode(data::Version::V2, 1).to_vec(); - data::entry::Header::Blob - .write_to(object.len() as u64, &mut bytes) - .expect("header write succeeds"); + data::entry::Header::Blob.write_to(object.len() as u64, &mut bytes)?; bytes.extend_from_slice(&deflate(&object)); bytes.extend_from_slice(&[0; 20]); @@ -160,10 +149,9 @@ fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { bytes.as_slice(), PathBuf::from("fuzzed-alloc-limit.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(64)); - let entry = file.entry(FIRST_ENTRY_OFFSET).expect("entry metadata is parseable"); + let entry = file.entry(FIRST_ENTRY_OFFSET)?; let result = catch_unwind(AssertUnwindSafe(|| { file.decode_entry( @@ -186,12 +174,13 @@ fn declared_object_size_over_alloc_limit_bytes_is_reported_as_out_of_memory() { }), Some(gix_error::ResourceExhaustionKind::AllocationLimit) ); + Ok(()) } /// Reproducer for the fuzz target OOM case: with the same allocation cap used by the fuzz harness, /// malformed delta chains must be rejected without panicking or tripping allocator aborts. #[test] -fn runaway_delta_allocation_is_rejected_with_fuzz_alloc_limit() { +fn runaway_delta_allocation_is_rejected_with_fuzz_alloc_limit() -> gix_testtools::TestResult { let bytes = [ 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0xf2, 0x00, 0x00, 0xdd, 0x09, 0x96, 0x98, 0xe7, 0x6c, 0x01, 0x78, 0x9c, 0x7a, 0x73, 0x6a, 0xfd, 0xff, 0x9b, 0x9b, 0x9b, 0x9b, 0x9b, 0x00, 0x5b, @@ -200,8 +189,7 @@ fn runaway_delta_allocation_is_rejected_with_fuzz_alloc_limit() { bytes.as_slice(), PathBuf::from("fuzzed-runaway-delta-allocation.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(64 * 1024 * 1024)); let len = bytes.len() as u64; let mut offsets = vec![0, FIRST_ENTRY_OFFSET.min(len - 1), (len - 1) / 2]; @@ -238,16 +226,15 @@ fn runaway_delta_allocation_is_rejected_with_fuzz_alloc_limit() { saw_parseable_entry, "the reproducer should exercise at least one parseable entry" ); + Ok(()) } /// Reproducer for the invalid ofs-delta base-distance fuzz case: a delta whose base distance points /// before the beginning of the pack must be rejected without panicking. #[test] -fn invalid_ofs_delta_base_distance_is_reported_without_panicking() { +fn invalid_ofs_delta_base_distance_is_reported_without_panicking() -> gix_testtools::TestResult { let mut bytes = data::header::encode(data::Version::V2, 1).to_vec(); - data::entry::Header::OfsDelta { base_distance: 13 } - .write_to(1, &mut bytes) - .expect("header write succeeds"); + data::entry::Header::OfsDelta { base_distance: 13 }.write_to(1, &mut bytes)?; bytes.extend_from_slice(&[0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]); bytes.extend_from_slice(&[0; 20]); @@ -255,10 +242,9 @@ fn invalid_ofs_delta_base_distance_is_reported_without_panicking() { bytes.as_slice(), PathBuf::from("fuzzed-invalid-ofs-delta.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); - let entry = file.entry(FIRST_ENTRY_OFFSET).expect("entry metadata is parseable"); + let entry = file.entry(FIRST_ENTRY_OFFSET)?; let result = catch_unwind(AssertUnwindSafe(|| { file.decode_entry( @@ -276,13 +262,14 @@ fn invalid_ofs_delta_base_distance_is_reported_without_panicking() { .is_err(), "invalid ofs-delta base distances should be rejected as corrupt pack data" ); + Ok(()) } /// Reproducer for the out-of-bounds data-offset fuzz case: malformed entry headers can produce an /// entry whose data offset points past the available pack data, which must be rejected without /// panicking during decompression. #[test] -fn out_of_bounds_entry_data_offset_is_reported_without_panicking() { +fn out_of_bounds_entry_data_offset_is_reported_without_panicking() -> gix_testtools::TestResult { let data = [ 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x97, 0x0e, 0x78, 0x9c, 0x95, 0x8b, 0x51, 0x0a, 0x83, 0x30, 0x10, 0x44, 0xff, 0x3d, 0x45, 0xa0, 0x9f, 0xa5, 0xb2, 0x89, 0x9b, 0x44, 0xa1, 0x94, @@ -302,8 +289,7 @@ fn out_of_bounds_entry_data_offset_is_reported_without_panicking() { data.as_slice(), PathBuf::from("fuzzed-out-of-bounds-data-offset.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); let len = data.len() as u64; let mut offsets = vec![0, FIRST_ENTRY_OFFSET.min(len - 1), (len - 1) / 2]; @@ -332,7 +318,7 @@ fn out_of_bounds_entry_data_offset_is_reported_without_panicking() { let outcome = result.expect("out-of-bounds entry data offsets must not panic"); if outcome.is_err() { - return; + return Ok(()); } } @@ -346,7 +332,7 @@ fn out_of_bounds_entry_data_offset_is_reported_without_panicking() { /// Reproducer for the malformed delta-rescue fuzz case: degenerate delta metadata can force the /// internal instruction buffer relocation path, which must not panic while moving bytes around. #[test] -fn malformed_delta_instruction_relocation_is_reported_without_panicking() { +fn malformed_delta_instruction_relocation_is_reported_without_panicking() -> gix_testtools::TestResult { let data = [ 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x29, 0x00, 0x00, 0x09, 0x97, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, @@ -359,8 +345,7 @@ fn malformed_delta_instruction_relocation_is_reported_without_panicking() { data.as_slice(), PathBuf::from("fuzzed-delta-relocation.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); let len = data.len() as u64; @@ -390,7 +375,7 @@ fn malformed_delta_instruction_relocation_is_reported_without_panicking() { let outcome = result.expect("malformed delta instruction relocation must not panic"); if outcome.is_err() { - return; + return Ok(()); } } @@ -404,7 +389,7 @@ fn malformed_delta_instruction_relocation_is_reported_without_panicking() { /// Reproducer for the runaway delta-chain fuzz case: malformed packs must not be able to grow /// delta bookkeeping without bound before decode eventually fails. #[test] -fn runaway_delta_chain_is_reported_without_panicking() { +fn runaway_delta_chain_is_reported_without_panicking() -> gix_testtools::TestResult { let data = [ 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x00, 0x84, 0x00, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x91, @@ -420,8 +405,7 @@ fn runaway_delta_chain_is_reported_without_panicking() { data.as_slice(), PathBuf::from("fuzzed-runaway-delta-chain.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); let len = data.len() as u64; @@ -449,7 +433,7 @@ fn runaway_delta_chain_is_reported_without_panicking() { let outcome = result.expect("runaway delta chains must not panic or OOM the process"); if outcome.is_err() { - return; + return Ok(()); } } @@ -459,7 +443,7 @@ fn runaway_delta_chain_is_reported_without_panicking() { /// Reproducer for the overlong delta-header fuzz case: malformed varints in delta headers must be /// rejected as corrupt input instead of overflowing the shift width while decoding sizes. #[test] -fn overlong_delta_header_size_is_reported_without_panicking() { +fn overlong_delta_header_size_is_reported_without_panicking() -> gix_testtools::TestResult { let data = [ 0x50, 0x41, 0x43, 0x4b, 0x00, 0x00, 0x00, 0x02, 0x29, 0x00, 0x00, 0x10, 0x68, 0xaf, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x32, 0x18, 0xa1, 0x71, 0x38, 0x95, 0x81, 0x48, 0x58, 0x49, 0x7f, 0x63, 0xe4, 0x4d, 0x6a, 0xe8, 0x85, @@ -471,8 +455,7 @@ fn overlong_delta_header_size_is_reported_without_panicking() { data.as_slice(), PathBuf::from("fuzzed-overlong-delta-header.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); let len = data.len() as u64; @@ -502,7 +485,7 @@ fn overlong_delta_header_size_is_reported_without_panicking() { let outcome = result.expect("overlong delta header sizes must not panic"); if outcome.is_err() { - return; + return Ok(()); } } @@ -516,7 +499,7 @@ fn overlong_delta_header_size_is_reported_without_panicking() { /// Reproducer for the short delta-application fuzz case: malformed delta instructions can produce /// fewer bytes than the advertised result size, which must be rejected without panicking. #[test] -fn short_delta_application_is_reported_without_panicking() { +fn short_delta_application_is_reported_without_panicking() -> gix_testtools::TestResult { fn deflate(bytes: &[u8]) -> Vec<u8> { let mut out = gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::BEST_SPEED); out.write_all(bytes).expect("writing to deflater succeeds"); @@ -529,17 +512,14 @@ fn short_delta_application_is_reported_without_panicking() { let delta = deflate(&delta_payload); let mut bytes = data::header::encode(data::Version::V2, 2).to_vec(); - data::entry::Header::Blob - .write_to(1, &mut bytes) - .expect("header write succeeds"); + data::entry::Header::Blob.write_to(1, &mut bytes)?; bytes.extend_from_slice(&base); let delta_pack_offset = bytes.len() as u64; data::entry::Header::OfsDelta { base_distance: delta_pack_offset - FIRST_ENTRY_OFFSET, } - .write_to(delta_payload.len() as u64, &mut bytes) - .expect("header write succeeds"); + .write_to(delta_payload.len() as u64, &mut bytes)?; bytes.extend_from_slice(&delta); bytes.extend_from_slice(&[0; 20]); @@ -547,8 +527,7 @@ fn short_delta_application_is_reported_without_panicking() { bytes.as_slice(), PathBuf::from("fuzzed-short-delta-application.pack"), gix_hash::Kind::Sha1, - ) - .expect("pack header is syntactically valid") + )? .with_alloc_limit_bytes(Some(8 * 1024 * 1024)); let result = catch_unwind(AssertUnwindSafe(|| { @@ -567,4 +546,5 @@ fn short_delta_application_is_reported_without_panicking() { .is_err(), "short delta applications should be rejected as corrupt pack data" ); + Ok(()) } diff --git a/gix-pack/tests/pack/data/header.rs b/gix-pack/tests/pack/data/header.rs index 7168d2a477e..d8258a965e7 100644 --- a/gix-pack/tests/pack/data/header.rs +++ b/gix-pack/tests/pack/data/header.rs @@ -1,4 +1,3 @@ -use crate::Result; use crate::fixture_path; #[test] @@ -19,7 +18,7 @@ fn unsupported_version_is_not_invalid_input_or_corruption() { } #[test] -fn encode_decode_roundtrip() -> Result { +fn encode_decode_roundtrip() -> gix_testtools::TestResult { let buf = std::fs::read(fixture_path( "objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack", ))?; diff --git a/gix-pack/tests/pack/data/input.rs b/gix-pack/tests/pack/data/input.rs index 06be6cf0bf5..2de01e522b1 100644 --- a/gix-pack/tests/pack/data/input.rs +++ b/gix-pack/tests/pack/data/input.rs @@ -1,5 +1,5 @@ mod lookup_ref_delta_objects { - use crate::Result; + use std::sync::atomic::{AtomicUsize, Ordering}; use gix_hash::{ObjectId, oid}; @@ -95,7 +95,7 @@ mod lookup_ref_delta_objects { } #[test] - fn only_ref_deltas_are_handled() -> Result { + fn only_ref_deltas_are_handled() -> gix_testtools::TestResult { let input = compute_offsets(vec![entry(base(), D_A), entry(delta_ofs(100), D_B)]); let expected = input.clone(); let actual = LookupRefDeltaObjectsIter::new( diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs index dcf4a4f44b3..d6c4034e483 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -14,7 +14,7 @@ use crate::{ }; #[test] -fn invalid_ofs_delta_base_distance_is_an_error() -> Result { +fn invalid_ofs_delta_base_distance_is_an_error() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let first_entry_offset = gix_pack::data::header::SIZE as gix_pack::data::Offset; for base_distance in [first_entry_offset, u64::MAX] { @@ -57,7 +57,7 @@ fn invalid_ofs_delta_base_distance_is_an_error() -> Result { } #[test] -fn thin_pack_base_lookup_distinguishes_errors_from_absence() -> gix_error::TestResult { +fn thin_pack_base_lookup_distinguishes_errors_from_absence() -> gix_testtools::TestResult { use gix_error::ErrorExt; let mut data = Vec::new(); @@ -130,7 +130,7 @@ fn thin_pack_base_lookup_distinguishes_errors_from_absence() -> gix_error::TestR } #[test] -fn traversals() -> Result { +fn traversals() -> gix_testtools::TestResult { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] struct Count { trees: usize, @@ -482,7 +482,7 @@ fn traversals() -> Result { } #[test] -fn tree_additions_from_each_merge_parent_are_kept() -> Result { +fn tree_additions_from_each_merge_parent_are_kept() -> gix_testtools::TestResult { use gix_object::Write; let object_hash = object_hash(); @@ -560,7 +560,7 @@ fn tree_additions_from_each_merge_parent_are_kept() -> Result { /// level 1 being much weaker than it used to be, entries have to be compressed with the /// configured level, defaulting to what `git` uses. #[test] -fn entry_sizes_depend_on_compression_level() -> Result { +fn entry_sizes_depend_on_compression_level() -> gix_testtools::TestResult { use gix_error::Result; use gix_object::WriteTo; let (tree_id, buf) = { diff --git a/gix-pack/tests/pack/index.rs b/gix-pack/tests/pack/index.rs index 55dbb559520..69d15526e04 100644 --- a/gix-pack/tests/pack/index.rs +++ b/gix-pack/tests/pack/index.rs @@ -1,6 +1,5 @@ const SHA1_SIZE: usize = gix_hash::Kind::Sha1.len_in_bytes(); -use crate::Result; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use gix_object::{self as object}; @@ -18,7 +17,7 @@ fn memory_backed_index(at: &str) -> gix_pack::index::File<&'static [u8]> { mod fuzzed; #[test] -fn unsupported_version_is_classified() -> gix_error::TestResult { +fn unsupported_version_is_classified() -> gix_testtools::TestResult { let mut data = std::fs::read(fixture_path(SMALL_PACK_INDEX))?; data[4..8].copy_from_slice(&3u32.to_be_bytes()); let err = gix_pack::index::File::from_data(data, "unsupported.idx".into(), gix_hash::Kind::Sha1) @@ -34,13 +33,12 @@ fn unsupported_version_is_classified() -> gix_error::TestResult { mod version { mod v1 { - use crate::Result; use gix_pack::index; use crate::{INDEX_V1, fixture_path}; #[test] - fn lookup() -> Result { + fn lookup() -> gix_testtools::TestResult { let object_hash = gix_hash::Kind::Sha1; let file = index::File::at(fixture_path(INDEX_V1), object_hash)?; for (id, desired_index, assertion) in &[ @@ -79,13 +77,12 @@ mod version { } mod v2 { - use crate::Result; use gix_pack::index; use crate::{INDEX_V2, fixture_path}; #[test] - fn lookup() -> Result { + fn lookup() -> gix_testtools::TestResult { let object_hash = gix_hash::Kind::Sha1; let file = index::File::at(fixture_path(INDEX_V2), object_hash)?; for (id, expected, assertion_message, hex_len) in [ @@ -148,7 +145,7 @@ mod version { } #[test] - fn write_to_stream() -> Result { + fn write_to_stream() -> gix_testtools::TestResult { fn assert_index_write( mode: &input::Mode, compressed: &input::EntryDataMode, @@ -248,7 +245,7 @@ mod version { } #[test] - fn write_to_stream_respects_alloc_limit_bytes() -> Result { + fn write_to_stream_respects_alloc_limit_bytes() -> gix_testtools::TestResult { let data_path = SMALL_PACK; let mut pack_iter = pack::data::input::BytesToEntriesIter::new_from_header( io::BufReader::new(fs::File::open(fixture_path(data_path))?), @@ -319,7 +316,7 @@ fn traverse_with_index_and_forward_ref_deltas() { } #[test] -fn traverse_with_index_respects_alloc_limit_bytes() -> Result { +fn traverse_with_index_respects_alloc_limit_bytes() -> gix_testtools::TestResult { let index = index::File::at(fixture_path(SMALL_PACK_INDEX), gix_hash::Kind::Sha1)?; let data = pack::data::File::at(fixture_path(SMALL_PACK), gix_hash::Kind::Sha1)?; @@ -395,7 +392,7 @@ static MODES: &[index::verify::Mode] = &[ ]; #[test] -fn pack_lookup() -> Result { +fn pack_lookup() -> gix_testtools::TestResult { for (index_path, pack_path, stats) in &[ ( INDEX_V2, @@ -563,7 +560,7 @@ fn pack_lookup() -> Result { } #[test] -fn verify_integrity_respects_pack_alloc_limit_bytes() -> Result { +fn verify_integrity_respects_pack_alloc_limit_bytes() -> gix_testtools::TestResult { let mut errors = Vec::new(); let prevent_allocation = Some(0); let idx = index::File::at(fixture_path(SMALL_PACK_INDEX), gix_hash::Kind::Sha1)?; @@ -611,7 +608,7 @@ fn verify_integrity_respects_pack_alloc_limit_bytes() -> Result { } #[test] -fn iter() -> Result { +fn iter() -> gix_testtools::TestResult { for (path, kind, num_objects, index_checksum, pack_checksum) in [ ( INDEX_V1, diff --git a/gix-pack/tests/pack/iter.rs b/gix-pack/tests/pack/iter.rs index c858168eb3f..69ffaf1d26a 100644 --- a/gix-pack/tests/pack/iter.rs +++ b/gix-pack/tests/pack/iter.rs @@ -14,7 +14,7 @@ fn size_of_entry() { } mod new_from_header { - use crate::Result; + use std::fs; use gix_odb::{ @@ -25,7 +25,7 @@ mod new_from_header { use crate::{SMALL_PACK, V2_PACKS_AND_INDICES, fixture_path}; #[test] - fn header_encode() -> Result { + fn header_encode() -> gix_testtools::TestResult { for (_, data_file) in V2_PACKS_AND_INDICES { let data = fs::read(fixture_path(data_file))?; for entry in pack::data::input::BytesToEntriesIter::new_from_header( @@ -56,7 +56,7 @@ mod new_from_header { } #[test] - fn generic_iteration() -> Result { + fn generic_iteration() -> gix_testtools::TestResult { for compression_mode in &[ EntryDataMode::Ignore, EntryDataMode::Keep, @@ -104,7 +104,7 @@ mod new_from_header { } #[test] - fn version_3_is_accepted() -> Result { + fn version_3_is_accepted() -> gix_testtools::TestResult { let mut data = fs::read(fixture_path(SMALL_PACK))?; data[4..8].copy_from_slice(&3u32.to_be_bytes()); @@ -128,7 +128,7 @@ mod new_from_header { } #[test] - fn restore_missing_trailer() -> Result { + fn restore_missing_trailer() -> gix_testtools::TestResult { let pack = fs::read(fixture_path(SMALL_PACK))?; let mut iter = pack::data::input::BytesToEntriesIter::new_from_header( std::io::BufReader::new(&pack[..pack.len() - 20]), @@ -147,7 +147,7 @@ mod new_from_header { } #[test] - fn restore_partial_pack() -> Result { + fn restore_partial_pack() -> gix_testtools::TestResult { let pack = fs::read(fixture_path(SMALL_PACK))?; let mut iter = pack::data::input::BytesToEntriesIter::new_from_header( std::io::BufReader::new(&pack[..pack.len() / 2]), diff --git a/gix-pack/tests/pack/malformed.rs b/gix-pack/tests/pack/malformed.rs index 53994c40358..7568d9d1992 100644 --- a/gix-pack/tests/pack/malformed.rs +++ b/gix-pack/tests/pack/malformed.rs @@ -11,7 +11,7 @@ use gix_pack::{cache, data}; const FIRST_ENTRY_OFFSET: data::Offset = data::header::SIZE as data::Offset; #[test] -fn plain_object_buffer_growth_respects_alloc_limit() -> Result { +fn plain_object_buffer_growth_respects_alloc_limit() -> gix_testtools::TestResult { let bytes = blob_pack_with_declared_size(&[b'A'; 65], 65)?; for limit in [Some(65), None] { let pack = data::File::from_data(bytes.clone(), PathBuf::from("allocation.pack"), gix_hash::Kind::Sha1)? @@ -36,7 +36,7 @@ fn plain_object_buffer_growth_respects_alloc_limit() -> Result { } #[test] -fn combined_delta_work_buffers_respect_alloc_limit() -> Result { +fn combined_delta_work_buffers_respect_alloc_limit() -> gix_testtools::TestResult { let mut bytes = blob_pack_with_declared_size(&[b'A'; 64], 64)?; bytes[..data::header::SIZE].copy_from_slice(&data::header::encode(data::Version::V2, 2)); bytes.truncate(bytes.len() - 20); @@ -89,7 +89,7 @@ fn combined_delta_work_buffers_respect_alloc_limit() -> Result { } #[test] -fn resolved_base_and_delta_instructions_respect_alloc_limit() -> Result { +fn resolved_base_and_delta_instructions_respect_alloc_limit() -> gix_testtools::TestResult { let bytes = ref_delta_pack(&[64, 64, 0x90, 64])?; let pack = data::File::from_data(bytes, PathBuf::from("allocation.pack"), gix_hash::Kind::Sha1)? .with_alloc_limit_bytes(Some(64)); @@ -421,7 +421,7 @@ fn ref_delta_header_resolver_some_decodes_external_base() -> TestResult { } #[test] -fn ref_delta_header_cycles_are_rejected() -> Result { +fn ref_delta_header_cycles_are_rejected() -> gix_testtools::TestResult { for (num_entries, close_with_ofs) in [(1, false), (2, false), (3, false), (2, true)] { let (pack, offsets) = ref_delta_chain(num_entries, &[0, 0], close_with_ofs)?; let resolutions = std::cell::Cell::new(0); @@ -441,7 +441,7 @@ fn ref_delta_header_cycles_are_rejected() -> Result { } #[test] -fn ref_delta_entry_cycles_are_rejected() -> Result { +fn ref_delta_entry_cycles_are_rejected() -> gix_testtools::TestResult { for delta in [&[0, 0][..], &[][..]] { for (num_entries, close_with_ofs) in [(1, false), (2, false), (3, false), (2, true)] { let (pack, offsets) = ref_delta_chain(num_entries, delta, close_with_ofs)?; @@ -470,7 +470,7 @@ fn ref_delta_entry_cycles_are_rejected() -> Result { } #[test] -fn delta_chain_metadata_respects_alloc_limit() -> Result { +fn delta_chain_metadata_respects_alloc_limit() -> gix_testtools::TestResult { let (pack, offsets) = ref_delta_chain(12, &[], false)?; let pack = pack.with_alloc_limit_bytes(Some(64)); let err = pack @@ -504,7 +504,7 @@ fn delta_chain_metadata_respects_alloc_limit() -> Result { } #[test] -fn forward_ref_delta_chain_is_accepted() -> Result { +fn forward_ref_delta_chain_is_accepted() -> gix_testtools::TestResult { let (pack, offsets) = ref_delta_chain(12, &[0, 0], false)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; let header = pack.decode_header(entry.clone(), &mut Default::default(), &|base_id| { @@ -578,7 +578,7 @@ fn ref_delta_chain( /// `gix_pack::data::File::decode_entry()` and panic while slicing the base object instead of /// returning an error for attacker-controlled pack data. #[test] -fn delta_copy_is_reported_without_panicking() -> Result { +fn delta_copy_is_reported_without_panicking() -> gix_testtools::TestResult { let pack_data = ref_delta_pack(&[1, 2, 0x90, 0x02])?; let pack = data::File::from_data(pack_data, PathBuf::from("malformed.pack"), gix_hash::Kind::Sha1)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; @@ -603,7 +603,7 @@ fn delta_copy_is_reported_without_panicking() -> Result { /// instead of rejecting the attacker-controlled size header. #[test] #[cfg(target_pointer_width = "64")] -fn oversized_delta_result_is_rejected_without_panicking() -> Result { +fn oversized_delta_result_is_rejected_without_panicking() -> gix_testtools::TestResult { let mut delta = encode_delta_size(1); delta.extend(encode_delta_size(isize::MAX as u64 + 1)); @@ -629,7 +629,7 @@ fn oversized_delta_result_is_rejected_without_panicking() -> Result { /// A delta entry can declare more decompressed bytes than zlib actually produces. Header parsing /// must only inspect the produced bytes, not the zero-filled remainder of the output buffer. #[test] -fn truncated_delta_header_ignores_zero_filled_remainder() -> Result { +fn truncated_delta_header_ignores_zero_filled_remainder() -> gix_testtools::TestResult { let pack_data = ref_delta_pack_with_declared_size(&[1, 0x80], 3)?; let pack = data::File::from_data(pack_data, PathBuf::from("malformed.pack"), gix_hash::Kind::Sha1)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; @@ -646,7 +646,7 @@ fn truncated_delta_header_ignores_zero_filled_remainder() -> Result { } #[test] -fn complete_delta_with_mismatched_declared_size_is_rejected() -> Result { +fn complete_delta_with_mismatched_declared_size_is_rejected() -> gix_testtools::TestResult { for (name, delta, decompressed_size) in [ ("shorter", &[1, 1, 0x90, 1][..], 5), ("longer", &[1, 1, 0x90, 1, 0][..], 4), @@ -668,7 +668,7 @@ fn complete_delta_with_mismatched_declared_size_is_rejected() -> Result { } #[test] -fn plain_object_with_mismatched_declared_size_is_rejected() -> Result { +fn plain_object_with_mismatched_declared_size_is_rejected() -> gix_testtools::TestResult { let mut diagnostics = Vec::new(); for (blob, decompressed_size) in [(b"A".as_slice(), 2), (b"AB".as_slice(), 1)] { let pack_data = blob_pack_with_declared_size(blob, decompressed_size)?; @@ -691,7 +691,7 @@ fn plain_object_with_mismatched_declared_size_is_rejected() -> Result { } #[test] -fn empty_plain_object_is_accepted() -> Result { +fn empty_plain_object_is_accepted() -> gix_testtools::TestResult { let pack_data = blob_pack_with_declared_size(b"", 0)?; let pack = data::File::from_data(pack_data, PathBuf::from("malformed.pack"), gix_hash::Kind::Sha1)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; @@ -707,7 +707,7 @@ fn empty_plain_object_is_accepted() -> Result { } #[test] -fn delta_with_mismatched_base_size_is_rejected_before_allocating_work_buffers() -> Result { +fn delta_with_mismatched_base_size_is_rejected_before_allocating_work_buffers() -> gix_testtools::TestResult { let declared_base_size = 1_000_000; let mut delta = encode_delta_size(declared_base_size); delta.extend([1, 0x90, 1]); @@ -728,7 +728,7 @@ fn delta_with_mismatched_base_size_is_rejected_before_allocating_work_buffers() } #[test] -fn in_pack_delta_base_with_mismatched_declared_size_is_rejected() -> Result { +fn in_pack_delta_base_with_mismatched_declared_size_is_rejected() -> gix_testtools::TestResult { let (pack_data, delta_offset) = ofs_delta_pack_with_mismatched_base_size()?; let pack = data::File::from_data( pack_data.as_slice(), @@ -746,7 +746,7 @@ fn in_pack_delta_base_with_mismatched_declared_size_is_rejected() -> Result { } #[test] -fn decode_header_ignores_zero_filled_delta_remainder() -> Result { +fn decode_header_ignores_zero_filled_delta_remainder() -> gix_testtools::TestResult { let pack_data = ref_delta_pack_with_declared_size(&[1, 0x80], 3)?; let pack = data::File::from_data(pack_data, PathBuf::from("malformed.pack"), gix_hash::Kind::Sha1)?; let entry = pack.entry(FIRST_ENTRY_OFFSET)?; @@ -759,7 +759,7 @@ fn decode_header_ignores_zero_filled_delta_remainder() -> Result { } #[test] -fn decode_header_with_mismatched_declared_delta_size_is_rejected() -> Result { +fn decode_header_with_mismatched_declared_delta_size_is_rejected() -> gix_testtools::TestResult { let mut diagnostics = Vec::new(); for (delta, decompressed_size) in [(&[1, 1, 0x90, 1][..], 5), (&[1, 1, 0x90, 1, 0][..], 4)] { let pack_data = ref_delta_pack_with_declared_size(delta, decompressed_size)?; @@ -781,7 +781,7 @@ fn decode_header_with_mismatched_declared_delta_size_is_rejected() -> Result { } #[test] -fn large_mismatched_declared_delta_size_is_rejected_during_full_decode() -> Result { +fn large_mismatched_declared_delta_size_is_rejected_during_full_decode() -> gix_testtools::TestResult { let mut diagnostics = Vec::new(); for (delta_len, decompressed_size) in [(33, 34), (34, 33)] { let delta = padded_delta(delta_len); diff --git a/gix-pack/tests/pack/multi_index/access.rs b/gix-pack/tests/pack/multi_index/access.rs index eb7492e215c..2b29f90efe9 100644 --- a/gix-pack/tests/pack/multi_index/access.rs +++ b/gix-pack/tests/pack/multi_index/access.rs @@ -90,7 +90,7 @@ fn lookup_missing() { } #[test] -fn general() { +fn general() -> gix_testtools::TestResult { let object_hash = object_hash(); let (file, path) = multi_index(object_hash); @@ -161,25 +161,22 @@ fn general() { let actual_oid = file.oid_at_index(*idx); assert_eq!(actual_oid, *expected_oid); assert_eq!(file.lookup(actual_oid), Some(*idx)); - let (pack_id, pack_offset) = file - .pack_id_and_pack_offset_at_index(*idx) - .expect("fixture contains valid pack and offset references"); + let (pack_id, pack_offset) = file.pack_id_and_pack_offset_at_index(*idx)?; assert_eq!(pack_id, 0, "we only have one pack here"); assert_eq!(pack_offset, *expected_pack_offset); } let mut count = 0; for (idx, entry) in file.iter().enumerate() { - let entry = entry.expect("fixture contains valid pack and offset references"); + let entry = entry?; assert_eq!(entry.oid, file.oid_at_index(idx as u32)); - let (pack_index, pack_offset) = file - .pack_id_and_pack_offset_at_index(idx as u32) - .expect("fixture contains valid pack and offset references"); + let (pack_index, pack_offset) = file.pack_id_and_pack_offset_at_index(idx as u32)?; assert_eq!(pack_index, entry.pack_index); assert_eq!(pack_offset, entry.pack_offset); count += 1; } assert_eq!(count, file.num_objects()); + Ok(()) } #[test] diff --git a/gix-pack/tests/pack/multi_index/fuzzed.rs b/gix-pack/tests/pack/multi_index/fuzzed.rs index 3a3c5a1d504..f13261164cb 100644 --- a/gix-pack/tests/pack/multi_index/fuzzed.rs +++ b/gix-pack/tests/pack/multi_index/fuzzed.rs @@ -55,14 +55,13 @@ fn malformed_fanout_is_reported_without_panicking() { /// Reproducer for the allocation-limit fuzz case: long user-controlled pack names in the `PNAM` /// chunk must be rejected as resource exhaustion when they exceed the configured allocation limit. #[test] -fn long_pack_names_over_alloc_limit_bytes_are_resource_exhaustion() { +fn long_pack_names_over_alloc_limit_bytes_are_resource_exhaustion() -> gix_testtools::TestResult { let long_name = format!("{}.idx", "a".repeat(65)); let index = gix_pack::multi_index::File::from_data( valid_multi_index_with_index_name(long_name.as_bytes()), PathBuf::from("fuzzed-long-name.midx"), None, - ) - .expect("synthetic multi-index is valid"); + )?; assert_eq!(index.index_names(), [PathBuf::from(&long_name)]); @@ -82,6 +81,7 @@ fn long_pack_names_over_alloc_limit_bytes_are_resource_exhaustion() { "an application limit doesn't make otherwise valid input corrupt" ); insta::assert_debug_snapshot!(err, "the error explains the allocation-limit failure", @"Pack names require more memory than allowed"); + Ok(()) } /// Reproducer for the fuzz target OOM case: the harness uses an allocation cap so attacker-controlled @@ -103,7 +103,7 @@ fn absurd_pack_count_is_rejected_with_fuzz_alloc_limit() { } #[test] -fn out_of_bounds_pack_indices_are_rejected_on_access() -> gix_error::TestResult { +fn out_of_bounds_pack_indices_are_rejected_on_access() -> gix_testtools::TestResult { for pack_index in [1, u32::MAX] { let index = gix_pack::multi_index::File::from_data( multi_index_with_offset(b"a.idx", pack_index, 0, None), @@ -135,7 +135,7 @@ fn out_of_bounds_pack_indices_are_rejected_on_access() -> gix_error::TestResult } #[test] -fn out_of_bounds_large_offsets_are_rejected_on_access() -> gix_error::TestResult { +fn out_of_bounds_large_offsets_are_rejected_on_access() -> gix_testtools::TestResult { for ordinal in [1, 0x7fff_ffff] { let index = gix_pack::multi_index::File::from_data( multi_index_with_offset(b"a.idx", 0, (1 << 31) | ordinal, Some(&[1 << 32])), @@ -167,7 +167,7 @@ fn out_of_bounds_large_offsets_are_rejected_on_access() -> gix_error::TestResult } #[test] -fn out_of_bounds_entry_indices_are_validation_errors() -> gix_error::TestResult { +fn out_of_bounds_entry_indices_are_validation_errors() -> gix_testtools::TestResult { let index = gix_pack::multi_index::File::from_data( valid_multi_index_with_index_name(b"a.idx"), PathBuf::from("valid.midx"), @@ -187,7 +187,7 @@ fn out_of_bounds_entry_indices_are_validation_errors() -> gix_error::TestResult } #[test] -fn integrity_verification_rejects_corrupt_offset_references() -> gix_error::TestResult { +fn integrity_verification_rejects_corrupt_offset_references() -> gix_testtools::TestResult { for mut data in [ multi_index_with_offset(b"a.idx", 1, 0, None), multi_index_with_offset(b"a.idx", 0, (1 << 31) | 1, Some(&[1 << 32])), @@ -213,7 +213,7 @@ fn integrity_verification_rejects_corrupt_offset_references() -> gix_error::Test } #[test] -fn high_bit_offsets_follow_the_presence_of_the_large_offset_chunk() -> gix_error::TestResult { +fn high_bit_offsets_follow_the_presence_of_the_large_offset_chunk() -> gix_testtools::TestResult { for (offset, large_offsets, expected) in [ (1 << 31, Some(&[1 << 32][..]), 1 << 32), ((1 << 31) | 1, Some(&[1 << 32, 1 << 33][..]), 1 << 33), @@ -225,8 +225,7 @@ fn high_bit_offsets_follow_the_presence_of_the_large_offset_chunk() -> gix_error multi_index_with_offset(b"a.idx", 0, offset, large_offsets), PathBuf::from("valid-large-offset.midx"), None, - ) - .expect("a high bit indicates a large-offset ordinal only when LOFF is present"); + )?; assert_eq!( index.pack_id_and_pack_offset_at_index(0)?, (0, expected), diff --git a/gix-pack/tests/pack/multi_index/verify.rs b/gix-pack/tests/pack/multi_index/verify.rs index 3280445127b..62567188ff4 100644 --- a/gix-pack/tests/pack/multi_index/verify.rs +++ b/gix-pack/tests/pack/multi_index/verify.rs @@ -1,4 +1,3 @@ -use crate::Result; use std::sync::atomic::AtomicBool; use gix_features::progress; @@ -7,7 +6,7 @@ use maplit::btreemap; use crate::{multi_index::multi_index, object_hash}; #[test] -fn checksum() -> Result { +fn checksum() -> gix_testtools::TestResult { let (file, _) = multi_index(object_hash()); assert_eq!( file.verify_checksum(&mut progress::Discard, &AtomicBool::new(false))?, diff --git a/gix-pack/tests/pack/multi_index/write.rs b/gix-pack/tests/pack/multi_index/write.rs index dcc78989e3f..71b7d56b257 100644 --- a/gix-pack/tests/pack/multi_index/write.rs +++ b/gix-pack/tests/pack/multi_index/write.rs @@ -10,7 +10,7 @@ use gix_testtools::fixture_path; /// Writes a multi-index from the static SHA-1 pack indices, with pinned SHA-1 expectations. /// The SHA-256 counterpart lives in [`from_a_hash_parameterized_pack`] below. #[test] -fn from_paths() -> Result { +fn from_paths() -> gix_testtools::TestResult { let pack_dir = fixture_path("objects/pack"); let written = write_multi_index_from_pack_dir(&pack_dir, gix_hash::Kind::Sha1)?; assert_eq!(written.input_indices.len(), 3); @@ -39,7 +39,7 @@ fn from_paths() -> Result { /// Like [`from_paths`], but sources its input index from the hash-parameterized fixture so the /// writer runs under both SHA-1 and SHA-256. The fixture's gc leaves one pack, hence one index. #[test] -fn from_a_hash_parameterized_pack() -> Result { +fn from_a_hash_parameterized_pack() -> gix_testtools::TestResult { let object_hash = crate::object_hash(); let pack_dir = crate::scripted_fixture_read_only("make_pack_gen_repo_multi_index.sh")?.join(".git/objects/pack"); let written = write_multi_index_from_pack_dir(&pack_dir, object_hash)?; diff --git a/gix-packetline/tests/packetline/decode.rs b/gix-packetline/tests/packetline/decode.rs index c4f622d1482..f03157141fe 100644 --- a/gix-packetline/tests/packetline/decode.rs +++ b/gix-packetline/tests/packetline/decode.rs @@ -1,5 +1,5 @@ mod streaming { - use gix_error::{Result, TestResult}; + use gix_error::Result; use gix_packetline::{ ErrorRef, PacketLineRef, decode::{Stream, streaming}, @@ -29,13 +29,11 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn trailing_line_feeds_are_removed_explicitly() -> gix_error::TestResult { + async fn trailing_line_feeds_are_removed_explicitly() -> gix_testtools::TestResult { let line = decode::all_at_once(b"0006a\n")?; assert_eq!(line.as_text().expect("text").0.as_bstr(), b"a".as_bstr()); let mut out = Vec::new(); - encode_io::write_text(&line.as_text().expect("text"), &mut out) - .await - .expect("write to memory works"); + encode_io::write_text(&line.as_text().expect("text"), &mut out).await?; assert_eq!(out, b"0006a\n", "it appends a newline in text mode"); Ok(()) } @@ -43,7 +41,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn all_kinds_of_packetlines() -> gix_error::TestResult { + async fn all_kinds_of_packetlines() -> gix_testtools::TestResult { for (line, bytes) in &[ (PacketLineRef::ResponseEnd, 4), (PacketLineRef::Delimiter, 4), @@ -60,7 +58,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn error_line() -> gix_error::TestResult { + async fn error_line() -> gix_testtools::TestResult { let mut out = Vec::new(); encode_io::write_error( &PacketLineRef::Data(b"the error").as_error().expect("data line"), @@ -75,7 +73,7 @@ mod streaming { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn side_bands() -> gix_error::TestResult { + async fn side_bands() -> gix_testtools::TestResult { for channel in &[Channel::Data, Channel::Error, Channel::Progress] { let mut out = Vec::new(); let band = PacketLineRef::Data(b"band data") @@ -83,7 +81,7 @@ mod streaming { .expect("data is valid for band"); encode_io::write_band(&band, &mut out).await?; let line = decode::all_at_once(&out)?; - assert_eq!(line.decode_band().expect("valid band"), band); + assert_eq!(line.decode_band()?, band); } Ok(()) } @@ -98,19 +96,19 @@ mod streaming { } #[test] - fn flush() -> TestResult { + fn flush() -> gix_testtools::TestResult { assert_complete(streaming(b"0000someotherstuff"), 4, PacketLineRef::Flush)?; Ok(()) } #[test] - fn trailing_line_feeds_are_not_removed_automatically() -> TestResult { + fn trailing_line_feeds_are_not_removed_automatically() -> gix_testtools::TestResult { assert_complete(streaming(b"0006a\n"), 6, PacketLineRef::Data(b"a\n"))?; Ok(()) } #[test] - fn ignore_extra_bytes() -> TestResult { + fn ignore_extra_bytes() -> gix_testtools::TestResult { assert_complete(streaming(b"0006a\nhello"), 6, PacketLineRef::Data(b"a\n"))?; Ok(()) } @@ -122,7 +120,7 @@ mod streaming { } #[test] - fn error_on_error_line() -> TestResult { + fn error_on_error_line() -> gix_testtools::TestResult { let line = PacketLineRef::Data(b"ERR the error"); assert_complete( streaming(b"0011ERR the error-and just ignored because not part of the size"), @@ -149,7 +147,7 @@ mod streaming { } mod incomplete { - use gix_error::{Result, TestResult}; + use gix_error::Result; use gix_packetline::decode::{Stream, streaming}; fn assert_incomplete(res: Result<Stream>, expected_missing: usize) -> Result { @@ -165,14 +163,14 @@ mod streaming { } #[test] - fn missing_hex_bytes() -> TestResult { + fn missing_hex_bytes() -> gix_testtools::TestResult { assert_incomplete(streaming(b"0"), 3)?; assert_incomplete(streaming(b"00"), 2)?; Ok(()) } #[test] - fn missing_data_bytes() -> TestResult { + fn missing_data_bytes() -> gix_testtools::TestResult { assert_incomplete(streaming(b"0005"), 1)?; assert_incomplete(streaming(b"0006a"), 1)?; Ok(()) diff --git a/gix-packetline/tests/packetline/encode.rs b/gix-packetline/tests/packetline/encode.rs index 370406d45f5..80c18815d89 100644 --- a/gix-packetline/tests/packetline/encode.rs +++ b/gix-packetline/tests/packetline/encode.rs @@ -14,7 +14,7 @@ mod data_to_write { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn binary_and_non_binary() -> gix_error::TestResult { + async fn binary_and_non_binary() -> gix_testtools::TestResult { let mut out = Vec::new(); let res = data_to_write(b"\0", &mut out).await?; assert_eq!(res, 5); @@ -76,7 +76,7 @@ mod text_to_write { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn always_appends_a_newline() -> gix_error::TestResult { + async fn always_appends_a_newline() -> gix_testtools::TestResult { let mut out = Vec::new(); let res = text_to_write(b"a", &mut out).await?; assert_eq!(res, 6); @@ -104,7 +104,7 @@ mod error { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn write_line() -> gix_error::TestResult { + async fn write_line() -> gix_testtools::TestResult { let mut out = Vec::new(); let res = error_to_write(b"hello error", &mut out).await?; assert_eq!(res, 19); @@ -123,7 +123,7 @@ mod flush_delim_response_end { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn success_flush_delim_response_end() -> gix_error::TestResult { + async fn success_flush_delim_response_end() -> gix_testtools::TestResult { let mut out = Vec::new(); let res = flush_to_write(&mut out).await?; assert_eq!(res, 4); diff --git a/gix-packetline/tests/packetline/read/mod.rs b/gix-packetline/tests/packetline/read/mod.rs index 578c1b63cdf..1f69f6217b1 100644 --- a/gix-packetline/tests/packetline/read/mod.rs +++ b/gix-packetline/tests/packetline/read/mod.rs @@ -25,7 +25,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_follows_read_line_delimiter_logic() -> gix_error::TestResult { + async fn peek_follows_read_line_delimiter_logic() -> gix_testtools::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a00000005b"[..], &[PacketLineRef::Flush], false); let res = rd.peek_line().await; assert_eq!(res.expect("line")??, PacketLineRef::Data(b"a")); @@ -53,7 +53,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_follows_read_line_err_logic() -> gix_error::TestResult { + async fn peek_follows_read_line_err_logic() -> gix_testtools::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a0009ERR e0000"[..], &[PacketLineRef::Flush], false); rd.fail_on_err_lines(true); let res = rd.peek_line().await; @@ -85,7 +85,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_eof_is_none() -> gix_error::TestResult { + async fn peek_eof_is_none() -> gix_testtools::TestResult { let mut rd = StreamingPeekableIter::new(&b"0005a0009ERR e0000"[..], &[PacketLineRef::Flush], false); rd.fail_on_err_lines(false); let res = rd.peek_line().await; @@ -108,7 +108,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek_non_data() -> gix_error::TestResult { + async fn peek_non_data() -> gix_testtools::TestResult { let mut inline_error_diagnostics = Vec::new(); let mut rd = StreamingPeekableIter::new(&b"000000010002"[..], &[PacketLineRef::ResponseEnd], false); let res = rd.read_line().await; @@ -164,7 +164,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn fail_on_err_lines() -> gix_error::TestResult { + async fn fail_on_err_lines() -> gix_testtools::TestResult { let input = b"00010009ERR e0002"; let mut rd = StreamingPeekableIter::new(&input[..], &[], false); let res = rd.read_line().await; @@ -207,13 +207,12 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn oversized_packet_lengths_are_reported_instead_of_panicking() -> gix_error::TestResult { + async fn oversized_packet_lengths_are_reported_instead_of_panicking() -> gix_testtools::TestResult { let mut rd = StreamingPeekableIter::new(&b"ffff\n"[..], &[], false); let err = rd .read_line() .await - .expect("a decode error instead of EOF") - .expect("no IO error expected") + .expect("a decode error instead of EOF")? .expect_err("decode should fail for oversized lengths"); insta::assert_debug_snapshot!(err, "oversized packet lengths are reported instead of panicking", @"The data received claims to be larger than the maximum allowed size: got 65535, exceeds 65516"); Ok(()) @@ -222,7 +221,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn peek() -> gix_error::TestResult { + async fn peek() -> gix_testtools::TestResult { let bytes = fixture_bytes("v1/fetch/01-many-refs.response"); let mut rd = StreamingPeekableIter::new(&bytes[..], &[PacketLineRef::Flush], false); let res = rd.peek_line().await; @@ -260,7 +259,7 @@ pub mod streaming_peek_iter { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] - async fn read_from_file_and_reader_advancement() -> gix_error::TestResult { + async fn read_from_file_and_reader_advancement() -> gix_testtools::TestResult { let mut bytes = fixture_bytes("v1/fetch/01-many-refs.response"); bytes.extend(fixture_bytes("v1/fetch/01-many-refs.response")); let mut rd = StreamingPeekableIter::new(&bytes[..], &[PacketLineRef::Flush], false); diff --git a/gix-packetline/tests/packetline/read/sideband.rs b/gix-packetline/tests/packetline/read/sideband.rs index 04edd4a5ab5..62d8bac52be 100644 --- a/gix-packetline/tests/packetline/read/sideband.rs +++ b/gix-packetline/tests/packetline/read/sideband.rs @@ -42,7 +42,7 @@ mod util { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn read_pack_with_progress_extraction() -> gix_error::TestResult { +async fn read_pack_with_progress_extraction() -> gix_testtools::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -105,7 +105,7 @@ async fn read_pack_with_progress_extraction() -> gix_error::TestResult { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> gix_error::TestResult { +async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> gix_testtools::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output-no-binary"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -156,7 +156,7 @@ async fn read_line_trait_method_reads_one_packet_line_at_a_time() -> gix_error:: #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn readline_reads_one_packet_line_at_a_time() -> gix_error::TestResult { +async fn readline_reads_one_packet_line_at_a_time() -> gix_testtools::TestResult { let buf = fixture_bytes("v1/01-clone.combined-output-no-binary"); let mut rd = StreamingPeekableIter::new(&buf[..], &[PacketLineRef::Flush], false); @@ -207,7 +207,7 @@ async fn readline_reads_one_packet_line_at_a_time() -> gix_error::TestResult { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> gix_error::TestResult { +async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> gix_testtools::TestResult { let input = b"0005\x020005\x030000"; let mut rd = StreamingPeekableIter::new(&input[..], &[PacketLineRef::Flush], false); let mut seen = Vec::new(); @@ -233,7 +233,7 @@ async fn empty_progress_and_error_sidebands_are_forwarded_without_panic() -> gix #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn peek_past_an_actual_eof_is_an_error() -> gix_error::TestResult { +async fn peek_past_an_actual_eof_is_an_error() -> gix_testtools::TestResult { let input = b"0009ERR e"; let mut rd = StreamingPeekableIter::new(&input[..], &[], false); let mut reader = rd.as_read(); @@ -274,7 +274,7 @@ async fn peek_past_an_actual_eof_is_an_error() -> gix_error::TestResult { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn peek_past_a_delimiter_is_no_error() -> gix_error::TestResult { +async fn peek_past_a_delimiter_is_no_error() -> gix_testtools::TestResult { let input = b"0009hello0000"; let mut rd = StreamingPeekableIter::new(&input[..], &[PacketLineRef::Flush], false); let mut reader = rd.as_read(); diff --git a/gix-packetline/tests/packetline/write.rs b/gix-packetline/tests/packetline/write.rs index c9e3bd42f63..a5029c5d96d 100644 --- a/gix-packetline/tests/packetline/write.rs +++ b/gix-packetline/tests/packetline/write.rs @@ -16,7 +16,7 @@ const MAX_LINE_LEN: usize = 4 + MAX_DATA_LEN; #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn each_write_results_in_one_line() -> gix_error::TestResult { +async fn each_write_results_in_one_line() -> gix_testtools::TestResult { let mut w = Writer::new(Vec::new()); w.write_all(b"hello").await?; w.write(b"world!").await?; @@ -29,7 +29,7 @@ async fn each_write_results_in_one_line() -> gix_error::TestResult { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn write_text_and_write_binary() -> gix_error::TestResult { +async fn write_text_and_write_binary() -> gix_testtools::TestResult { let buf = { let mut w = Writer::new(Vec::new()); w.enable_text_mode(); @@ -46,7 +46,7 @@ async fn write_text_and_write_binary() -> gix_error::TestResult { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-io", test)] #[cfg_attr(all(feature = "async-io", not(feature = "blocking-io")), async_std::test)] -async fn huge_writes_are_split_into_lines() -> gix_error::TestResult { +async fn huge_writes_are_split_into_lines() -> gix_testtools::TestResult { let buf = { let data = vec![0u8; MAX_DATA_LEN * 2]; let mut w = Writer::new(Vec::new()); diff --git a/gix-path/src/convert.rs b/gix-path/src/convert.rs index 08594c4b0f6..a88d3ed8a88 100644 --- a/gix-path/src/convert.rs +++ b/gix-path/src/convert.rs @@ -1,4 +1,4 @@ -use gix_error::Result; +use gix_error::{OptionExt, Result}; use std::{ borrow::Cow, ffi::{OsStr, OsString}, @@ -16,20 +16,30 @@ fn utf8_error() -> gix_error::Message { /// Like [`into_bstr()`], but takes `OsStr` as input for a lossless, but fallible, conversion. pub fn os_str_into_bstr(path: &OsStr) -> Result<&BStr> { - let path = try_into_bstr(Cow::Borrowed(path.as_ref()))?; - match path { - Cow::Borrowed(path) => Ok(path), - Cow::Owned(_) => unreachable!("borrowed cows stay borrowed"), - } + #[cfg(unix)] + let bytes = { + use std::os::unix::ffi::OsStrExt; + path.as_bytes() + }; + #[cfg(not(unix))] + let bytes = std::str::from_utf8(path.as_encoded_bytes()) + .or_raise(utf8_error)? + .as_bytes(); + Ok(bytes.into()) } /// Like [`into_bstr()`], but takes `OsString` as input for a lossless, but fallible, conversion. pub fn os_string_into_bstring(path: OsString) -> Result<BString> { - let path = try_into_bstr(Cow::Owned(path.into()))?; - match path { - Cow::Borrowed(_path) => unreachable!("borrowed cows stay borrowed"), - Cow::Owned(path) => Ok(path), - } + #[cfg(unix)] + let bytes = { + use std::os::unix::ffi::OsStringExt; + path.into_vec() + }; + #[cfg(not(unix))] + let bytes = String::from_utf8(path.into_encoded_bytes()) + .or_raise(utf8_error)? + .into_bytes(); + Ok(bytes.into()) } /// Like [`into_bstr()`], but takes `Cow<OsStr>` as input for a lossless, but fallible, conversion. @@ -44,41 +54,11 @@ pub fn try_os_str_into_bstr(path: Cow<'_, OsStr>) -> Result<Cow<'_, BStr>> { /// /// On non-Unix platforms, if the source `Path` contains ill-formed, lone surrogates, the UTF-8 conversion will fail /// causing a [`gix_error::Error`] with the encoding failure as its source to be returned. -pub fn try_into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, BStr>> { - let path = path.into(); - let path_str = match path { - Cow::Owned(path) => Cow::Owned({ - #[cfg(unix)] - let p: BString = { - use std::os::unix::ffi::OsStringExt; - path.into_os_string().into_vec().into() - }; - #[cfg(not(unix))] - let p: BString = String::from_utf8(path.into_os_string().into_encoded_bytes()) - .or_raise(utf8_error)? - .into(); - p - }), - Cow::Borrowed(path) => Cow::Borrowed({ - #[cfg(unix)] - let p: &BStr = { - use std::os::unix::ffi::OsStrExt; - path.as_os_str().as_bytes().into() - }; - #[cfg(not(unix))] - let p: &BStr = std::str::from_utf8(path.as_os_str().as_encoded_bytes()) - .or_raise(utf8_error)? - .as_bytes() - .into(); - p - }), - }; - Ok(path_str) -} - -/// Similar to [`try_into_bstr()`] but **panics** if malformed surrogates are encountered on Windows. -pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Cow<'a, BStr> { - try_into_bstr(path).expect("prefix path doesn't contain ill-formed UTF-8") +pub fn into_bstr<'a>(path: impl Into<Cow<'a, Path>>) -> Result<Cow<'a, BStr>> { + match path.into() { + Cow::Owned(path) => os_string_into_bstring(path.into_os_string()).map(Cow::Owned), + Cow::Borrowed(path) => os_str_into_bstr(path.as_os_str()).map(Cow::Borrowed), + } } /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`. @@ -93,10 +73,8 @@ pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into<Cow<'a, BStr>>, path: impl /// Given `input` bytes, produce a `Path` from them ignoring encoding entirely if on Unix. /// -/// On non-Unix platforms, the input is required to be valid UTF-8, which is guaranteed if we wrote it before. -/// There are some potential Git versions and Windows installations which produce malformed UTF-16 -/// if certain emojis are in the path. It's as rare as it sounds, but possible. -pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path> { +/// On non-Unix platforms, invalid UTF-8 returns an error with the encoding failure as its source. +pub fn from_byte_slice(input: &[u8]) -> Result<&Path> { #[cfg(unix)] let p = { use std::os::unix::ffi::OsStrExt; @@ -108,21 +86,16 @@ pub fn try_from_byte_slice(input: &[u8]) -> Result<&Path> { } /// Similar to [`from_byte_slice()`], but takes either borrowed or owned `input`. -pub fn try_from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, Path>> { +pub fn from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, Path>> { let input = input.into(); match input { - Cow::Borrowed(input) => try_from_byte_slice(input).map(Cow::Borrowed), - Cow::Owned(input) => try_from_bstring(input).map(Cow::Owned), + Cow::Borrowed(input) => from_byte_slice(input).map(Cow::Borrowed), + Cow::Owned(input) => from_bstring(input).map(Cow::Owned), } } -/// Similar to [`try_from_bstr()`], but **panics** if malformed surrogates are encountered on Windows. -pub fn from_bstr<'a>(input: impl Into<Cow<'a, BStr>>) -> Cow<'a, Path> { - try_from_bstr(input).expect("prefix path doesn't contain ill-formed UTF-8") -} - -/// Similar to [`try_from_bstr()`], but takes and produces owned data. -pub fn try_from_bstring(input: impl Into<BString>) -> Result<PathBuf> { +/// Similar to [`from_bstr()`], but takes and produces owned data. +pub fn from_bstring(input: impl Into<BString>) -> Result<PathBuf> { let input = input.into(); #[cfg(unix)] let p = { @@ -134,16 +107,6 @@ pub fn try_from_bstring(input: impl Into<BString>) -> Result<PathBuf> { Ok(p) } -/// Similar to [`try_from_bstring()`], but will **panic** if there is ill-formed UTF-8 in the `input`. -pub fn from_bstring(input: impl Into<BString>) -> PathBuf { - try_from_bstring(input).expect("well-formed UTF-8 on windows") -} - -/// Similar to [`try_from_byte_slice()`], but will **panic** if there is ill-formed UTF-8 in the `input`. -pub fn from_byte_slice(input: &[u8]) -> &Path { - try_from_byte_slice(input).expect("well-formed UTF-8 on windows") -} - fn replace<'a>(path: impl Into<Cow<'a, BStr>>, find: u8, replace: u8) -> Cow<'a, BStr> { let path = path.into(); match path { @@ -176,8 +139,8 @@ pub fn to_native_separators<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, BStr> } /// Convert paths with slashes to backslashes on Windows and do nothing on Unix, -/// but **panic** if unpaired surrogates are encountered on Windows. -pub fn to_native_path_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Cow<'a, std::path::Path> { +/// returning an error for invalid UTF-8 on non-Unix platforms. +pub fn to_native_path_on_windows<'a>(path: impl Into<Cow<'a, BStr>>) -> Result<Cow<'a, std::path::Path>> { #[cfg(not(windows))] { crate::from_bstr(path) @@ -255,8 +218,10 @@ pub fn normalize<'a>(path: Cow<'a, Path>, current_dir: &Path) -> Option<Cow<'a, } /// Like [`normalize()`], but treats `..` components beyond the filesystem root as no-ops. -pub fn normalize_saturating<'a>(path: Cow<'a, Path>, current_dir: &Path) -> Cow<'a, Path> { - normalize_inner(path, current_dir, true).expect("saturating normalization always produces a path") +/// Returns an error if normalization exhausts an empty or relative `current_dir`. +pub fn normalize_saturating<'a>(path: Cow<'a, Path>, current_dir: &Path) -> Result<Cow<'a, Path>> { + normalize_inner(path, current_dir, true) + .ok_or_raise(|| gix_error::validation("Cannot normalize a path after exhausting the current directory")) } fn normalize_inner<'a>(path: Cow<'a, Path>, current_dir: &Path, saturate_at_root: bool) -> Option<Cow<'a, Path>> { diff --git a/gix-path/src/env/git/tests.rs b/gix-path/src/env/git/tests.rs index b3bfc01245e..73e5c8097ea 100644 --- a/gix-path/src/env/git/tests.rs +++ b/gix-path/src/env/git/tests.rs @@ -951,6 +951,8 @@ mod exe_info { fn check_exe_info() { let path = exe_info() .map(crate::from_bstring) + .transpose() + .expect("Git reports a representable configuration path") .expect("It is present in the test environment (nonempty config)"); assert!(path.is_absolute(), "Git reports an absolute installation path"); @@ -1056,7 +1058,7 @@ mod exe_info { #[test] #[cfg(unix)] - fn one_scoped_query_finds_both_config_paths() { + fn one_scoped_query_finds_both_config_paths() -> gix_testtools::TestResult { let (_tempdir, executable) = fake_git( r#"#!/bin/sh printf '%s\n' "$*" >> "${0}.log" @@ -1064,9 +1066,8 @@ printf 'unknown\000file:/installation/gitconfig\000core.one\000system\000file:/s "#, ); - let paths = config_paths_from_executable_at(executable.clone()).expect("fake Git can be queried"); assert_eq!( - paths, + config_paths_from_executable_at(executable.clone())?, ConfigPaths { installation: Some("/installation/gitconfig".into()), installation_is_system: false, @@ -1083,11 +1084,12 @@ printf 'unknown\000file:/installation/gitconfig\000core.one\000system\000file:/s invocations[0].contains("--no-includes"), "included files must not be mistaken for top-level configuration paths" ); + Ok(()) } #[test] #[cfg(unix)] - fn retries_without_scope_for_old_git() { + fn retries_without_scope_for_old_git() -> gix_testtools::TestResult { let (_tempdir, executable) = fake_git( r#"#!/bin/sh printf '%s\n' "$*" >> "${0}.log" @@ -1098,9 +1100,8 @@ printf 'file:/legacy/gitconfig\000core.one\000' "#, ); - let paths = config_paths_from_executable_at(executable.clone()).expect("fake Git can be queried"); assert_eq!( - paths, + config_paths_from_executable_at(executable.clone())?, ConfigPaths { installation: Some("/legacy/gitconfig".into()), ..Default::default() @@ -1117,6 +1118,7 @@ printf 'file:/legacy/gitconfig\000core.one\000' invocations.get(1).is_some_and(|line| !line.contains("--show-scope")), "the fallback query omits unsupported scope reporting" ); + Ok(()) } #[test] diff --git a/gix-path/src/env/mod.rs b/gix-path/src/env/mod.rs index 9fb8b7f4061..faf9f776c78 100644 --- a/gix-path/src/env/mod.rs +++ b/gix-path/src/env/mod.rs @@ -22,7 +22,7 @@ mod git; /// This can invoke the Git binary, which is slow on Windows. An unambiguous Git for Windows /// installation identified by `EXEPATH` avoids that invocation. pub fn installation_config() -> Option<&'static Path> { - git::install_config_path().and_then(|p| crate::try_from_byte_slice(p).ok()) + git::install_config_path().and_then(|p| crate::from_byte_slice(p).ok()) } /// Return whether [`installation_config()`] was reported with system scope by Git. @@ -50,7 +50,7 @@ pub fn installation_config_prefix() -> Option<&'static Path> { /// The caller is responsible for applying `GIT_CONFIG_SYSTEM` and `GIT_CONFIG_NOSYSTEM`. pub fn system_config() -> Option<&'static Path> { if cfg!(windows) { - git::system_config_path().and_then(|p| crate::try_from_byte_slice(p).ok()) + git::system_config_path().and_then(|p| crate::from_byte_slice(p).ok()) } else { Some(Path::new("/etc/gitconfig")) } diff --git a/gix-path/src/env/tests.rs b/gix-path/src/env/tests.rs index 7afd9cd8ecc..5028e0e830a 100644 --- a/gix-path/src/env/tests.rs +++ b/gix-path/src/env/tests.rs @@ -205,18 +205,15 @@ mod system_prefix { #[test] #[serial] - fn exepath_nonempty_relative() { + fn exepath_nonempty_relative() -> gix_testtools::TestResult { for name in ["clangarm64", "ucrt64", "mingw64", "mingw32"] { - let grandparent = tempfile::tempdir().expect("can create new temporary directory"); - let parent = grandparent - .path() - .canonicalize() - .expect("path to the new directory works") - .join("dir"); - std::fs::create_dir_all(parent.join(name)).expect("can create directories"); - let _cwd = CurrentDir::set(grandparent.path()).expect("can change to test dir"); + let grandparent = tempfile::tempdir()?; + let parent = grandparent.path().canonicalize()?.join("dir"); + std::fs::create_dir_all(parent.join(name))?; + let _cwd = CurrentDir::set(grandparent.path())?; let outcome = system_prefix_from_exepath_var(|key| if_exepath(key, "dir")); assert_eq!(outcome, None); } + Ok(()) } } diff --git a/gix-path/src/lib.rs b/gix-path/src/lib.rs index de91776d1ee..b5d4dd51b22 100644 --- a/gix-path/src/lib.rs +++ b/gix-path/src/lib.rs @@ -1,7 +1,7 @@ //! This crate contains an assortment of utilities to deal with paths and their conversions. //! -//! Generally `git` treats paths as bytes, but inherently assumes non-illformed UTF-8 as encoding on windows. Internally, it expects -//! slashes to be used as path separators and paths in files must have slashes, with conversions being performed on windows accordingly. +//! Git treats paths as bytes, using UTF-8 for native path conversion on Windows. Paths in Git files use +//! slashes as separators, with conversions to native separators performed on Windows as needed. //! //! ## Examples //! @@ -17,49 +17,16 @@ //! assert_eq!(unix.as_ref(), b"dir/subdir/file".as_bstr()); //! ``` //! -//! <details> +//! ## Path encodings //! -//! ### Research +//! Unix paths contain arbitrary bytes, which these conversions preserve without UTF-8 validation. +//! Windows paths use potentially ill-formed UTF-16: even modern Windows permits unpaired surrogate +//! code units. Such paths cannot be represented losslessly as UTF-8, so conversion to Git path bytes +//! returns an error. Likewise, converting Git path bytes to a native path requires valid UTF-8 on +//! non-Unix platforms. Valid surrogate pairs, including those used for emoji, convert normally. //! -//! * **windows** -//! - [`dirent.c`](https://github.com/git/git/blob/main/compat/win32/dirent.c#L31:L31) contains all implementation (seemingly) of opening directories and reading their entries, along with all path conversions (UTF-16 for windows). This is done on the fly so git can work with [in UTF-8](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). -//! - mingw [is used for the conversion](https://github.com/git/git/blob/main/compat/mingw.h#L579:L579) and it appears they handle surrogates during the conversion, maybe some sort of non-strict UTF-8 converter? Actually it uses [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) -//! under the hood which by now does fail if the UTF-8 would be invalid unicode, i.e. unicode pairs. -//! - `OsString` on windows already stores strings as WTF-8, which supports [surrogate pairs](https://unicodebook.readthedocs.io/unicode_encodings.html), -//! something that UTF-8 isn't allowed do it for security reasons, after all it's UTF-16 specific and exists only to extend -//! the encodable code-points. -//! - informative reading on [WTF-8](https://simonsapin.github.io/wtf-8/#motivation) which is the encoding used by Rust -//! internally that deals with surrogates and non-wellformed surrogates (those that aren't in pairs). -//! * **unix** -//! - It uses [opendir](https://man7.org/linux/man-pages/man3/opendir.3.html) and [readdir](https://man7.org/linux/man-pages/man3/readdir.3.html) -//! respectively. There is no encoding specified, except that these paths are null-terminated. -//! -//! ### Learnings -//! -//! Surrogate pairs are a way to extend the encodable value range in UTF-16 encodings, used primarily on windows and in Javascript. -//! For a long time these codepoints used for surrogates, always to be used in pairs, were not assigned, until…they were for rare -//! emojies and the likes. The unicode standard does not require surrogates to happen in pairs, even though by now unpaired surrogates -//! in UTF-16 are considered ill-formed, which aren't supposed to be converted to UTF-8 for example. -//! -//! This is the reason we have to deal with `to_string_lossy()`, it's _just_ for that quirk. -//! -//! This also means the only platform ever eligible to see conversion errors is windows, and there it's only older pre-vista -//! windows versions which incorrectly allow ill-formed UTF-16 strings. Newer versions don't perform such conversions anymore, for -//! example when going from UTF-16 to UTF-8, they will trigger an error. -//! -//! ### Conclusions -//! -//! Since [WideCharToMultiByte](https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte) by now is -//! fixed (Vista onward) to produce valid UTF-8, lone surrogate codepoints will cause failure, which `git` -//! [doesn't care about](https://github.com/git/git/blob/main/compat/win32/dirent.c#L12:L12). -//! -//! We will, though, which means from now on we can just convert to UTF-8 on windows and bubble up errors where necessary, -//! preventing potential mismatched surrogate pairs to ever be saved on disk by gitoxide. -//! -//! Even though the error only exists on older windows versions, we will represent it in the type system through fallible function calls. -//! Callers may `.expect()` on the result to indicate they don't wish to handle this special and rare case. Note that servers should not -//! ever get into a code-path which does panic though. -//! </details> +//! Conversions preserve borrowed and owned inputs and report encoding failures through +//! [`gix_error::Result`], retaining the concrete encoding error as a source. #![deny(missing_docs)] #![cfg_attr(not(test), deny(unsafe_code))] diff --git a/gix-path/src/relative_path.rs b/gix-path/src/relative_path.rs index ecfc7f74331..f1c2a6a5e81 100644 --- a/gix-path/src/relative_path.rs +++ b/gix-path/src/relative_path.rs @@ -5,7 +5,7 @@ use bstr::{BStr, BString, ByteSlice}; use gix_error::{ResultExt, bail}; use gix_validate::path::component::Options; -use crate::{os_str_into_bstr, try_from_bstr, try_from_byte_slice}; +use crate::{from_byte_slice, os_str_into_bstr}; pub(super) mod types { use bstr::{BStr, ByteSlice}; @@ -32,11 +32,11 @@ pub(super) mod types { use types::RelativePath; impl RelativePath { - fn new_unchecked(value: &BStr) -> Result<&RelativePath> { + fn new_unchecked(value: &BStr) -> &RelativePath { // SAFETY: `RelativePath` is transparent and equivalent to a `&BStr` if provided as reference. #[expect(unsafe_code)] unsafe { - Ok(std::mem::transmute::<&BStr, &RelativePath>(value)) + std::mem::transmute::<&BStr, &RelativePath>(value) } } } @@ -55,7 +55,7 @@ fn relative_path_from_value_and_path<'a>(path_bstr: &'a BStr, path: &Path) -> Re .or_raise(|| validation("Relative path contains an invalid component").with_input(component))?; } - RelativePath::new_unchecked(BStr::new(path_bstr.as_bytes())) + Ok(RelativePath::new_unchecked(path_bstr)) } impl<'a> TryFrom<&'a str> for &'a RelativePath { @@ -70,8 +70,8 @@ impl<'a> TryFrom<&'a BStr> for &'a RelativePath { type Error = gix_error::Error; fn try_from(value: &'a BStr) -> Result<Self> { - let path = try_from_bstr(value)?; - relative_path_from_value_and_path(value, &path) + let path = from_byte_slice(value)?; + relative_path_from_value_and_path(value, path) } } @@ -80,7 +80,7 @@ impl<'a> TryFrom<&'a [u8]> for &'a RelativePath { #[inline] fn try_from(value: &'a [u8]) -> Result<Self> { - let path = try_from_byte_slice(value)?; + let path = from_byte_slice(value)?; relative_path_from_value_and_path(value.as_bstr(), path) } } @@ -90,7 +90,7 @@ impl<'a, const N: usize> TryFrom<&'a [u8; N]> for &'a RelativePath { #[inline] fn try_from(value: &'a [u8; N]) -> Result<Self> { - let path = try_from_byte_slice(value.as_bstr())?; + let path = from_byte_slice(value.as_bstr())?; relative_path_from_value_and_path(value.as_bstr(), path) } } @@ -99,7 +99,7 @@ impl<'a> TryFrom<&'a BString> for &'a RelativePath { type Error = gix_error::Error; fn try_from(value: &'a BString) -> Result<Self> { - let path = try_from_bstr(value.as_bstr())?; - relative_path_from_value_and_path(value.as_bstr(), &path) + let path = from_byte_slice(value.as_bstr())?; + relative_path_from_value_and_path(value.as_bstr(), path) } } diff --git a/gix-path/tests/path/convert/mod.rs b/gix-path/tests/path/convert/mod.rs index 2999d0f6d59..3ddbfff4f9a 100644 --- a/gix-path/tests/path/convert/mod.rs +++ b/gix-path/tests/path/convert/mod.rs @@ -20,6 +20,120 @@ fn assure_windows_separators() { mod normalize; +fn assert_roundtrip(bytes: &[u8]) -> gix_testtools::Result { + use std::borrow::Cow; + + let path = gix_path::from_byte_slice(bytes)?; + let borrowed = gix_path::into_bstr(path)?; + assert!(matches!(borrowed, Cow::Borrowed(_)), "borrowed paths stay borrowed"); + assert_eq!( + borrowed.as_ptr(), + bytes.as_ptr(), + "borrowed conversion keeps the original storage" + ); + assert_eq!(borrowed.as_ref(), bytes.as_bstr(), "conversion preserves every byte"); + assert!( + matches!(gix_path::from_bstr(bytes.as_bstr())?, Cow::Borrowed(_)), + "borrowed bytes stay borrowed" + ); + + let owned = gix_path::from_bstring(bytes.as_bstr())?; + let owned = gix_path::into_bstr(owned)?; + assert!(matches!(owned, Cow::Owned(_)), "owned paths stay owned"); + assert_eq!(owned.as_ref(), bytes.as_bstr(), "owned conversion preserves every byte"); + let owned = gix_path::from_bstr(owned)?; + assert!(matches!(owned, Cow::Owned(_)), "owned bytes stay owned"); + assert_eq!(owned.as_ref(), path, "owned and borrowed conversions agree"); + assert_eq!(gix_path::os_str_into_bstr(path.as_os_str())?, bytes.as_bstr()); + assert_eq!( + gix_path::os_string_into_bstring(path.as_os_str().to_owned())?, + bytes.as_bstr() + ); + assert!(matches!( + gix_path::try_os_str_into_bstr(Cow::Borrowed(path.as_os_str()))?, + Cow::Borrowed(_) + )); + assert!(matches!( + gix_path::try_os_str_into_bstr(Cow::Owned(path.as_os_str().to_owned()))?, + Cow::Owned(_) + )); + Ok(()) +} + +#[test] +fn unicode_paths_preserve_bytes_and_ownership() -> gix_testtools::TestResult { + for input in ["", "directory/file", "ĂŠ/😀"] { + assert_roundtrip(input.as_bytes())?; + } + Ok(()) +} + +#[cfg(unix)] +#[test] +fn unix_paths_preserve_non_utf8_bytes() -> gix_testtools::TestResult { + let bytes = b"directory/\xff\x80\xed\xa0\x80"; + assert_roundtrip(bytes)?; + assert_eq!( + gix_path::to_native_path_on_windows(bytes.as_bstr())? + .as_os_str() + .as_encoded_bytes(), + bytes, + "Unix native paths retain arbitrary bytes" + ); + Ok(()) +} + +#[cfg(windows)] +#[test] +fn all_conversions_reject_invalid_windows_encodings() -> gix_testtools::TestResult { + use std::{ + borrow::Cow, + ffi::OsString, + os::windows::ffi::OsStringExt, + path::{Path, PathBuf}, + }; + + let check = |err: gix_error::Error| { + assert!(err.is_validation(), "invalid path encoding is a validation error"); + assert!( + err.downcast_any_ref::<std::str::Utf8Error>().is_some() + || err.downcast_any_ref::<std::string::FromUtf8Error>().is_some(), + "the encoding failure remains available in the error chain" + ); + }; + for surrogate in [0xd800, 0xdc00] { + let path = OsString::from_wide(&[surrogate]); + check(gix_path::into_bstr(Path::new(&path)).expect_err("borrowed native path rejects lone surrogates")); + check(gix_path::into_bstr(PathBuf::from(&path)).expect_err("owned native path rejects lone surrogates")); + check(gix_path::os_str_into_bstr(&path).expect_err("borrowed OS string rejects lone surrogates")); + check(gix_path::os_string_into_bstring(path.clone()).expect_err("owned OS string rejects lone surrogates")); + check(gix_path::try_os_str_into_bstr(Cow::Borrowed(&path)).expect_err("borrowed Cow rejects lone surrogates")); + check(gix_path::try_os_str_into_bstr(Cow::Owned(path)).expect_err("owned Cow rejects lone surrogates")); + } + for bytes in [b"\xff".as_slice(), b"\xc0\x80", b"\xed\xa0\x80", b"\xe2\x82"] { + check(gix_path::from_byte_slice(bytes).expect_err("byte slice must be UTF-8")); + check(gix_path::from_bstring(bytes.as_bstr()).expect_err("owned bytes must be UTF-8")); + check(gix_path::from_bstr(bytes.as_bstr()).expect_err("borrowed bytes must be UTF-8")); + check(gix_path::from_bstr(bytes.as_bstr().to_owned()).expect_err("owned Cow must be UTF-8")); + check( + gix_path::to_native_path_on_windows(bytes.as_bstr()) + .expect_err("native separators do not hide invalid UTF-8"), + ); + } + let pair = OsString::from_wide(&[0xd83d, 0xde00]); + assert_eq!( + gix_path::os_str_into_bstr(&pair)?, + "😀", + "paired surrogates are valid Unicode" + ); + assert_eq!( + gix_path::from_bstring("😀")?.as_os_str(), + pair, + "valid pairs round-trip losslessly" + ); + Ok(()) +} + #[cfg(windows)] #[test] fn invalid_encodings_retain_their_sources() { @@ -30,8 +144,8 @@ fn invalid_encodings_retain_their_sources() { for err in [ gix_path::os_str_into_bstr(&path).expect_err("a lone surrogate is not UTF-8"), gix_path::os_string_into_bstring(path).expect_err("owned paths reject lone surrogates too"), - gix_path::try_from_byte_slice(b"\xff").expect_err("Windows paths require UTF-8"), - gix_path::try_from_bstring(b"\xff".as_bstr()).expect_err("owned Windows paths require UTF-8"), + gix_path::from_byte_slice(b"\xff").expect_err("Windows paths require UTF-8"), + gix_path::from_bstring(b"\xff".as_bstr()).expect_err("owned Windows paths require UTF-8"), ] { error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert!(err.is_validation(), "invalid encodings remain validation errors"); @@ -120,31 +234,57 @@ mod normalize_saturating { } #[test] - fn walking_up_too_much_stays_at_the_root() { + fn walking_up_too_much_stays_at_the_root() -> gix_testtools::TestResult { let cwd = "/users/name".as_ref(); assert_eq!( - normalize_saturating(p("./a/b/../../../../../actually-valid").into(), cwd).as_ref(), + normalize_saturating(p("./a/b/../../../../../actually-valid").into(), cwd)?.as_ref(), p("/actually-valid") ); assert_eq!( - normalize_saturating(p("/a/b/../../../../actually-valid").into(), cwd).as_ref(), + normalize_saturating(p("/a/b/../../../../actually-valid").into(), cwd)?.as_ref(), p("/actually-valid") ); assert_eq!( - normalize_saturating(p("/a/b/../../../../..").into(), cwd).as_ref(), + normalize_saturating(p("/a/b/../../../../..").into(), cwd)?.as_ref(), p("/") ); + Ok(()) } #[test] - fn preserves_normal_paths() { + fn preserves_normal_paths() -> gix_testtools::TestResult { let path = p("a/b"); - assert!(matches!(normalize_saturating(path.into(), p("/users")), Cow::Borrowed(actual) if actual == path)); + assert!(matches!(normalize_saturating(path.into(), p("/users"))?, Cow::Borrowed(actual) if actual == path)); assert_eq!( - normalize_saturating(p("a/../b").into(), p("/users")).as_ref(), + normalize_saturating(p("a/../b").into(), p("/users"))?.as_ref(), p("b"), "paths that do not reach the root normalize as usual" ); + Ok(()) + } + + #[test] + fn exhausted_relative_cwd_returns_an_error() -> gix_testtools::TestResult { + for (input, cwd) in [("../../x", "a"), ("../..", "")] { + let err = normalize_saturating(p(input).into(), p(cwd)) + .expect_err("the relative current directory cannot supply another parent"); + assert!( + err.is_validation(), + "an exhausted current directory is a validation error" + ); + } + for cwd in ["", "a"] { + assert!( + matches!(normalize_saturating(p("a/b").into(), p(cwd))?, Cow::Borrowed(_)), + "an unused current directory does not prevent borrowing an unchanged path" + ); + assert_eq!( + normalize_saturating(p("a/../b").into(), p(cwd))?.as_ref(), + p("b"), + "local normalization does not require an absolute current directory" + ); + } + Ok(()) } } diff --git a/gix-path/tests/path/convert/normalize.rs b/gix-path/tests/path/convert/normalize.rs index e99401be8d8..b1eee946961 100644 --- a/gix-path/tests/path/convert/normalize.rs +++ b/gix-path/tests/path/convert/normalize.rs @@ -15,7 +15,7 @@ fn no_change_if_there_are_no_trailing_relative_components() { } #[test] -fn special_cases_around_cwd() -> gix_error::TestResult { +fn special_cases_around_cwd() -> gix_testtools::TestResult { let cwd = std::env::current_dir()?; assert_eq!( normalize(p("./../../.git/modules/src/llvm-project").into(), &cwd).unwrap(), diff --git a/gix-path/tests/path/env.rs b/gix-path/tests/path/env.rs index 8d1896447fc..c93023e5915 100644 --- a/gix-path/tests/path/env.rs +++ b/gix-path/tests/path/env.rs @@ -56,10 +56,9 @@ fn core_dir() { } #[test] -fn core_dir_program() { +fn core_dir_program() -> gix_testtools::TestResult { let core_dir = gix_path::env::core_dir().expect("Git is always in PATH when we run tests"); - let programs = std::fs::read_dir(core_dir) - .expect("the core directory can be listed") + let programs = std::fs::read_dir(core_dir)? .filter_map(Result::ok) .filter(|entry| entry.file_type().is_ok_and(|t| t.is_file())) .filter_map(|entry| { @@ -109,6 +108,7 @@ fn core_dir_program() { None, "Windows path separators in installation program names are rejected" ); + Ok(()) } #[test] @@ -140,17 +140,17 @@ fn system_prefix() { #[cfg(windows)] mod windows_prefix { #[test] - fn mixed_installation_keeps_mingw64_active() -> gix_testtools::Result { - mixed_installation("mingw64") + fn mixed_installation_keeps_mingw64_active() -> gix_testtools::TestResult { + Ok(mixed_installation("mingw64")?) } #[test] - fn mixed_installation_keeps_ucrt64_active() -> gix_testtools::Result { - mixed_installation("ucrt64") + fn mixed_installation_keeps_ucrt64_active() -> gix_testtools::TestResult { + Ok(mixed_installation("ucrt64")?) } #[test] - fn shortcut_uses_the_same_installation_and_system_config() -> gix_testtools::Result { + fn shortcut_uses_the_same_installation_and_system_config() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } diff --git a/gix-path/tests/path/realpath.rs b/gix-path/tests/path/realpath.rs index ba95ca5e375..f93b5b9e0e1 100644 --- a/gix-path/tests/path/realpath.rs +++ b/gix-path/tests/path/realpath.rs @@ -1,4 +1,4 @@ -use gix_error::{Result, TestResult}; +use gix_error::Result; use std::{ path::{Path, PathBuf}, time::Duration, @@ -14,7 +14,7 @@ fn assert_validation<T>(result: Result<T>) -> gix_error::Error { } #[test] -fn fuzzed_timeout() -> TestResult { +fn fuzzed_timeout() -> gix_testtools::TestResult { let path = PathBuf::from(std::fs::read("tests/fixtures/fuzzed/54k-path-components.path")?.into_string()?); assert_eq!(path.components().count(), 54862); let start = std::time::Instant::now(); @@ -34,7 +34,7 @@ fn fuzzed_timeout() -> TestResult { } #[test] -fn assorted() -> TestResult { +fn assorted() -> gix_testtools::TestResult { let cwd = tempfile::tempdir()?; let cwd = cwd.path(); let symlinks_disabled = 0; @@ -88,7 +88,7 @@ fn assorted() -> TestResult { #[test] #[cfg(windows)] -fn drive_relative_paths_use_the_supplied_directory_on_the_same_drive() -> TestResult { +fn drive_relative_paths_use_the_supplied_directory_on_the_same_drive() -> gix_testtools::TestResult { for cwd in [r"C:\gix-realpath-test-base", r"\\?\C:\gix-realpath-test-base"] { let cwd = Path::new(cwd); for (input, suffix) in [ @@ -114,7 +114,7 @@ fn drive_relative_paths_use_the_supplied_directory_on_the_same_drive() -> TestRe #[test] #[cfg(windows)] -fn drive_relative_paths_use_windows_drive_directories_when_needed() -> TestResult { +fn drive_relative_paths_use_windows_drive_directories_when_needed() -> gix_testtools::TestResult { for input in ["C:", "C:gix-realpath-test-missing", "D:gix-realpath-test-missing"] { let input = Path::new(input); let expected = std::path::absolute(input)?; @@ -133,7 +133,7 @@ fn drive_relative_paths_use_windows_drive_directories_when_needed() -> TestResul } #[test] -fn parent_components_are_resolved_after_symlinks() -> TestResult { +fn parent_components_are_resolved_after_symlinks() -> gix_testtools::TestResult { let tmp = canonicalized_tempdir()?; let target = tmp.path().join("target/nested"); std::fs::create_dir_all(&target)?; @@ -148,7 +148,7 @@ fn parent_components_are_resolved_after_symlinks() -> TestResult { } #[test] -fn link_cycle_is_detected() -> TestResult { +fn link_cycle_is_detected() -> gix_testtools::TestResult { let tmp_dir = canonicalized_tempdir()?; let dir = tmp_dir.path(); let link_name = "link"; @@ -162,7 +162,7 @@ fn link_cycle_is_detected() -> TestResult { } #[test] -fn symlink_with_absolute_path_gets_expanded() -> TestResult { +fn symlink_with_absolute_path_gets_expanded() -> gix_testtools::TestResult { let tmp_dir = canonicalized_tempdir()?; let dir = tmp_dir.path(); let link_from = dir.join("a").join("b").join("tmp_p_q_link"); @@ -178,7 +178,7 @@ fn symlink_with_absolute_path_gets_expanded() -> TestResult { } #[test] -fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> TestResult { +fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> gix_testtools::TestResult { let cwd = canonicalized_tempdir()?; let dir = cwd.path(); let link_name = "pq_link"; @@ -192,7 +192,7 @@ fn symlink_to_relative_path_gets_expanded_into_absolute_path() -> TestResult { } #[test] -fn symlink_processing_is_disabled_if_the_value_is_zero() -> TestResult { +fn symlink_processing_is_disabled_if_the_value_is_zero() -> gix_testtools::TestResult { let cwd = canonicalized_tempdir()?; let link_name = "x_link"; create_symlink(cwd.path().join(link_name), Path::new("link destination does not exist"))?; diff --git a/gix-pathspec/src/pattern.rs b/gix-pathspec/src/pattern.rs index 603765c4c98..4a18d758c27 100644 --- a/gix-pathspec/src/pattern.rs +++ b/gix-pathspec/src/pattern.rs @@ -60,7 +60,7 @@ impl Pattern { if count > 0 { count as usize } else { Default::default() } } - let mut path = gix_path::from_bstr(self.path.as_bstr()); + let mut path = gix_path::from_bstr(self.path.as_bstr())?; let mut num_prefix_components = 0; let mut was_absolute = false; if gix_path::is_absolute(path.as_ref()) { @@ -70,7 +70,7 @@ impl Pattern { Err(_) => { bail!( validation(format!("The path is not inside of the worktree \"{}\"", root.display())) - .with_input(gix_path::into_bstr(path.into_owned()).into_owned()) + .with_input(gix_path::into_bstr(path.into_owned())?.into_owned()) ); } }; @@ -108,7 +108,7 @@ impl Pattern { None => { bail!( validation("The path leaves the repository") - .with_input(gix_path::into_bstr(path.into_owned()).into_owned()) + .with_input(gix_path::into_bstr(path.into_owned())?.into_owned()) ); } }; @@ -118,7 +118,7 @@ impl Pattern { BString::from(".") } else { let cleaned = PathBuf::from_iter(path.components().filter(|c| !matches!(c, Component::CurDir))); - let mut out = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(cleaned)).into_owned(); + let mut out = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(cleaned)?).into_owned(); self.prefix_len = { if self.signature.contains(MagicSignature::MUST_BE_DIR) { out.push(b'/'); diff --git a/gix-pathspec/src/search/mod.rs b/gix-pathspec/src/search/mod.rs index e9a3ec1a24d..52e390dda24 100644 --- a/gix-pathspec/src/search/mod.rs +++ b/gix-pathspec/src/search/mod.rs @@ -1,6 +1,7 @@ use std::{borrow::Cow, path::Path}; use bstr::{BStr, ByteSlice}; +use gix_error::Result; use crate::{MagicSignature, Pattern, Search}; @@ -67,7 +68,8 @@ impl Search { /// as they are not quire the same. /// /// See also: [`maybe_prefix_directory()`](Self::longest_common_directory). - pub fn prefix_directory(&self) -> Cow<'_, Path> { + /// Returns an error if the directory cannot be represented as a platform path. + pub fn prefix_directory(&self) -> Result<Cow<'_, Path>> { gix_path::from_bstr( self.patterns .iter() @@ -81,8 +83,11 @@ impl Search { /// Note that if it is returned, it's guaranteed to be longer than the [prefix-directory](Self::prefix_directory). /// /// Returns `None` if the returned directory would be empty, or if all pathspecs are exclusive. - pub fn longest_common_directory(&self) -> Option<Cow<'_, Path>> { - let first_non_excluded = self.patterns.iter().find(|p| !p.value.pattern.is_excluded())?; + /// Returns an error if the directory cannot be represented as a platform path. + pub fn longest_common_directory(&self) -> Result<Option<Cow<'_, Path>>> { + let Some(first_non_excluded) = self.patterns.iter().find(|p| !p.value.pattern.is_excluded()) else { + return Ok(None); + }; let common_prefix = first_non_excluded.value.pattern.path[..self.common_prefix_len].as_bstr(); let stripped_prefix = if first_non_excluded .value @@ -92,9 +97,12 @@ impl Search { { common_prefix } else { - common_prefix[..common_prefix.rfind_byte(b'/')?].as_bstr() + let Some(last_slash) = common_prefix.rfind_byte(b'/') else { + return Ok(None); + }; + common_prefix[..last_slash].as_bstr() }; - Some(gix_path::from_bstr(stripped_prefix)) + gix_path::from_bstr(stripped_prefix).map(Some) } } diff --git a/gix-pathspec/tests/defaults.rs b/gix-pathspec/tests/defaults.rs index 979b64e8e1c..24c8bc0bd15 100644 --- a/gix-pathspec/tests/defaults.rs +++ b/gix-pathspec/tests/defaults.rs @@ -3,7 +3,7 @@ use serial_test::serial; #[test] #[serial] -fn literal_only_combines_with_icase() -> gix_testtools::Result { +fn literal_only_combines_with_icase() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; { let _env = gix_testtools::Env::new() @@ -37,7 +37,7 @@ fn literal_only_combines_with_icase() -> gix_testtools::Result { } #[test] #[serial] -fn nothing_is_set_then_it_is_like_the_default_impl() -> gix_testtools::Result { +fn nothing_is_set_then_it_is_like_the_default_impl() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; assert_eq!( Defaults::from_environment(&mut |name| std::env::var_os(name))?, @@ -48,7 +48,7 @@ fn nothing_is_set_then_it_is_like_the_default_impl() -> gix_testtools::Result { #[test] #[serial] -fn glob_and_noglob_cause_error() -> gix_testtools::Result { +fn glob_and_noglob_cause_error() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_GLOB_PATHSPECS", "1") .set("GIT_NOGLOB_PATHSPECS", "yes"); @@ -73,7 +73,7 @@ fn invalid_values_retain_the_config_validation_error() { #[test] #[serial] -fn noglob_works() -> gix_testtools::Result { +fn noglob_works() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_GLOB_PATHSPECS", "0") .set("GIT_NOGLOB_PATHSPECS", "true"); @@ -91,7 +91,7 @@ fn noglob_works() -> gix_testtools::Result { #[test] #[serial] -fn glob_works() -> gix_testtools::Result { +fn glob_works() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_GLOB_PATHSPECS", "yes"); assert_eq!( Defaults::from_environment(&mut |name| std::env::var_os(name))?, diff --git a/gix-pathspec/tests/normalize/mod.rs b/gix-pathspec/tests/normalize/mod.rs index 007f921fd1b..24cae3e78e0 100644 --- a/gix-pathspec/tests/normalize/mod.rs +++ b/gix-pathspec/tests/normalize/mod.rs @@ -1,8 +1,8 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; #[test] -fn consuming_the_entire_prefix_does_not_lead_to_a_single_dot() -> Result { +fn consuming_the_entire_prefix_does_not_lead_to_a_single_dot() -> TestResult { let spec = normalized_spec("..", "a", "")?; assert_eq!( spec.path(), @@ -18,7 +18,7 @@ fn consuming_the_entire_prefix_does_not_lead_to_a_single_dot() -> Result { } #[test] -fn removes_relative_path_components() -> Result { +fn removes_relative_path_components() -> TestResult { for (input_path, expected_path, expected_prefix) in [ ("..", "a", ""), ("c", "a/b/c", "a/b"), @@ -47,7 +47,7 @@ fn removes_relative_path_components() -> Result { } #[test] -fn single_dot_is_special_and_directory_is_implied_without_trailing_slash() -> Result { +fn single_dot_is_special_and_directory_is_implied_without_trailing_slash() -> TestResult { for (input_path, expected) in [(".", "."), ("./", ".")] { let spec = normalized_spec(input_path, "", "/repo")?; assert_eq!(spec.path(), expected); @@ -58,7 +58,7 @@ fn single_dot_is_special_and_directory_is_implied_without_trailing_slash() -> Re } #[test] -fn absolute_path_made_relative() -> Result { +fn absolute_path_made_relative() -> TestResult { for (input_path, expected, prefix_dir) in [ ("/repo/a", "a", ""), ("/repo/a/..//.///b", "b", ""), @@ -79,7 +79,7 @@ fn absolute_path_made_relative() -> Result { } #[test] -fn relative_top_patterns_ignore_the_prefix() -> Result { +fn relative_top_patterns_ignore_the_prefix() -> TestResult { let spec = normalized_spec(":(top)c", "a/b", "")?; assert_eq!(spec.path(), "c"); assert_eq!(spec.prefix_directory(), ""); @@ -87,7 +87,7 @@ fn relative_top_patterns_ignore_the_prefix() -> Result { } #[test] -fn absolute_top_patterns_ignore_the_prefix_but_are_made_relative() -> Result { +fn absolute_top_patterns_ignore_the_prefix_but_are_made_relative() -> TestResult { let spec = normalized_spec(":(top)/a/b", "prefix-ignored", "/a")?; assert_eq!(spec.path(), "b"); assert_eq!(spec.prefix_directory(), ""); diff --git a/gix-pathspec/tests/parse/valid.rs b/gix-pathspec/tests/parse/valid.rs index 43c170ba99d..f3853f0dbc4 100644 --- a/gix-pathspec/tests/parse/valid.rs +++ b/gix-pathspec/tests/parse/valid.rs @@ -1,6 +1,6 @@ -use crate::Result; use gix_attributes::State; use gix_pathspec::{MagicSignature, SearchMode}; +use gix_testtools::TestResult; use crate::parse::{NormalizedPattern, check_against_baseline, check_valid_inputs}; @@ -59,7 +59,7 @@ fn glob_negations_are_always_literal() { } #[test] -fn literal_default_prevents_parsing() { +fn literal_default_prevents_parsing() -> gix_testtools::TestResult { let pattern = gix_pathspec::parse( ":".as_bytes(), gix_pathspec::Defaults { @@ -67,8 +67,7 @@ fn literal_default_prevents_parsing() { search_mode: SearchMode::PathAwareGlob, literal: true, }, - ) - .expect("valid"); + )?; assert!(!pattern.is_nil()); assert_eq!(pattern.path(), ":"); assert!(matches!(pattern.search_mode, SearchMode::Literal)); @@ -81,8 +80,7 @@ fn literal_default_prevents_parsing() { search_mode: SearchMode::Literal, literal: true, }, - ) - .expect("valid"); + )?; assert_eq!(pattern.path(), input, "no parsing happens at all"); assert!(matches!(pattern.search_mode, SearchMode::Literal)); @@ -93,16 +91,16 @@ fn literal_default_prevents_parsing() { search_mode: SearchMode::Literal, literal: false, }, - ) - .expect("valid"); + )?; assert_eq!(pattern.path(), "f[o][o]", "in literal default mode, we still parse"); assert!(matches!(pattern.search_mode, SearchMode::Literal)); + Ok(()) } #[test] -fn there_is_no_pathspec_pathspec() { +fn there_is_no_pathspec_pathspec() -> gix_testtools::TestResult { check_against_baseline(":"); - let pattern = gix_pathspec::parse(":".as_bytes(), Default::default()).expect("valid"); + let pattern = gix_pathspec::parse(":".as_bytes(), Default::default())?; assert!(pattern.is_nil()); let actual: NormalizedPattern = pattern.into(); @@ -115,13 +113,13 @@ fn there_is_no_pathspec_pathspec() { search_mode: SearchMode::PathAwareGlob, literal: false, }, - ) - .expect("valid"); + )?; assert!(pattern.is_nil()); + Ok(()) } #[test] -fn defaults_are_used() -> Result { +fn defaults_are_used() -> TestResult { let defaults = gix_pathspec::Defaults { signature: MagicSignature::EXCLUDE, search_mode: SearchMode::Literal, @@ -137,7 +135,7 @@ fn defaults_are_used() -> Result { } #[test] -fn literal_from_defaults_is_overridden_by_element_glob() -> Result { +fn literal_from_defaults_is_overridden_by_element_glob() -> TestResult { let defaults = gix_pathspec::Defaults { search_mode: SearchMode::Literal, ..Default::default() @@ -152,7 +150,7 @@ fn literal_from_defaults_is_overridden_by_element_glob() -> Result { } #[test] -fn glob_from_defaults_is_overridden_by_element_glob() -> Result { +fn glob_from_defaults_is_overridden_by_element_glob() -> TestResult { let defaults = gix_pathspec::Defaults { search_mode: SearchMode::PathAwareGlob, ..Default::default() diff --git a/gix-pathspec/tests/search/mod.rs b/gix-pathspec/tests/search/mod.rs index 527d8ebcb89..1362c30bf5e 100644 --- a/gix-pathspec/tests/search/mod.rs +++ b/gix-pathspec/tests/search/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use bstr::BStr; @@ -12,12 +12,12 @@ fn is_send_with_parallel_enabled() { } #[test] -fn directories() -> Result { - baseline::run("directory", true, baseline::directories) +fn directories() -> TestResult { + baseline::run("directory", true, baseline::directories).map_err(Into::into) } #[test] -fn directory_matches_prefix() -> Result { +fn directory_matches_prefix() -> TestResult { for spec in ["dir", "dir/", "di*", "dir/*", "dir/*.o"] { for specs in [&[spec] as &[_], &[spec, "other"]] { let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?; @@ -59,7 +59,7 @@ fn directory_matches_prefix() -> Result { } #[test] -fn directory_matches_prefix_starting_wildcards_always_match() -> Result { +fn directory_matches_prefix_starting_wildcards_always_match() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["*ir"]), None, Path::new(""))?; assert!(search.directory_matches_prefix("dir".into(), false)); assert!(search.directory_matches_prefix("d".into(), false)); @@ -67,7 +67,7 @@ fn directory_matches_prefix_starting_wildcards_always_match() -> Result { } #[test] -fn empty_dir_always_matches() -> Result { +fn empty_dir_always_matches() -> TestResult { for specs in [ &["*ir"] as &[_], &[], @@ -92,7 +92,7 @@ fn empty_dir_always_matches() -> Result { } #[test] -fn directory_matches_prefix_leading() -> Result { +fn directory_matches_prefix_leading() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["d/d/generated/b"]), None, Path::new(""))?; assert!(!search.directory_matches_prefix("di".into(), false)); assert!(!search.directory_matches_prefix("di".into(), true)); @@ -115,7 +115,7 @@ fn directory_matches_prefix_leading() -> Result { } #[test] -fn directory_matches_prefix_negative_wildcard() -> Result { +fn directory_matches_prefix_negative_wildcard() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&[":!*generated*"]), None, Path::new(""))?; assert!( search.directory_matches_prefix("di".into(), false), @@ -138,7 +138,7 @@ fn directory_matches_prefix_negative_wildcard() -> Result { } #[test] -fn directory_matches_prefix_all_excluded() -> Result { +fn directory_matches_prefix_all_excluded() -> TestResult { for spec in ["!dir", "!dir/", "!d*", "!di*", "!dir/*", "!dir/*.o", "!*ir"] { for specs in [&[spec] as &[_], &[spec, "other"]] { let search = gix_pathspec::Search::from_specs(pathspecs(specs), None, Path::new(""))?; @@ -152,7 +152,7 @@ fn directory_matches_prefix_all_excluded() -> Result { } #[test] -fn no_pathspecs_match_everything() -> Result { +fn no_pathspecs_match_everything() -> TestResult { let mut search = gix_pathspec::Search::from_specs([], None, Path::new(""))?; assert_eq!(search.patterns().count(), 0, "nothing artificial is added"); let m = search @@ -170,7 +170,7 @@ fn no_pathspecs_match_everything() -> Result { } #[test] -fn included_directory_and_excluded_subdir_top_level_with_prefix() -> Result { +fn included_directory_and_excluded_subdir_top_level_with_prefix() -> TestResult { let mut search = gix_pathspec::Search::from_specs(pathspecs(&[":/foo", ":!/foo/target/"]), None, Path::new("foo"))?; let m = search .pattern_matching_relative_path("foo".into(), Some(true), &mut no_attrs) @@ -211,7 +211,7 @@ fn included_directory_and_excluded_subdir_top_level_with_prefix() -> Result { } #[test] -fn starts_with() -> Result { +fn starts_with() -> TestResult { let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/*"]), None, Path::new(""))?; assert!( search @@ -251,7 +251,7 @@ fn starts_with() -> Result { } #[test] -fn simplified_search_respects_must_be_dir() -> Result { +fn simplified_search_respects_must_be_dir() -> TestResult { let mut search = gix_pathspec::Search::from_specs(pathspecs(&["a/be/"]), None, Path::new(""))?; assert_eq!( search @@ -320,7 +320,7 @@ fn simplified_search_respects_must_be_dir() -> Result { } #[test] -fn simplified_search_respects_ignore_case() -> Result { +fn simplified_search_respects_ignore_case() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&[":(icase)foo/**/bar"]), None, Path::new(""))?; assert!(search.can_match_relative_path("Foo".into(), None)); assert!(search.can_match_relative_path("foo".into(), Some(true))); @@ -330,7 +330,7 @@ fn simplified_search_respects_ignore_case() -> Result { } #[test] -fn simplified_search_respects_all_excluded() -> Result { +fn simplified_search_respects_all_excluded() -> TestResult { let search = gix_pathspec::Search::from_specs( pathspecs(&[":(exclude)a/file", ":(exclude)b/file"]), None, @@ -351,7 +351,7 @@ fn simplified_search_respects_all_excluded() -> Result { } #[test] -fn simplified_search_wildcards() -> Result { +fn simplified_search_wildcards() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["**/a*"]), None, Path::new(""))?; assert!( search.can_match_relative_path("a".into(), None), @@ -367,7 +367,7 @@ fn simplified_search_wildcards() -> Result { } #[test] -fn simplified_search_wildcards_simple() -> Result { +fn simplified_search_wildcards_simple() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["dir/*"]), None, Path::new(""))?; for is_dir in [None, Some(false), Some(true)] { assert!( @@ -392,7 +392,7 @@ fn simplified_search_wildcards_simple() -> Result { } #[test] -fn simplified_search_handles_nil() -> Result { +fn simplified_search_handles_nil() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&[":"]), None, Path::new(""))?; assert!(search.can_match_relative_path("a".into(), None), "everything matches"); assert!(search.can_match_relative_path("a".into(), Some(false))); @@ -412,12 +412,12 @@ fn simplified_search_handles_nil() -> Result { } #[test] -fn longest_common_directory_no_prefix() -> Result { +fn longest_common_directory_no_prefix() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["tests/a/", "tests/b/", ":!*.sh"]), None, Path::new(""))?; assert_eq!(search.common_prefix(), "tests/"); - assert_eq!(search.prefix_directory(), Path::new("")); + assert_eq!(search.prefix_directory()?, Path::new("")); assert_eq!( - search.longest_common_directory().expect("present").to_string_lossy(), + search.longest_common_directory()?.expect("present").to_string_lossy(), "tests/", "trailing slashes are not stripped" ); @@ -425,7 +425,7 @@ fn longest_common_directory_no_prefix() -> Result { } #[test] -fn longest_common_directory_with_prefix() -> Result { +fn longest_common_directory_with_prefix() -> TestResult { let search = gix_pathspec::Search::from_specs( pathspecs(&["tests/a/", "tests/b/", ":!*.sh"]), Some(Path::new("a/b")), @@ -433,12 +433,12 @@ fn longest_common_directory_with_prefix() -> Result { )?; assert_eq!(search.common_prefix(), "a/b/tests/"); assert_eq!( - search.prefix_directory().to_string_lossy(), + search.prefix_directory()?.to_string_lossy(), "a/b", "trailing slashes are not contained" ); assert_eq!( - search.longest_common_directory().expect("present").to_string_lossy(), + search.longest_common_directory()?.expect("present").to_string_lossy(), "a/b/tests/", "trailing slashes are present, they don't matter" ); @@ -446,7 +446,7 @@ fn longest_common_directory_with_prefix() -> Result { } #[test] -fn init_with_exclude() -> Result { +fn init_with_exclude() -> TestResult { let search = gix_pathspec::Search::from_specs(pathspecs(&["tests/", ":!*.sh"]), None, Path::new(""))?; assert_eq!(search.patterns().count(), 2, "nothing artificial is added"); assert!( @@ -455,12 +455,12 @@ fn init_with_exclude() -> Result { ); assert_eq!(search.common_prefix(), "tests"); assert_eq!( - search.prefix_directory(), + search.prefix_directory()?, Path::new(""), "there was no prefix during initialization" ); assert_eq!( - search.longest_common_directory(), + search.longest_common_directory()?, Some(Path::new("tests").into()), "but this works here, and it should be tested" ); @@ -477,7 +477,7 @@ fn init_with_exclude() -> Result { } #[test] -fn no_pathspecs_respect_prefix() -> Result { +fn no_pathspecs_respect_prefix() -> TestResult { let mut search = gix_pathspec::Search::from_specs([], Some(Path::new("a")), Path::new(""))?; assert_eq!( search.patterns().count(), @@ -509,7 +509,7 @@ fn no_pathspecs_respect_prefix() -> Result { } #[test] -fn prefixes_are_always_case_sensitive() -> Result { +fn prefixes_are_always_case_sensitive() -> TestResult { let path = gix_testtools::scripted_fixture_read_only("match_baseline_files.sh")?.join("paths"); let items = baseline::parse_paths(path)?; @@ -555,7 +555,7 @@ fn prefixes_are_always_case_sensitive() -> Result { Path::new(""), )?; assert_eq!(search.common_prefix(), common_prefix, "{spec} {prefix}"); - assert_eq!(search.prefix_directory(), Path::new(expected_common_dir)); + assert_eq!(search.prefix_directory()?, Path::new(expected_common_dir)); let actual: Vec<_> = items .iter() .filter(|relative_path| { @@ -586,7 +586,7 @@ fn prefixes_are_always_case_sensitive() -> Result { } #[test] -fn common_prefix() -> Result { +fn common_prefix() -> TestResult { for (specs, prefix, expected_common_prefix, expected_common_dir) in [ (&["foo/bar", ":(icase)foo/bar"] as &[_], None, "", ""), (&["foo/bar", "foo"], None, "foo", ""), @@ -604,7 +604,7 @@ fn common_prefix() -> Result { )?; assert_eq!(search.common_prefix(), expected_common_prefix, "{specs:?} {prefix:?}"); assert_eq!( - search.prefix_directory(), + search.prefix_directory()?, Path::new(expected_common_dir), "{specs:?} {prefix:?}" ); @@ -613,8 +613,8 @@ fn common_prefix() -> Result { } #[test] -fn files() -> Result { - baseline::run("file", false, baseline::files) +fn files() -> TestResult { + baseline::run("file", false, baseline::files).map_err(Into::into) } fn pathspecs(input: &[&str]) -> Vec<gix_pathspec::Pattern> { diff --git a/gix-prompt/tests/options/mod.rs b/gix-prompt/tests/options/mod.rs index 2482349d634..7a79fa82439 100644 --- a/gix-prompt/tests/options/mod.rs +++ b/gix-prompt/tests/options/mod.rs @@ -7,7 +7,7 @@ mod apply_environment { #[test] #[serial] - fn git_askpass_overrides_everything_and_ssh_askpass_does_not() -> gix_testtools::Result { + fn git_askpass_overrides_everything_and_ssh_askpass_does_not() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_ASKPASS", "override") .set("SSH_ASKPASS", "does not matter"); @@ -27,7 +27,7 @@ mod apply_environment { #[test] #[serial] - fn git_askpass_is_used_first_and_sets_unset_askpass_values() -> gix_testtools::Result { + fn git_askpass_is_used_first_and_sets_unset_askpass_values() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("GIT_ASKPASS", "from-env") .set("SSH_ASKPASS", "does not matter"); @@ -44,7 +44,7 @@ mod apply_environment { #[test] #[serial] - fn ssh_askpass_is_used_as_fallback() -> gix_testtools::Result { + fn ssh_askpass_is_used_as_fallback() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .unset("GIT_ASKPASS") .set("SSH_ASKPASS", "fallback"); @@ -64,7 +64,7 @@ mod apply_environment { #[test] #[serial] - fn ssh_askpass_does_not_override_current_value() -> gix_testtools::Result { + fn ssh_askpass_does_not_override_current_value() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .unset("GIT_ASKPASS") .set("SSH_ASKPASS", "fallback"); @@ -84,7 +84,7 @@ mod apply_environment { #[test] #[serial] - fn mode_is_left_untouched_if_terminal_prompt_is_trueish() -> gix_testtools::Result { + fn mode_is_left_untouched_if_terminal_prompt_is_trueish() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_TERMINAL_PROMPT", "true"); assert_eq!( @@ -101,7 +101,7 @@ mod apply_environment { #[test] #[serial] - fn mode_is_disabled_if_terminal_prompt_is_falseish() -> gix_testtools::Result { + fn mode_is_disabled_if_terminal_prompt_is_falseish() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_TERMINAL_PROMPT", "0"); assert_eq!( @@ -118,7 +118,7 @@ mod apply_environment { #[test] #[serial] - fn mode_is_unchanged_if_git_terminal_prompt_is_not_set() -> gix_testtools::Result { + fn mode_is_unchanged_if_git_terminal_prompt_is_not_set() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?.unset("GIT_TERMINAL_PROMPT"); assert_eq!( Options { diff --git a/gix-prompt/tests/prompt.rs b/gix-prompt/tests/prompt.rs index f93a40f6ec7..96517352111 100644 --- a/gix-prompt/tests/prompt.rs +++ b/gix-prompt/tests/prompt.rs @@ -71,7 +71,7 @@ mod ask { } #[test] - fn askpass_only() -> gix_testtools::Result { + fn askpass_only() -> gix_testtools::TestResult { let mut cmd = std::process::Command::new(env!("CARGO")); cmd.args([ "build", @@ -87,7 +87,7 @@ mod ask { "askpass", ]); assert!( - cmd.status().expect("Cargo can build prompt examples").success(), + cmd.status()?.success(), "prompt examples must build successfully before they run" ); @@ -100,7 +100,7 @@ mod ask { } #[test] - fn username_password() -> gix_testtools::Result { + fn username_password() -> gix_testtools::TestResult { let mut cmd = std::process::Command::new(env!("CARGO")); cmd.args([ "build", @@ -114,7 +114,7 @@ mod ask { "credentials", ]); assert!( - cmd.status().expect("Cargo can build prompt examples").success(), + cmd.status()?.success(), "prompt examples must build successfully before they run" ); diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 85907575064..9cbb8a0a914 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -202,7 +202,7 @@ pub(crate) mod function { #[cfg(feature = "blocking-client")] #[test] - fn invoke_preserves_transport_retryability() { + fn invoke_preserves_transport_retryability() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use std::io::{self, ErrorKind}; @@ -216,8 +216,7 @@ pub(crate) mod function { } } - let capabilities = super::Capabilities::from_lines("version 2\nls-refs\nagent=test\n".into()) - .expect("valid V2 capabilities"); + let capabilities = super::Capabilities::from_lines("version 2\nls-refs\nagent=test\n".into())?; for (kind, retryable) in [ (ErrorKind::BrokenPipe, true), (ErrorKind::ConnectionReset, true), @@ -268,6 +267,7 @@ pub(crate) mod function { 1: permission denied, ] "); + Ok(()) } #[test] @@ -304,23 +304,16 @@ pub(crate) mod function { } #[test] - fn from_refspecs_keeps_exact_refs_and_dwim_expansions() { + fn from_refspecs_keeps_exact_refs_and_dwim_expansions() -> gix_testtools::TestResult { let specs = [ - gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch) - .expect("valid") - .to_owned(), - gix_refspec::parse("dwim".into(), gix_refspec::parse::Operation::Fetch) - .expect("valid") - .to_owned(), + gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch)?.to_owned(), + gix_refspec::parse("dwim".into(), gix_refspec::parse::Operation::Fetch)?.to_owned(), gix_refspec::parse( "refs/tags/prefix*:refs/tags/prefix*".into(), gix_refspec::parse::Operation::Fetch, - ) - .expect("valid") + )? .to_owned(), - gix_refspec::parse("refs/heads/main".into(), gix_refspec::parse::Operation::Fetch) - .expect("valid") - .to_owned(), + gix_refspec::parse("refs/heads/main".into(), gix_refspec::parse::Operation::Fetch)?.to_owned(), ]; let prefixes = RefPrefixes::from_refspecs(&specs); @@ -342,6 +335,7 @@ pub(crate) mod function { .map(BString::from) .collect::<Vec<_>>() ); + Ok(()) } } } diff --git a/gix-protocol/tests/protocol/fetch/response.rs b/gix-protocol/tests/protocol/fetch/response.rs index 3b69efb08f5..e4f47771434 100644 --- a/gix-protocol/tests/protocol/fetch/response.rs +++ b/gix-protocol/tests/protocol/fetch/response.rs @@ -29,7 +29,7 @@ fn missing_required_features_are_unsupported() { mod v1 { mod from_line_reader { - use crate::Result; + use crate::TestResult; #[cfg(feature = "blocking-client")] use std::io::Read; @@ -46,7 +46,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn clone() -> Result { + async fn clone() -> TestResult { let mut provider = mock_reader("v1/clone-only.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V1, &mut reader, true, false).await?; @@ -61,7 +61,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn shallow_clone() -> Result { + async fn shallow_clone() -> TestResult { let mut provider = mock_reader("v1/clone-deepen-1.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V1, &mut reader, true, false).await?; @@ -80,7 +80,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn empty_shallow_clone_due_to_depth_being_too_high() -> Result { + async fn empty_shallow_clone_due_to_depth_being_too_high() -> TestResult { let mut provider = mock_reader("v1/clone-deepen-5.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V1, &mut reader, true, false).await?; @@ -96,7 +96,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn unshallow_fetch() -> Result { + async fn unshallow_fetch() -> TestResult { let mut provider = mock_reader("v1/fetch-unshallow.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V1, &mut reader, true, true).await?; @@ -129,7 +129,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn fetch_acks_without_pack() -> Result { + async fn fetch_acks_without_pack() -> TestResult { let mut provider = mock_reader("v1/fetch-no-pack.response"); let r = fetch::Response::from_line_reader(Protocol::V1, &mut provider.as_read_without_sidebands(), true, true) @@ -148,7 +148,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn fetch_acks_and_pack() -> Result { + async fn fetch_acks_and_pack() -> TestResult { let mut provider = mock_reader("v1/fetch.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V1, &mut reader, true, true).await?; @@ -171,7 +171,7 @@ mod v1 { } mod arguments { - use crate::Result; + use crate::TestResult; use bstr::ByteSlice; use gix_protocol::{Command, fetch}; use gix_transport::{Protocol, client::Capabilities}; @@ -181,7 +181,7 @@ mod v1 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn all() -> Result { + async fn all() -> TestResult { let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) ?; let mut args = fetch::Arguments::new( @@ -238,7 +238,7 @@ mod v1 { } mod v2 { mod from_line_reader { - use crate::Result; + use crate::TestResult; #[cfg(feature = "blocking-client")] use std::io::Read; @@ -259,7 +259,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn clone() -> Result { + async fn clone() -> TestResult { for keepalive in [false, true] { let fixture = format!( "v2/clone-only{}.response", @@ -285,7 +285,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn shallow_clone() -> Result { + async fn shallow_clone() -> TestResult { let mut provider = mock_reader("v2/clone-deepen-1.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V2, &mut reader, true, true).await?; @@ -304,7 +304,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn unshallow_fetch() -> Result { + async fn unshallow_fetch() -> TestResult { let mut provider = mock_reader("v2/fetch-unshallow.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V2, &mut reader, true, true).await?; @@ -338,7 +338,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn empty_shallow_clone() -> Result { + async fn empty_shallow_clone() -> TestResult { let mut provider = mock_reader("v2/clone-deepen-5.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V2, &mut reader, true, true).await?; @@ -354,7 +354,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn clone_with_sidebands() -> Result { + async fn clone_with_sidebands() -> TestResult { let mut provider = mock_reader("v2/clone-only-2.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V2, &mut reader, true, true).await?; @@ -379,7 +379,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn fetch_acks_without_pack() -> Result { + async fn fetch_acks_without_pack() -> TestResult { let mut provider = mock_reader("v2/fetch-no-pack.response"); let r = fetch::Response::from_line_reader(Protocol::V2, &mut provider.as_read_without_sidebands(), true, true) @@ -417,7 +417,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn fetch_acks_and_pack() -> Result { + async fn fetch_acks_and_pack() -> TestResult { let mut provider = mock_reader("v2/fetch.response"); let mut reader = provider.as_read_without_sidebands(); let r = fetch::Response::from_line_reader(Protocol::V2, &mut reader, true, true).await?; @@ -442,7 +442,7 @@ mod v2 { } mod arguments { - use crate::Result; + use crate::TestResult; use bstr::ByteSlice; use gix_protocol::{Command, fetch}; use gix_transport::{Protocol, client::Capabilities}; @@ -452,7 +452,7 @@ mod v2 { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] - async fn all() -> Result { + async fn all() -> TestResult { let (caps, _) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack filter side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) ?; let mut args = fetch::Arguments::new( diff --git a/gix-protocol/tests/protocol/fetch/v1.rs b/gix-protocol/tests/protocol/fetch/v1.rs index 583f15e900b..9709426a2ef 100644 --- a/gix-protocol/tests/protocol/fetch/v1.rs +++ b/gix-protocol/tests/protocol/fetch/v1.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteSlice; use gix_features::progress; use gix_protocol::handshake; @@ -9,7 +9,7 @@ use crate::fetch::{_impl::FetchConnection, CloneDelegate, LsRemoteDelegate, help #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn clone() -> Result { +async fn clone() -> TestResult { for with_keepalive in [false, true] { let out = Vec::new(); let mut dlg = CloneDelegate::default(); @@ -44,7 +44,7 @@ async fn clone() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn clone_empty_with_capabilities() -> Result { +async fn clone_empty_with_capabilities() -> TestResult { let out = Vec::new(); let mut dlg = CloneDelegate::default(); crate::fetch( @@ -69,7 +69,7 @@ async fn clone_empty_with_capabilities() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ls_remote() -> Result { +async fn ls_remote() -> TestResult { let out = Vec::new(); let mut delegate = LsRemoteDelegate::default(); let mut transport = transport( @@ -115,7 +115,7 @@ async fn ls_remote() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ls_remote_handshake_failure_due_to_downgrade() -> Result { +async fn ls_remote_handshake_failure_due_to_downgrade() -> TestResult { let out = Vec::new(); let delegate = LsRemoteDelegate::default(); @@ -133,7 +133,6 @@ async fn ls_remote_handshake_failure_due_to_downgrade() -> Result { "agent", false, ) - .await - .expect("V1 is OK for this transport"); + .await?; Ok(()) } diff --git a/gix-protocol/tests/protocol/fetch/v2.rs b/gix-protocol/tests/protocol/fetch/v2.rs index 181389a85e1..d774c8f6e43 100644 --- a/gix-protocol/tests/protocol/fetch/v2.rs +++ b/gix-protocol/tests/protocol/fetch/v2.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteSlice; use gix_features::progress; use gix_protocol::handshake; @@ -11,7 +11,7 @@ use crate::fetch::{ #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn clone_abort_prep() -> Result { +async fn clone_abort_prep() -> TestResult { let out = Vec::new(); let mut dlg = CloneDelegate { abort_with: Some(std::io::Error::other("hello world")), @@ -62,7 +62,7 @@ async fn clone_abort_prep() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ls_remote() -> Result { +async fn ls_remote() -> TestResult { let out = Vec::new(); let mut delegate = LsRemoteDelegate::default(); let mut transport = transport( @@ -119,7 +119,7 @@ async fn ls_remote() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ls_remote_abort_in_prep_ls_refs() -> Result { +async fn ls_remote_abort_in_prep_ls_refs() -> TestResult { let out = Vec::new(); let mut delegate = LsRemoteDelegate { abort_with: Some(std::io::Error::other("hello world")), @@ -159,7 +159,7 @@ async fn ls_remote_abort_in_prep_ls_refs() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ref_in_want() -> Result { +async fn ref_in_want() -> TestResult { let out = Vec::new(); let mut delegate = CloneRefInWantDelegate { want_refs: vec!["refs/heads/main".into()], @@ -218,7 +218,7 @@ async fn ref_in_want() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn ref_in_want_sha256() -> Result { +async fn ref_in_want_sha256() -> TestResult { let out = Vec::new(); let mut delegate = CloneRefInWantDelegate { want_refs: vec!["refs/heads/main".into()], diff --git a/gix-protocol/tests/protocol/handshake.rs b/gix-protocol/tests/protocol/handshake.rs index 33a80a40009..8b998bdd674 100644 --- a/gix-protocol/tests/protocol/handshake.rs +++ b/gix-protocol/tests/protocol/handshake.rs @@ -11,7 +11,7 @@ use gix_protocol::handshake::{Ref, refs}; #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn extract_references_from_v2_refs() { +async fn extract_references_from_v2_refs() -> gix_testtools::TestResult { let input = &mut Fixture( "808e50d724f604f69ab93c6da2919c014667bedb HEAD symref-target:refs/heads/main 808e50d724f604f69ab93c6da2919c014667bedb MISSING_NAMESPACE_TARGET symref-target:(null) @@ -25,7 +25,7 @@ unborn refs/heads/symbolic symref-target:refs/heads/target .as_bytes(), ); - let out = refs::from_v2_refs(input).await.expect("no failure on valid input"); + let out = refs::from_v2_refs(input).await?; assert_eq!( out, @@ -69,12 +69,13 @@ unborn refs/heads/symbolic symref-target:refs/heads/target }, ] ); + Ok(()) } #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn extract_references_from_v1_refs() { +async fn extract_references_from_v1_refs() -> gix_testtools::TestResult { let input = &mut Fixture( "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD 21c9b7500cb144b3169a6537961ec2b9e865be81 MISSING_NAMESPACE_TARGET @@ -86,13 +87,11 @@ dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/gix-commitgraph-v0.0.0 ); let (out, shallow) = refs::from_v1_refs_received_as_part_of_handshake_and_capabilities( input, - Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)") - .expect("valid capabilities") + Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)")? .0 .iter(), ) - .await - .expect("no failure from valid input"); + .await?; assert!(shallow.is_empty()); assert_eq!( out, @@ -122,12 +121,13 @@ dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/gix-commitgraph-v0.0.0 }, ] ); + Ok(()) } #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn extract_references_from_v1_refs_with_shallow() { +async fn extract_references_from_v1_refs_with_shallow() -> gix_testtools::TestResult { use gix_protocol::fetch::response::ShallowUpdate; let input = &mut Fixture( "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD @@ -142,13 +142,11 @@ shallow dce0ea858eef7ff61ad345cc5cdac62203fb3c10" ); let (out, shallow) = refs::from_v1_refs_received_as_part_of_handshake_and_capabilities( input, - Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)") - .expect("valid capabilities") + Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)")? .0 .iter(), ) - .await - .expect("no failure from valid input"); + .await?; assert_eq!( shallow, @@ -185,6 +183,7 @@ shallow dce0ea858eef7ff61ad345cc5cdac62203fb3c10" }, ] ); + Ok(()) } #[cfg(any(feature = "async-client", feature = "blocking-client"))] diff --git a/gix-protocol/tests/protocol/mod.rs b/gix-protocol/tests/protocol/mod.rs index 5bb5ba471cf..e76004d5029 100644 --- a/gix-protocol/tests/protocol/mod.rs +++ b/gix-protocol/tests/protocol/mod.rs @@ -1,4 +1,4 @@ -pub type Result = std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>; +pub use gix_testtools::TestResult; pub fn fixture_bytes(path: &str) -> Vec<u8> { std::fs::read(std::path::PathBuf::from("tests").join("fixtures").join(path)) diff --git a/gix-quote/tests/quote.rs b/gix-quote/tests/quote.rs index 0e95de01d9b..3515c16f7f3 100644 --- a/gix-quote/tests/quote.rs +++ b/gix-quote/tests/quote.rs @@ -104,20 +104,20 @@ mod ansi_c { } mod undo { + use std::borrow::Cow; + use bstr::ByteSlice; use gix_quote::ansi_c; macro_rules! test { ($name:ident, $input:literal, $expected:literal, $consumed:literal) => { #[test] - fn $name() { + fn $name() -> gix_error::TestResult { assert_eq!( - ansi_c::undo($input.as_bytes().as_bstr()).expect("valid input"), - ( - std::borrow::Cow::Borrowed($expected.as_bytes().as_bstr()), - $consumed - ) + ansi_c::undo($input.as_bytes().as_bstr())?, + (Cow::Borrowed($expected.as_bytes().as_bstr()), $consumed) ); + Ok(()) } }; } @@ -145,7 +145,7 @@ mod ansi_c { fn out_of_quote_characters_can_be_passed_and_will_not_be_consumed() { let input = br#""hello there" out of quote"#.as_bstr(); let (unquoted, consumed) = ansi_c::undo(input).expect("valid input"); - assert_eq!(unquoted, std::borrow::Cow::Borrowed(b"hello there".as_bstr())); + assert_eq!(unquoted, Cow::Borrowed(b"hello there".as_bstr())); assert_eq!(&input[consumed..], " out of quote"); } diff --git a/gix-ref/src/fullname.rs b/gix-ref/src/fullname.rs index 3d71dbb26fd..9ac76168058 100644 --- a/gix-ref/src/fullname.rs +++ b/gix-ref/src/fullname.rs @@ -84,7 +84,8 @@ impl FullNameRef { } /// Convert this name into the relative path identifying the reference location. - pub fn to_path(&self) -> &Path { + /// Return an error if the name cannot be represented as a native path. + pub fn to_path(&self) -> gix_error::Result<&Path> { gix_path::from_byte_slice(&self.0) } @@ -196,8 +197,8 @@ impl Category<'_> { } impl FullName { - /// Convert this name into the relative path, lossily, identifying the reference location relative to a repository - pub fn to_path(&self) -> &Path { + /// Convert this name into the relative path identifying its location, failing if its encoding is unrepresentable. + pub fn to_path(&self) -> gix_error::Result<&Path> { gix_path::from_byte_slice(&self.0) } diff --git a/gix-ref/src/name.rs b/gix-ref/src/name.rs index 15a7752d3a5..e0fedd33df7 100644 --- a/gix-ref/src/name.rs +++ b/gix-ref/src/name.rs @@ -96,7 +96,8 @@ impl PartialNameRef { impl PartialNameRef { /// Convert this name into the relative path possibly identifying the reference location. /// Note that it may be only a partial path though. - pub fn to_partial_path(&self) -> &Path { + /// Return an error if the name cannot be represented as a native path. + pub fn to_partial_path(&self) -> gix_error::Result<&Path> { gix_path::from_byte_slice(self.0.as_bstr()) } diff --git a/gix-ref/src/namespace.rs b/gix-ref/src/namespace.rs index 912fe190ddc..4b987205d81 100644 --- a/gix-ref/src/namespace.rs +++ b/gix-ref/src/namespace.rs @@ -15,7 +15,8 @@ impl Namespace { self.0.as_ref() } /// Return ourselves as a path for use within the filesystem. - pub fn to_path(&self) -> &Path { + /// Return an error if the namespace cannot be represented as a native path. + pub fn to_path(&self) -> gix_error::Result<&Path> { gix_path::from_byte_slice(&self.0) } /// Append the given `prefix` to this namespace so it becomes usable for prefixed iteration. diff --git a/gix-ref/src/store/file/find.rs b/gix-ref/src/store/file/find.rs index 15267b7ac8e..7e57781e1cd 100644 --- a/gix-ref/src/store/file/find.rs +++ b/gix-ref/src/store/file/find.rs @@ -91,15 +91,20 @@ impl file::Store { packed: Option<&packed::Buffer>, ) -> Result<Option<Reference>> { let mut buf = BString::default(); - let mut precomposed_partial_name_storage = packed.filter(|_| self.precompose_unicode).and_then(|_| { - use gix_object::bstr::ByteSlice; - let precomposed = partial_name.0.to_str().ok()?; - let precomposed = gix_utils::str::precompose_path(Path::new(precomposed).into()); - match precomposed { - Cow::Owned(precomposed) => Some(PartialName(gix_path::into_bstr(precomposed).into_owned())), - Cow::Borrowed(_) => None, - } - }); + let mut precomposed_partial_name_storage = packed + .filter(|_| self.precompose_unicode) + .and_then(|_| { + use gix_object::bstr::ByteSlice; + let precomposed = partial_name.0.to_str().ok()?; + let precomposed = gix_utils::str::precompose_path(Path::new(precomposed).into()); + match precomposed { + Cow::Owned(precomposed) => { + Some(gix_path::into_bstr(precomposed).map(|name| PartialName(name.into_owned()))) + } + Cow::Borrowed(_) => None, + } + }) + .transpose()?; let precomposed_partial_name = precomposed_partial_name_storage .as_ref() .map(std::convert::AsRef::as_ref); @@ -179,7 +184,7 @@ impl file::Store { Ok(content_buf) => content_buf, Err(err) if err.kind() == io::ErrorKind::NotADirectory => return Ok(None), Err(err) => { - return Err(err.and_raise(read_reference_error(self.reference_path(full_name)))); + return Err(err.and_raise(read_reference_error(self.reference_path(full_name)?))); } }; @@ -219,6 +224,7 @@ impl file::Store { } else { full_name }; + let relative_path = full_name.to_path()?; Ok(Some( loose::Reference::try_from_path(full_name.to_owned(), &content, self.object_hash) .map(Into::into) @@ -229,7 +235,7 @@ impl file::Store { r }) .or_raise(|| ReferenceDecode { - relative_path: full_name.to_path().to_owned(), + relative_path: relative_path.to_owned(), })?, )) } @@ -242,18 +248,20 @@ impl file::Store { &self, name: &'a FullNameRef, is_reflog: bool, - ) -> (Cow<'_, Path>, &'a FullNameRef) { + ) -> Result<(Cow<'_, Path>, &'a FullNameRef)> { let commondir = self.common_dir_resolved(); - let linked_git_dir = - |worktree_name: &BStr| commondir.join("worktrees").join(gix_path::from_bstr(worktree_name)); - name.category_and_short_name() - .map(|(c, sn)| { + let linked_git_dir = |worktree_name: &BStr| -> Result<PathBuf> { + Ok(commondir.join("worktrees").join(gix_path::from_bstr(worktree_name)?)) + }; + name.category_and_short_name().map_or_else( + || Ok((commondir.into(), name)), + |(c, sn)| { use crate::Category::*; let sn = FullNameRef::new_unchecked(sn); - match c { + Ok(match c { LinkedPseudoRef { name: worktree_name } => { if is_reflog { - (linked_git_dir(worktree_name).into(), sn) + (linked_git_dir(worktree_name)?.into(), sn) } else { (commondir.into(), name) } @@ -263,7 +271,7 @@ impl file::Store { LinkedRef { name: worktree_name } => { if sn.category().is_some_and(|cat| cat.is_worktree_private()) { if is_reflog { - (linked_git_dir(worktree_name).into(), sn) + (linked_git_dir(worktree_name)?.into(), sn) } else { (commondir.into(), name) } @@ -272,29 +280,29 @@ impl file::Store { } } PseudoRef | Bisect | Rewritten | WorktreePrivate => (self.git_dir.as_path().into(), name), - } - }) - .unwrap_or((commondir.into(), name)) + }) + }, + ) } /// Implements the logic required to transform a fully qualified refname into a filesystem path - pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> (Cow<'_, Path>, Cow<'b, Path>) { - let (base, name) = self.to_base_dir_and_relative_name(name, false); - ( + pub(crate) fn reference_path_with_base<'b>(&self, name: &'b FullNameRef) -> Result<(Cow<'_, Path>, Cow<'b, Path>)> { + let (base, name) = self.to_base_dir_and_relative_name(name, false)?; + Ok(( base, match &self.namespace { None => gix_path::to_native_path_on_windows(name.as_bstr()), Some(namespace) => { gix_path::to_native_path_on_windows(namespace.to_owned().into_namespaced_name(name).into_inner()) } - }, - ) + }?, + )) } /// Implements the logic required to transform a fully qualified refname into a filesystem path - pub(crate) fn reference_path(&self, name: &FullNameRef) -> PathBuf { - let (base, relative_path) = self.reference_path_with_base(name); - base.join(relative_path) + pub(crate) fn reference_path(&self, name: &FullNameRef) -> Result<PathBuf> { + let (base, relative_path) = self.reference_path_with_base(name)?; + Ok(base.join(relative_path)) } /// If `prohibit_windows_device_names` is set, check that `name` does not @@ -303,7 +311,9 @@ impl file::Store { if !self.prohibit_windows_device_names { return Ok(()); } - let (_, relative_path) = self.reference_path_with_base(name); + let (_, relative_path) = self + .reference_path_with_base(name) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; if relative_path .components() .filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok()) @@ -321,7 +331,9 @@ impl file::Store { /// Read the file contents with a verified full reference path and return it in the given vector if possible. pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result<Option<Vec<u8>>> { self.check_windows_device_name(name)?; - let (base, relative_path) = self.reference_path_with_base(name); + let (base, relative_path) = self + .reference_path_with_base(name) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let ref_path = base.join(&relative_path); match std::fs::File::open(&ref_path) { Ok(mut file) => { @@ -415,7 +427,7 @@ impl file::Store { match self.find_one_with_verified_input(path, packed) { Ok(Some(r)) => Ok(r), Ok(None) => Err(NotFound { - name: path.to_partial_path().to_owned(), + name: path.as_bstr().to_owned(), } .raise()), Err(err) => Err(err), @@ -433,7 +445,7 @@ pub(super) fn read_reference_error(path: impl Into<PathBuf>) -> Message { #[derive(Debug)] pub struct NotFound { /// The name whose lookup failed. It may have been discovered while following symbolic references. - pub name: PathBuf, + pub name: BString, } impl std::fmt::Display for NotFound { diff --git a/gix-ref/src/store/file/log/iter.rs b/gix-ref/src/store/file/log/iter.rs index 4f832e0765a..dd2b2116e09 100644 --- a/gix-ref/src/store/file/log/iter.rs +++ b/gix-ref/src/store/file/log/iter.rs @@ -51,13 +51,21 @@ impl Platform<'_, '_> { pub fn rev(&mut self) -> std::io::Result<Option<log::iter::Reverse<'_, std::fs::File>>> { self.buf.clear(); self.buf.resize(1024 * 4, 0); - self.store.reflog_iter_rev_inner(self.name, &mut self.buf) + let path = self + .store + .reflog_path(self.name) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + self.store.reflog_iter_rev_inner(&path, &mut self.buf) } /// Return a forward iterator over all log-lines, oldest to most recent. pub fn all(&mut self) -> std::io::Result<Option<log::iter::Forward<'_>>> { self.buf.clear(); - self.store.reflog_iter_inner(self.name, &mut self.buf) + let path = self + .store + .reflog_path(self.name) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + self.store.reflog_iter_inner(&path, &mut self.buf) } } diff --git a/gix-ref/src/store/file/loose/iter.rs b/gix-ref/src/store/file/loose/iter.rs index 6a3383c380a..7259dd7b520 100644 --- a/gix-ref/src/store/file/loose/iter.rs +++ b/gix-ref/src/store/file/loose/iter.rs @@ -51,7 +51,7 @@ impl Iterator for SortedLoosePaths { let full_name = full_path .strip_prefix(&self.base) .expect("prefix-stripping cannot fail as base is within our root"); - let Ok(full_name) = gix_path::try_into_bstr(full_name) + let Ok(full_name) = gix_path::into_bstr(full_name) .map(|name| gix_path::to_unix_separators_on_windows(name).into_owned()) else { continue; diff --git a/gix-ref/src/store/file/loose/reference/logiter.rs b/gix-ref/src/store/file/loose/reference/logiter.rs index c14a55a141c..0b762ec49a1 100644 --- a/gix-ref/src/store/file/loose/reference/logiter.rs +++ b/gix-ref/src/store/file/loose/reference/logiter.rs @@ -10,10 +10,8 @@ impl Reference { /// Please note that this method shouldn't be used to check if a log exists before trying to read it, but instead /// is meant to be the fastest possible way to determine if a log exists or not. /// If the caller needs to know if it's readable, try to read the log instead with a reverse or forward iterator. - pub fn log_exists(&self, store: &file::Store) -> bool { - store - .reflog_exists(self.name.as_ref()) - .expect("name conversion infallible") + pub fn log_exists(&self, store: &file::Store) -> Result<bool> { + store.reflog_exists(self.name.as_ref()) } /// Return a reflog reverse iterator for this ref, reading chunks from the back into the fixed buffer `buf`, in the given `store`. /// @@ -24,7 +22,10 @@ impl Reference { store: &file::Store, buf: &'b mut [u8], ) -> std::io::Result<Option<log::iter::Reverse<'b, std::fs::File>>> { - store.reflog_iter_rev_inner(self.name.as_ref(), buf) + let path = store + .reflog_path(self.name.as_ref()) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + store.reflog_iter_rev_inner(&path, buf) } /// Return a reflog forward iterator for this ref and write its file contents into `buf`, in the given `store`. @@ -36,6 +37,9 @@ impl Reference { store: &file::Store, buf: &'b mut Vec<u8>, ) -> std::io::Result<Option<impl Iterator<Item = Result<log::LineRef<'b>>> + 'a>> { - store.reflog_iter_inner(self.name.as_ref(), buf) + let path = store + .reflog_path(self.name.as_ref()) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + store.reflog_iter_inner(&path, buf) } } diff --git a/gix-ref/src/store/file/loose/reflog.rs b/gix-ref/src/store/file/loose/reflog.rs index 66639a09484..bd39fcea17a 100644 --- a/gix-ref/src/store/file/loose/reflog.rs +++ b/gix-ref/src/store/file/loose/reflog.rs @@ -1,7 +1,10 @@ use gix_error::Result; use gix_error::{Message, ResultExt, message}; -use std::{io::Read, path::PathBuf}; +use std::{ + io::Read, + path::{Path, PathBuf}, +}; use crate::{ FullNameRef, @@ -14,11 +17,15 @@ impl file::Store { /// Please note that this method shouldn't be used to check if a log exists before trying to read it, but instead /// is meant to be the fastest possible way to determine if a log exists or not. /// If the caller needs to know if it's readable, try to read the log instead with a reverse or forward iterator. - pub fn reflog_exists<'a, Name, E>(&self, name: Name) -> std::result::Result<bool, E> + pub fn reflog_exists<'a, Name, E>(&self, name: Name) -> Result<bool> where Name: TryInto<&'a FullNameRef, Error = E>, + std::result::Result<&'a FullNameRef, E>: ResultExt<Success = &'a FullNameRef>, { - Ok(self.reflog_path(name.try_into()?).is_file()) + let name = name + .try_into() + .or_raise(|| message("The reflog name or path is not a valid ref name"))?; + Ok(self.reflog_path(name)?.is_file()) } /// Return a reflog reverse iterator for the given fully qualified `name`, reading chunks from the back into the fixed buffer `buf`. @@ -39,20 +46,20 @@ impl file::Store { let name = name .try_into() .or_raise(|| message("The reflog name or path is not a valid ref name"))?; - self.reflog_iter_rev_inner(name, buf) - .or_raise(|| read_reflog_error(self.reflog_path(name))) + let path = self.reflog_path(name)?; + self.reflog_iter_rev_inner(&path, buf) + .or_raise(|| read_reflog_error(path)) } pub(crate) fn reflog_iter_rev_inner<'b>( &self, - name: &FullNameRef, + path: &Path, buf: &'b mut [u8], ) -> std::io::Result<Option<log::iter::Reverse<'b, std::fs::File>>> { - let path = self.reflog_path(name); if path.is_dir() { return Ok(None); } - match std::fs::File::open(&path) { + match std::fs::File::open(path) { Ok(file) => Ok(Some(log::iter::reverse(file, buf)?)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err), @@ -77,17 +84,16 @@ impl file::Store { let name = name .try_into() .or_raise(|| message("The reflog name or path is not a valid ref name"))?; - self.reflog_iter_inner(name, buf) - .or_raise(|| read_reflog_error(self.reflog_path(name))) + let path = self.reflog_path(name)?; + self.reflog_iter_inner(&path, buf).or_raise(|| read_reflog_error(path)) } pub(crate) fn reflog_iter_inner<'b>( &self, - name: &FullNameRef, + path: &Path, buf: &'b mut Vec<u8>, ) -> std::io::Result<Option<log::iter::Forward<'b>>> { - let path = self.reflog_path(name); - match std::fs::File::open(&path) { + match std::fs::File::open(path) { Ok(mut file) => { buf.clear(); if let Err(err) = file.read_to_end(buf) { @@ -105,9 +111,9 @@ impl file::Store { impl file::Store { /// Implements the logic required to transform a fully qualified refname into its log name - pub(crate) fn reflog_path(&self, name: &FullNameRef) -> PathBuf { - let (base, rela_path) = self.reflog_base_and_relative_path(name); - base.join(rela_path) + pub(crate) fn reflog_path(&self, name: &FullNameRef) -> Result<PathBuf> { + let (base, rela_path) = self.reflog_base_and_relative_path(name)?; + Ok(base.join(rela_path)) } } @@ -144,7 +150,7 @@ pub mod create_or_update { message: &BStr, mut force_create_reflog: bool, ) -> Result { - let (reflog_base, full_name) = self.reflog_base_and_relative_path(name); + let (reflog_base, full_name) = self.reflog_base_and_relative_path(name)?; match self.write_reflog { WriteReflog::Normal | WriteReflog::Always => { if self.write_reflog == WriteReflog::Always { @@ -210,18 +216,18 @@ pub mod create_or_update { pub(in crate::store_impl::file) fn reflog_base_and_relative_path<'a>( &self, name: &'a FullNameRef, - ) -> (PathBuf, Cow<'a, Path>) { + ) -> Result<(PathBuf, Cow<'a, Path>)> { let is_reflog = true; - let (base, name) = self.to_base_dir_and_relative_name(name, is_reflog); - ( + let (base, name) = self.to_base_dir_and_relative_name(name, is_reflog)?; + Ok(( base.join("logs"), match &self.namespace { None => gix_path::to_native_path_on_windows(name.as_bstr()), Some(namespace) => gix_path::to_native_path_on_windows( namespace.to_owned().into_namespaced_name(name).into_inner(), ), - }, - ) + }?, + )) } } diff --git a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs index e2ff3a5a588..85ee6f7eb31 100644 --- a/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs +++ b/gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs @@ -5,7 +5,7 @@ use gix_testtools::tempfile::TempDir; use super::*; -use gix_testtools::Result; +use gix_testtools::{Result, TestResult}; static SHA1_TO_SHA256_HASHES: std::sync::LazyLock<std::collections::HashMap<&str, &str>> = std::sync::LazyLock::new(|| { @@ -63,7 +63,7 @@ fn reflog_lines(store: &file::Store, name: &str, buf: &mut Vec<u8>) -> Result<Ve const WRITE_MODES: &[WriteReflog] = &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always]; #[test] -fn should_autocreate_is_unaffected_by_writemode() -> Result { +fn should_autocreate_is_unaffected_by_writemode() -> TestResult { let (_keep, store) = empty_store(WriteReflog::Disable)?; for should_create_name in &["HEAD", "refs/heads/main", "refs/remotes/any", "refs/notes/any"] { assert!(store.should_autocreate_reflog(Path::new(should_create_name))); @@ -75,7 +75,7 @@ fn should_autocreate_is_unaffected_by_writemode() -> Result { } #[test] -fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append_log_lines() -> Result { +fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append_log_lines() -> TestResult { for mode in WRITE_MODES { let (_keep, store) = empty_store(*mode)?; let full_name_str = "refs/heads/main"; @@ -141,7 +141,7 @@ fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append // create onto existing directory let full_name_str = "refs/heads/other"; let full_name: &FullNameRef = full_name_str.try_into()?; - let reflog_path = store.reflog_path(full_name_str.try_into().expect("valid")); + let reflog_path = store.reflog_path(full_name_str.try_into()?)?; let directory_in_place_of_reflog = reflog_path.join("empty-a").join("empty-b"); std::fs::create_dir_all(directory_in_place_of_reflog)?; @@ -184,10 +184,10 @@ fn missing_reflog_creates_it_even_if_similarly_named_empty_dir_exists_and_append } #[test] -fn non_empty_reflog_directory_preserves_open_error_context() -> Result { +fn non_empty_reflog_directory_preserves_open_error_context() -> TestResult { let (_keep, store) = empty_store(WriteReflog::Normal)?; let name: &FullNameRef = "refs/heads/main".try_into()?; - let path = store.reflog_path(name); + let path = store.reflog_path(name)?; std::fs::create_dir_all(&path)?; std::fs::write(path.join("keep"), b"not an empty directory")?; @@ -232,7 +232,7 @@ fn non_empty_reflog_directory_preserves_open_error_context() -> Result { } #[test] -fn reflog_write_normalizes_committer_name_and_email_like_git() -> Result { +fn reflog_write_normalizes_committer_name_and_email_like_git() -> TestResult { let (_keep, store) = empty_store(WriteReflog::Always)?; let full_name_str = "refs/heads/main"; let full_name: &FullNameRef = full_name_str.try_into()?; diff --git a/gix-ref/src/store/file/overlay_iter.rs b/gix-ref/src/store/file/overlay_iter.rs index 39f631d42ea..6aa86565a49 100644 --- a/gix-ref/src/store/file/overlay_iter.rs +++ b/gix-ref/src/store/file/overlay_iter.rs @@ -251,14 +251,15 @@ pub(crate) enum IterInfo<'a> { } impl<'a> IterInfo<'a> { - fn prefix(&self) -> Option<Cow<'_, BStr>> { + fn prefix(&self) -> Result<Option<Cow<'_, BStr>>> { match self { IterInfo::Base { .. } => None, IterInfo::PrefixAndBase { prefix, .. } => Some(gix_path::into_bstr(*prefix)), IterInfo::BaseAndIterRoot { prefix, .. } => Some(gix_path::into_bstr(prefix.clone())), - IterInfo::ComputedIterationRoot { prefix, .. } => Some(prefix.clone()), + IterInfo::ComputedIterationRoot { prefix, .. } => Some(Ok(prefix.clone())), IterInfo::Pseudo { .. } => None, } + .transpose() } fn into_iter(self) -> Peekable<SortedLoosePaths> { @@ -299,7 +300,8 @@ impl<'a> IterInfo<'a> { } fn from_prefix(base: &'a Path, prefix: &'a RelativePath, precompose_unicode: bool) -> std::io::Result<Self> { - let prefix_path = gix_path::from_bstr(prefix.as_ref().as_bstr()); + let prefix_path = gix_path::from_bstr(prefix.as_ref().as_bstr()) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; let iter_root = base.join(&prefix_path); if prefix.as_ref().ends_with(b"/") { Ok(IterInfo::BaseAndIterRoot { @@ -332,19 +334,24 @@ impl file::Store { packed: Option<&'p packed::Buffer>, ) -> std::io::Result<LooseThenPacked<'p, 's>> { match self.namespace.as_ref() { - Some(namespace) => self.iter_from_info( - IterInfo::PrefixAndBase { - base: self.git_dir(), - prefix: namespace.to_path(), - precompose_unicode: self.precompose_unicode, - }, - self.common_dir().map(|base| IterInfo::PrefixAndBase { - base, - prefix: namespace.to_path(), - precompose_unicode: self.precompose_unicode, - }), - packed, - ), + Some(namespace) => { + let prefix = namespace + .to_path() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; + self.iter_from_info( + IterInfo::PrefixAndBase { + base: self.git_dir(), + prefix, + precompose_unicode: self.precompose_unicode, + }, + self.common_dir().map(|base| IterInfo::PrefixAndBase { + base, + prefix, + precompose_unicode: self.precompose_unicode, + }), + packed, + ) + } None => self.iter_from_info( IterInfo::Base { base: self.git_dir(), @@ -420,7 +427,10 @@ impl file::Store { object_hash: self.object_hash, iter_packed: match packed { Some(packed) => Some( - match git_dir_info.prefix() { + match git_dir_info + .prefix() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))? + { Some(prefix) => packed.iter_prefixed(prefix.into_owned()), None => packed.iter(), } diff --git a/gix-ref/src/store/file/raw_ext.rs b/gix-ref/src/store/file/raw_ext.rs index e4f2539690a..f499d4aa909 100644 --- a/gix-ref/src/store/file/raw_ext.rs +++ b/gix-ref/src/store/file/raw_ext.rs @@ -19,7 +19,7 @@ pub trait ReferenceExt: Sealed { fn log_iter<'a, 's>(&'a self, store: &'s file::Store) -> log::iter::Platform<'a, 's>; /// For details, see [`Reference::log_exists()`]. - fn log_exists(&self, store: &file::Store) -> bool; + fn log_exists(&self, store: &file::Store) -> Result<bool>; /// Follow all symbolic targets this reference might point to and peel the underlying object /// to the end of the tag-chain, returning the first non-tag object the annotated tag points to, @@ -99,10 +99,8 @@ impl ReferenceExt for Reference { } } - fn log_exists(&self, store: &file::Store) -> bool { - store - .reflog_exists(self.name.as_ref()) - .expect("infallible name conversion") + fn log_exists(&self, store: &file::Store) -> Result<bool> { + store.reflog_exists(self.name.as_ref()) } fn peel_to_id_in_place(&mut self, store: &file::Store, objects: &dyn gix_object::Find) -> Result<ObjectId> { @@ -196,7 +194,7 @@ impl ReferenceExt for Reference { if seen.contains(&next.name) { bail!( corruption("Aborting symbolic reference cycle") - .with("path", store.reference_path(cursor.name.as_ref())) + .with("path", store.reference_path(cursor.name.as_ref())?) ); } *cursor = next; @@ -226,7 +224,7 @@ impl ReferenceExt for Reference { Target::Symbolic(full_name) => match store.try_find_packed(full_name.as_ref(), packed) { Ok(Some(next)) => Some(Ok(next)), Ok(None) => Some(Err(file::find::NotFound { - name: full_name.to_path().to_owned(), + name: full_name.as_bstr().to_owned(), } .raise())), Err(err) => Some(Err(err)), diff --git a/gix-ref/src/store/file/transaction/commit.rs b/gix-ref/src/store/file/transaction/commit.rs index 4aa122c4d90..8707da4067a 100644 --- a/gix-ref/src/store/file/transaction/commit.rs +++ b/gix-ref/src/store/file/transaction/commit.rs @@ -125,7 +125,7 @@ impl Transaction<'_, '_> { } for change in &mut updates { - let (reflog_root, relative_name) = self.store.reflog_base_and_relative_path(change.update.name.as_ref()); + let (reflog_root, relative_name) = self.store.reflog_base_and_relative_path(change.update.name.as_ref())?; match &change.update.change { Change::Update { .. } => {} Change::Delete { .. } => { @@ -168,7 +168,7 @@ impl Transaction<'_, '_> { }; if take_lock_and_delete { let lock = change.lock.take(); - let reference_path = self.store.reference_path(change.update.name.as_ref()); + let reference_path = self.store.reference_path(change.update.name.as_ref())?; if let Err(err) = std::fs::remove_file(reference_path) && err.kind() != std::io::ErrorKind::NotFound { diff --git a/gix-ref/src/store/file/transaction/prepare.rs b/gix-ref/src/store/file/transaction/prepare.rs index 27efdea57a2..87d697c9345 100644 --- a/gix-ref/src/store/file/transaction/prepare.rs +++ b/gix-ref/src/store/file/transaction/prepare.rs @@ -58,7 +58,7 @@ impl Transaction<'_, '_> { let lock = match &mut change.update.change { Change::Delete { expected, .. } => { - let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref()); + let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref())?; let lock = gix_lock::Marker::acquire_to_hold_resource( base.join(relative_path.as_ref()), lock_fail_mode, @@ -101,7 +101,7 @@ impl Transaction<'_, '_> { Some(lock) } Change::Update { expected, new, .. } => { - let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref()); + let (base, relative_path) = store.reference_path_with_base(change.update.name.as_ref())?; let obtain_lock = || { gix_lock::File::acquire_to_update_resource( base.join(relative_path.as_ref()), diff --git a/gix-ref/src/store/packed/decode/tests.rs b/gix-ref/src/store/packed/decode/tests.rs index b3d9494772c..19aa44530c0 100644 --- a/gix-ref/src/store/packed/decode/tests.rs +++ b/gix-ref/src/store/packed/decode/tests.rs @@ -1,7 +1,4 @@ -type Result = std::result::Result<(), Box<dyn std::error::Error>>; - mod reference { - use super::Result; use crate::{ FullNameRef, store_impl::{packed, packed::decode}, @@ -29,7 +26,7 @@ mod reference { } #[test] - fn uppercase_hex() -> Result { + fn uppercase_hex() -> gix_testtools::TestResult { let mut input: &[u8] = b"D53C4B0F91F1B29769C9430F2D1C0BCAB1170C75 refs/heads/uppercase ^E9CDC958E7CE2290E2D7958CDB5AA9323EF35D37\n"; let parsed = decode::reference(&mut input, HASH_KIND).unwrap(); @@ -42,7 +39,7 @@ mod reference { } #[test] - fn sha256_hex() -> Result { + fn sha256_hex() -> gix_testtools::TestResult { let mut input: &[u8] = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\n"; let parsed = decode::reference(&mut input, gix_hash::Kind::Sha256).unwrap(); @@ -53,7 +50,7 @@ mod reference { } #[test] - fn two_refs_in_a_row() -> Result { + fn two_refs_in_a_row() -> gix_testtools::TestResult { let mut input: &[u8] = b"d53c4b0f91f1b29769c9430f2d1c0bcab1170c75 refs/heads/alternates-after-packs-and-loose ^e9cdc958e7ce2290e2d7958cdb5aa9323ef35d37\neaae9c1bc723209d793eb93f5587fa2604d5cd92 refs/heads/avoid-double-lookup\n"; let parsed = decode::reference(&mut input, HASH_KIND).unwrap(); @@ -256,7 +253,6 @@ mod name_at_record_start { mod header { use gix_object::bstr::ByteSlice; - use super::Result; use crate::store_impl::packed::{ decode, decode::{Header, Peeled}, @@ -274,7 +270,7 @@ mod header { } #[test] - fn valid_fully_peeled_stored() -> Result { + fn valid_fully_peeled_stored() -> gix_testtools::TestResult { let mut input: &[u8] = b"# pack-refs with: peeled fully-peeled sorted \nsomething else"; let header = decode::header(&mut input).expect("valid input"); @@ -290,7 +286,7 @@ mod header { } #[test] - fn valid_peeled_unsorted() -> Result { + fn valid_peeled_unsorted() -> gix_testtools::TestResult { let mut input: &[u8] = b"# pack-refs with: peeled\n"; let header = decode::header(&mut input).unwrap(); @@ -306,7 +302,7 @@ mod header { } #[test] - fn valid_empty() -> Result { + fn valid_empty() -> gix_testtools::TestResult { let mut input: &[u8] = b"# pack-refs with: \n"; let header = decode::header(&mut input).unwrap(); diff --git a/gix-ref/src/store/packed/find.rs b/gix-ref/src/store/packed/find.rs index d1aad63ff07..c7ffbb7fcf4 100644 --- a/gix-ref/src/store/packed/find.rs +++ b/gix-ref/src/store/packed/find.rs @@ -70,7 +70,7 @@ impl packed::Buffer { .or_raise(|| message("The ref name or path is not a valid ref name"))?; self.try_find::<_, std::convert::Infallible>(name)? .ok_or_raise(|| crate::file::find::NotFound { - name: name.to_partial_path().to_owned(), + name: name.as_bstr().to_owned(), }) } diff --git a/gix-ref/src/store/packed/transaction.rs b/gix-ref/src/store/packed/transaction.rs index c01c2dbac6f..7957f40f3ba 100644 --- a/gix-ref/src/store/packed/transaction.rs +++ b/gix-ref/src/store/packed/transaction.rs @@ -57,46 +57,34 @@ impl packed::Transaction { assert!(self.edits.is_none(), "BUG: cannot call prepare(…) more than once"); let buffer = &self.buffer; // Remove all edits which are deletions that aren't here in the first place - let mut edits: Vec<Edit> = edits - .into_iter() - .map(|mut edit| { - use gix_object::bstr::ByteSlice; - if self.precompose_unicode { - let precomposed = edit - .name - .0 - .to_str() - .ok() - .map(|name| gix_utils::str::precompose_path(Path::new(name).into())); - match precomposed { - None | Some(Cow::Borrowed(_)) => edit, - Some(Cow::Owned(precomposed)) => { - edit.name.0 = gix_path::into_bstr(precomposed).into_owned(); - edit - } - } - } else { - edit - } - }) - .map(|mut edit| { - if let Some(namespace) = &self.namespace { - edit.name = namespace.clone().into_namespaced_name(edit.name.as_ref()); + let mut prepared_edits = Vec::new(); + for mut edit in edits { + use gix_object::bstr::ByteSlice; + if self.precompose_unicode { + let precomposed = edit + .name + .0 + .to_str() + .ok() + .map(|name| gix_utils::str::precompose_path(Path::new(name).into())); + if let Some(Cow::Owned(precomposed)) = precomposed { + edit.name.0 = gix_path::into_bstr(precomposed)?.into_owned(); } - edit - }) - .filter(|edit| { - if let Change::Delete { .. } = edit.change { - buffer.as_ref().is_none_or(|b| b.find(edit.name.as_ref()).is_ok()) - } else { - true - } - }) - .map(|change| Edit { - inner: change, + } + if let Some(namespace) = &self.namespace { + edit.name = namespace.clone().into_namespaced_name(edit.name.as_ref()); + } + if matches!(edit.change, Change::Delete { .. }) + && buffer.as_ref().is_some_and(|b| b.find(edit.name.as_ref()).is_err()) + { + continue; + } + prepared_edits.push(Edit { + inner: edit, peeled: None, - }) - .collect(); + }); + } + let mut edits = prepared_edits; let mut buf = Vec::new(); for edit in &mut edits { diff --git a/gix-ref/tests/refs/equality.rs b/gix-ref/tests/refs/equality.rs index 9106d8ffb5b..871f947e248 100644 --- a/gix-ref/tests/refs/equality.rs +++ b/gix-ref/tests/refs/equality.rs @@ -82,7 +82,7 @@ macro_rules! assert_reference_name_equality { } #[test] -fn name_types_compare_with_text_and_byte_strings() -> gix_testtools::Result { +fn name_types_compare_with_text_and_byte_strings() -> gix_testtools::TestResult { let full = FullName::try_from("refs/heads/main")?; let full_ref: &FullNameRef = full.as_ref(); assert_eq!(full, full_ref, "owned and borrowed full names match"); @@ -103,7 +103,7 @@ fn name_types_compare_with_text_and_byte_strings() -> gix_testtools::Result { } #[test] -fn names_compare_as_exact_bytes() -> gix_testtools::Result { +fn names_compare_as_exact_bytes() -> gix_testtools::TestResult { let full_bytes = b"refs/heads/\xff".as_bstr(); let full = FullName::try_from(full_bytes)?; let full_ref: &FullNameRef = full.as_ref(); @@ -126,7 +126,7 @@ fn names_compare_as_exact_bytes() -> gix_testtools::Result { } #[test] -fn references_compare_by_name_without_changing_structural_equality() -> gix_testtools::Result { +fn references_compare_by_name_without_changing_structural_equality() -> gix_testtools::TestResult { let name = FullName::try_from("refs/heads/main")?; let raw = gix_ref::Reference { name: name.clone(), diff --git a/gix-ref/tests/refs/error.rs b/gix-ref/tests/refs/error.rs index 3a16b8a6123..c244b928873 100644 --- a/gix-ref/tests/refs/error.rs +++ b/gix-ref/tests/refs/error.rs @@ -3,7 +3,7 @@ use gix_error::{Class, Error, ErrorExt, Message, MetadataValue}; use gix_ref::{file::ReferenceExt, packed, transaction::PreviousValue}; #[test] -fn missing_references_retain_their_name_and_classification() -> Result { +fn missing_references_retain_their_name_and_classification() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let store = crate::file::store_with_packed_refs()?; let packed = store.open_packed_buffer()?.expect("the fixture has packed refs"); @@ -28,9 +28,8 @@ fn missing_references_retain_their_name_and_classification() -> Result { assert_eq!( err.downcast_any_ref::<gix_ref::file::find::NotFound>() .expect("the concrete reference lookup error is retained") - .name - .as_os_str(), - missing, + .name, + "missing", "the lookup failure retains the missing reference name" ); } @@ -46,7 +45,7 @@ fn missing_references_retain_their_name_and_classification() -> Result { } #[test] -fn peeling_missing_targets_is_classified() -> Result { +fn peeling_missing_targets_is_classified() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use gix_lock::acquire::Fail; use gix_ref::{file::transaction::PackedRefs, transaction::RefEdit}; @@ -130,7 +129,7 @@ fn peeling_missing_targets_is_classified() -> Result { } #[test] -fn peeling_missing_objects_has_one_classified_diagnostic() -> Result { +fn peeling_missing_objects_has_one_classified_diagnostic() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); for err in peeling_errors(gix_object::find::Never)? { error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); @@ -168,7 +167,7 @@ fn peeling_missing_objects_has_one_classified_diagnostic() -> Result { } #[test] -fn object_lookup_failures_retain_their_causes() -> Result { +fn object_lookup_failures_retain_their_causes() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); struct UnavailableObjects(std::io::ErrorKind); impl gix_object::Find for UnavailableObjects { @@ -279,7 +278,7 @@ fn peeling_errors(objects: impl gix_object::Find) -> Result<[gix_error::Error; 2 } #[test] -fn malformed_tags_are_corruption_instead_of_missing_objects() -> Result { +fn malformed_tags_are_corruption_instead_of_missing_objects() -> gix_testtools::TestResult { struct MalformedTag; impl gix_object::Find for MalformedTag { fn try_find<'a>( @@ -316,7 +315,7 @@ fn malformed_tags_are_corruption_instead_of_missing_objects() -> Result { } #[test] -fn malformed_reference_data_is_classified() -> Result { +fn malformed_reference_data_is_classified() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let store = crate::file::store_at("make_ref_repository.sh")?; let hash = crate::fixture_hash_kind(); @@ -410,7 +409,7 @@ fn malformed_reference_data_is_classified() -> Result { } #[test] -fn missing_transaction_targets_are_classified() -> Result { +fn missing_transaction_targets_are_classified() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use gix_lock::acquire::Fail; use gix_ref::transaction::RefEdit; @@ -448,7 +447,7 @@ fn missing_transaction_targets_are_classified() -> Result { } #[test] -fn invalid_reflog_input_is_classified() -> Result { +fn invalid_reflog_input_is_classified() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); use crate::file::transaction::prepare_and_commit::{committer, create_at, empty_store}; use gix_lock::acquire::Fail; @@ -505,7 +504,7 @@ fn invalid_reflog_input_is_classified() -> Result { } #[test] -fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> Result { +fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> gix_testtools::TestResult { let hash = crate::fixture_hash_kind(); let packed = packed::Buffer::from_bytes( format!("# pack-refs with: sorted\n{} refs/heads/bad..name\n", hash.null()).as_bytes(), @@ -547,7 +546,7 @@ fn malformed_packed_names_and_reflog_signatures_retain_parser_errors() -> Result } #[test] -fn custom_name_conversion_errors_keep_their_sources() -> Result { +fn custom_name_conversion_errors_keep_their_sources() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); struct Name<E>(E); impl<E> TryInto<&'static gix_ref::PartialNameRef> for Name<E> { @@ -610,7 +609,7 @@ fn custom_name_conversion_errors_keep_their_sources() -> Result { } #[test] -fn a_depth_limit_does_not_imply_corruption() -> Result { +fn a_depth_limit_does_not_imply_corruption() -> gix_testtools::TestResult { let (_keep, store) = crate::file::transaction::prepare_and_commit::empty_store()?; let refs = store.git_dir().join("refs/heads"); std::fs::create_dir_all(&refs)?; @@ -650,7 +649,7 @@ fn a_depth_limit_does_not_imply_corruption() -> Result { } #[test] -fn loose_reference_diagnostics_keep_input_with_the_failure() -> Result { +fn loose_reference_diagnostics_keep_input_with_the_failure() -> gix_testtools::TestResult { let hash = crate::fixture_hash_kind(); let contents = b"invalid\xff"; let err = gix_ref::file::loose::Reference::try_from_path("HEAD".try_into()?, contents, hash) diff --git a/gix-ref/tests/refs/file/log.rs b/gix-ref/tests/refs/file/log.rs index 327b99b939d..192387a214f 100644 --- a/gix-ref/tests/refs/file/log.rs +++ b/gix-ref/tests/refs/file/log.rs @@ -1,11 +1,11 @@ mod line { mod write_to { - use crate::Result; use gix_object::bstr::ByteVec; use gix_ref::file::log; #[test] - fn newlines_in_message_of_the_input_fails_and_we_trust_signature_writing_validation() -> Result { + fn newlines_in_message_of_the_input_fails_and_we_trust_signature_writing_validation() + -> gix_testtools::TestResult { let line = "0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c1"; let mut line = log::LineRef::from_bytes(line.as_bytes())?.to_owned(); line.message.push_str("and here come\nthe newline"); @@ -22,7 +22,7 @@ mod line { } #[test] - fn round_trips() -> Result { + fn round_trips() -> gix_testtools::TestResult { let lines = &[ "0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c1\n", "0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 \n", @@ -39,11 +39,10 @@ mod line { } mod parse { - use crate::Result; use gix_ref::file::log; #[test] - fn angle_bracket_in_comment() -> Result { + fn angle_bracket_in_comment() -> gix_testtools::TestResult { let line = log::LineRef::from_bytes(b"7b114132d03c468a9cd97836901553658c9792de 306cdbab5457c323d1201aa8a59b3639f600a758 First Last <first.last@example.com> 1727013187 +0200\trebase (pick): Replace Into<Range<u32>> by From<LineRange>")?; assert_eq!(line.signature.name, "First Last"); assert_eq!(line.signature.email, "first.last@example.com"); @@ -85,10 +84,9 @@ mod iter { } mod with_buffer_too_small_for_single_line { - use crate::Result; #[test] - fn single_line() -> Result { + fn single_line() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let mut buf = [0u8; 128]; let two_lines: Vec<u8> = b"0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c1".to_vec(); @@ -128,13 +126,12 @@ mod iter { } mod with_buffer_big_enough_for_largest_line { - use crate::Result; use gix_ref::log::Line; use crate::file::log::iter::reflog; #[test] - fn single_line() -> Result { + fn single_line() -> gix_testtools::TestResult { let mut buf = [0u8; 1024]; let two_lines: Vec<u8> = b"0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c1".to_vec(); let two_lines_trailing_nl = { @@ -160,7 +157,7 @@ mod iter { } #[test] - fn two_lines() -> Result { + fn two_lines() -> gix_testtools::TestResult { let two_lines: Vec<u8> = b"1000000000000000000000000000000000000000 234385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c2\n0000000000000000000000000000000000000000 134385f6d781b7e97062102c6a483440bfda2a03 committer <committer@example.com> 946771200 +0000 commit (initial): c1".to_vec(); let two_lines_trailing_nl = { let mut l = two_lines.clone(); @@ -198,7 +195,7 @@ mod iter { } #[test] - fn realistic_logs_can_be_read_completely() -> Result { + fn realistic_logs_can_be_read_completely() -> gix_testtools::TestResult { let log = reflog("refs/heads/old")?; let mut buf = Vec::with_capacity(16 * 1024); for size in [2048, 3000, 4096, 8192, 16384] { @@ -217,13 +214,12 @@ mod iter { } } mod forward { - use crate::Result; use gix_object::bstr::B; use crate::{file::log::iter::reflog, hex_to_id}; #[test] - fn all_success() -> Result { + fn all_success() -> gix_testtools::TestResult { let log = reflog("HEAD")?; let iter = gix_ref::file::log::iter::forward(&log); assert_eq!(iter.count(), 5, "the log as a known amount of entries"); diff --git a/gix-ref/tests/refs/file/reference.rs b/gix-ref/tests/refs/file/reference.rs index 1f1afdd0905..2f77d781159 100644 --- a/gix-ref/tests/refs/file/reference.rs +++ b/gix-ref/tests/refs/file/reference.rs @@ -1,21 +1,20 @@ mod reflog { mod packed { - use crate::Result; use gix_ref::file::ReferenceExt; use crate::file; #[test] - fn iter() -> Result { + fn iter() -> gix_testtools::TestResult { let store = file::store_with_packed_refs()?; let r = store.find("main")?; assert_eq!(r.log_iter(&store).all()?.expect("log exists").count(), 1); - assert!(r.log_exists(&store), "it exists if its readable"); + assert!(r.log_exists(&store)?, "it exists if its readable"); Ok(()) } #[test] - fn iter_rev() -> Result { + fn iter_rev() -> gix_testtools::TestResult { let store = file::store_with_packed_refs()?; let r = store.find("main")?; assert_eq!(r.log_iter(&store).rev()?.expect("log exists").count(), 1); @@ -24,21 +23,20 @@ mod reflog { } mod loose { - use crate::Result; use crate::file; #[test] - fn iter() -> Result { + fn iter() -> gix_testtools::TestResult { let store = file::store()?; let r = store.find_loose("HEAD")?; let mut buf = Vec::new(); assert_eq!(r.log_iter(&store, &mut buf)?.expect("log exists").count(), 1); - assert!(r.log_exists(&store), "it exists if its readable"); + assert!(r.log_exists(&store)?, "it exists if its readable"); Ok(()) } #[test] - fn iter_rev() -> Result { + fn iter_rev() -> gix_testtools::TestResult { let store = file::store()?; let r = store.find_loose("HEAD")?; let mut buf = [0u8; 256]; @@ -49,7 +47,6 @@ mod reflog { } mod peel { - use crate::Result; use gix_error::Message; use gix_object::FindExt; use gix_ref::{Reference, file::ReferenceExt}; @@ -61,12 +58,12 @@ mod peel { }; #[test] - fn one_level() -> Result { + fn one_level() -> gix_testtools::TestResult { let store = file::store()?; let r = store.find_loose("HEAD")?; assert_eq!(r.kind(), gix_ref::Kind::Symbolic, "there is something to peel"); - let nr = Reference::from(r).follow(&store).expect("exists").expect("no failure"); + let nr = Reference::from(r).follow(&store).expect("exists")?; assert!( matches!(nr.target.to_ref(), gix_ref::TargetRef::Object(_)), "iteration peels a single level" @@ -81,7 +78,7 @@ mod peel { } #[test] - fn peel_with_packed_involvement() -> Result { + fn peel_with_packed_involvement() -> gix_testtools::TestResult { let store = store_with_packed_refs()?; let mut head: Reference = store.find_loose("HEAD")?.into(); let expected = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); @@ -95,7 +92,7 @@ mod peel { } #[test] - fn peel_one_level_with_pack() -> Result { + fn peel_one_level_with_pack() -> gix_testtools::TestResult { let store = store_with_packed_refs()?; let mut head = store.find("dt1")?; @@ -133,7 +130,7 @@ mod peel { } #[test] - fn to_id_multi_hop() -> Result { + fn to_id_multi_hop() -> gix_testtools::TestResult { let store = file::store()?; let mut r: Reference = store.find_loose("multi-link")?.into(); assert_eq!(r.kind(), gix_ref::Kind::Symbolic, "there is something to peel"); @@ -157,7 +154,7 @@ mod peel { } #[test] - fn to_id_long_jump() -> Result { + fn to_id_long_jump() -> gix_testtools::TestResult { for packed in [None, Some("packed")] { let store = file::store_at_with_args("make_multi_hop_ref.sh", packed)?; let odb = crate::file::odb_at(store.git_dir().join("objects"))?; @@ -189,7 +186,7 @@ mod peel { } #[test] - fn to_id_cycle() -> Result { + fn to_id_cycle() -> gix_testtools::TestResult { let store = file::store()?; let mut r: Reference = store.find_loose("loop-a")?.into(); assert_eq!(r.kind(), gix_ref::Kind::Symbolic, "there is something to peel"); diff --git a/gix-ref/tests/refs/file/store/access.rs b/gix-ref/tests/refs/file/store/access.rs index 529e88fbc79..b9ffa5f1f9c 100644 --- a/gix-ref/tests/refs/file/store/access.rs +++ b/gix-ref/tests/refs/file/store/access.rs @@ -1,8 +1,7 @@ -use crate::Result; use crate::file::{named_store_at, store}; #[test] -fn set_packed_buffer_mmap_threshold() -> Result { +fn set_packed_buffer_mmap_threshold() -> gix_testtools::TestResult { let mut store = store()?; let prev = store.set_packed_buffer_mmap_threshold(0); if cfg!(windows) { @@ -23,7 +22,7 @@ fn set_packed_buffer_mmap_threshold() -> Result { } #[test] -fn is_pristine() -> Result { +fn is_pristine() -> gix_testtools::TestResult { let store = named_store_at("make_pristine.sh", "untouched")?; assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(true)); assert_eq!(store.is_pristine("refs/heads/other".try_into()?), Some(false)); diff --git a/gix-ref/tests/refs/file/store/find.rs b/gix-ref/tests/refs/file/store/find.rs index 189a5e62f17..50b815eae35 100644 --- a/gix-ref/tests/refs/file/store/find.rs +++ b/gix-ref/tests/refs/file/store/find.rs @@ -1,9 +1,8 @@ mod existing { - use crate::Result; use crate::{file::store_at, hex_to_id}; #[test] - fn various_repositories() -> Result { + fn various_repositories() -> gix_testtools::TestResult { for fixture in [ "make_ref_repository.sh", "make_packed_ref_repository.sh", @@ -24,12 +23,11 @@ mod existing { } mod convert { - use crate::Result; use gix_ref::{PartialName, PartialNameRef}; // TODO: figure this out #[test] - fn possible_inputs() -> Result { + fn possible_inputs() -> gix_testtools::TestResult { let store = crate::file::store()?; store.find_loose("dt1")?; store.find_loose(&String::from("dt1"))?; // Owned Strings don't have an impl for PartialName @@ -104,17 +102,14 @@ mod existing { } mod loose { - use crate::Result; use crate::{file::store, hex_to_id}; mod existing { - use crate::Result; - use std::path::Path; use crate::file::store; #[test] - fn capitalized_branch() -> Result { + fn capitalized_branch() -> gix_testtools::TestResult { let store = store()?; assert_eq!( store.find("A")?, @@ -125,7 +120,7 @@ mod loose { } #[test] - fn success_and_failure() -> Result { + fn success_and_failure() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let store = store()?; for (partial_name, expected_path) in [("main", Some("refs/heads/main")), ("does-not-exist", None)] { @@ -140,7 +135,7 @@ mod loose { err.downcast_any_ref::<gix_ref::file::find::NotFound>() .expect("absent reference") .name, - Path::new(partial_name) + partial_name ); } }, @@ -156,7 +151,7 @@ mod loose { } #[test] - fn fetch_head_can_be_parsed() -> Result { + fn fetch_head_can_be_parsed() -> gix_testtools::TestResult { let store = store()?; assert_eq!( store.find_loose("FETCH_HEAD")?.target.id(), @@ -167,7 +162,7 @@ mod loose { } #[test] - fn success() -> Result { + fn success() -> gix_testtools::TestResult { let store = store()?; for (partial_name, expected_path, expected_ref_kind) in &[ ("dt1", "refs/tags/dt1", gix_ref::Kind::Object), // tags before heads @@ -192,7 +187,7 @@ mod loose { } #[test] - fn failure() -> Result { + fn failure() -> gix_testtools::TestResult { let store = store()?; for (partial_name, reason, is_err) in &[ ("foobar", "does not exist", false), @@ -247,7 +242,7 @@ mod loose { } #[test] - fn prefix_file_collision_is_not_found() -> Result { + fn prefix_file_collision_is_not_found() -> gix_testtools::TestResult { let (_tmp, store) = crate::file::store_writable("make_ref_repository.sh")?; assert!( diff --git a/gix-ref/tests/refs/file/store/iter.rs b/gix-ref/tests/refs/file/store/iter.rs index 285a46c288a..e28c6216066 100644 --- a/gix-ref/tests/refs/file/store/iter.rs +++ b/gix-ref/tests/refs/file/store/iter.rs @@ -1,4 +1,3 @@ -use crate::Result; use crate::{ file::{store, store_at, store_with_packed_refs}, hex_to_id, @@ -7,7 +6,7 @@ use gix_object::bstr::ByteSlice; #[test] #[cfg_attr(not(target_os = "macos"), ignore = "Needs filesystem that folds Unicode composition")] -fn emoji_parent_does_not_duplicate_precomposed_references() -> Result { +fn emoji_parent_does_not_duplicate_precomposed_references() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let root = tmp.path().join("📹"); std::fs::create_dir_all(root.join("refs/heads"))?; @@ -43,13 +42,12 @@ fn emoji_parent_does_not_duplicate_precomposed_references() -> Result { } mod with_namespace { - use crate::Result; use gix_object::bstr::{BString, ByteSlice}; use crate::file::{store_at, transaction::prepare_and_commit::empty_store}; #[test] - fn missing_refs_dir_yields_empty_iteration() -> Result { + fn missing_refs_dir_yields_empty_iteration() -> gix_testtools::TestResult { let (_dir, store) = empty_store()?; assert_eq!(store.iter()?.all()?.count(), 0); assert_eq!(store.loose_iter()?.count(), 0); @@ -57,7 +55,7 @@ mod with_namespace { } #[test] - fn iteration_can_trivially_use_namespaces_as_prefixes() -> Result { + fn iteration_can_trivially_use_namespaces_as_prefixes() -> gix_testtools::TestResult { let store = store_at("make_namespaced_packed_ref_repository.sh")?; let packed = store.open_packed_buffer()?; @@ -167,7 +165,7 @@ mod with_namespace { } #[test] - fn iteration_on_store_with_namespace_makes_namespace_transparent() -> Result { + fn iteration_on_store_with_namespace_makes_namespace_transparent() -> gix_testtools::TestResult { let ns_two = gix_ref::namespace::expand("bar")?; let mut ns_store = { let mut s = store_at("make_namespaced_packed_ref_repository.sh")?; @@ -266,7 +264,7 @@ mod with_namespace { } #[test] -fn no_packed_available_thus_no_iteration_possible() -> Result { +fn no_packed_available_thus_no_iteration_possible() -> gix_testtools::TestResult { let store_without_packed = store()?; assert!( store_without_packed.open_packed_buffer()?.is_none(), @@ -276,14 +274,14 @@ fn no_packed_available_thus_no_iteration_possible() -> Result { } #[test] -fn packed_file_iter() -> Result { +fn packed_file_iter() -> gix_testtools::TestResult { let store = store_with_packed_refs()?; assert_eq!(store.open_packed_buffer()?.expect("pack available").iter()?.count(), 11); Ok(()) } #[test] -fn pseudo_refs_iter() -> Result { +fn pseudo_refs_iter() -> gix_testtools::TestResult { let store = store_at("make_pseudo_ref_repository.sh")?; let actual = store @@ -296,7 +294,7 @@ fn pseudo_refs_iter() -> Result { } #[test] -fn loose_iter_with_broken_refs() -> Result { +fn loose_iter_with_broken_refs() -> gix_testtools::TestResult { let store = store()?; let mut actual: Vec<_> = store.loose_iter()?.collect(); @@ -350,12 +348,11 @@ fn loose_iter_with_broken_refs() -> Result { } #[test] -fn loose_iter_with_prefix() -> Result { +fn loose_iter_with_prefix() -> gix_testtools::TestResult { let prefix_with_slash = b"refs/heads/"; let actual = store()? .loose_iter_prefixed(prefix_with_slash.try_into().unwrap())? - .collect::<std::result::Result<Vec<_>, _>>() - .expect("no broken ref in this subset"); + .collect::<std::result::Result<Vec<_>, _>>()?; assert_eq!( actual, @@ -372,12 +369,11 @@ fn loose_iter_with_prefix() -> Result { } #[test] -fn loose_iter_with_partial_prefix_dir() -> Result { +fn loose_iter_with_partial_prefix_dir() -> gix_testtools::TestResult { let prefix_without_slash = b"refs/heads"; let actual = store()? .loose_iter_prefixed(prefix_without_slash.try_into().unwrap())? - .collect::<std::result::Result<Vec<_>, _>>() - .expect("no broken ref in this subset"); + .collect::<std::result::Result<Vec<_>, _>>()?; assert_eq!( actual, @@ -394,18 +390,17 @@ fn loose_iter_with_partial_prefix_dir() -> Result { } #[test] -fn loose_iter_with_partial_prefix() -> Result { +fn loose_iter_with_partial_prefix() -> gix_testtools::TestResult { let actual = store()? .loose_iter_prefixed(b"refs/heads/d".as_bstr().try_into().unwrap())? - .collect::<std::result::Result<Vec<_>, _>>() - .expect("no broken ref in this subset"); + .collect::<std::result::Result<Vec<_>, _>>()?; assert_eq!(actual, ["refs/heads/d1", "refs/heads/dt1"], "all paths are as expected"); Ok(()) } #[test] -fn overlay_iter() -> Result { +fn overlay_iter() -> gix_testtools::TestResult { use gix_ref::Target::*; let store = store_at("make_packed_ref_repository_for_overlay.sh")?; @@ -439,7 +434,7 @@ fn overlay_iter() -> Result { } #[test] -fn overlay_iter_reproduce_1850() -> Result { +fn overlay_iter_reproduce_1850() -> gix_testtools::TestResult { let store = store_at("make_repo_for_1850_repro.sh")?; let ref_names = store .iter()? @@ -533,7 +528,7 @@ fn overlay_iter_reproduce_1850() -> Result { } #[test] -fn overlay_iter_reproduce_1928() -> Result { +fn overlay_iter_reproduce_1928() -> gix_testtools::TestResult { let store = store_at("make_repo_for_1928_repro.sh")?; let ref_names = store .iter()? @@ -573,7 +568,7 @@ fn overlay_iter_reproduce_1928() -> Result { } #[test] -fn overlay_prefixed_iter() -> Result { +fn overlay_prefixed_iter() -> gix_testtools::TestResult { use gix_ref::Target::*; let store = store_at("make_packed_ref_repository_for_overlay.sh")?; @@ -596,7 +591,7 @@ fn overlay_prefixed_iter() -> Result { } #[test] -fn overlay_partial_prefix_iter() -> Result { +fn overlay_partial_prefix_iter() -> gix_testtools::TestResult { use gix_ref::Target::*; let store = store_at("make_packed_ref_repository_for_overlay.sh")?; @@ -612,7 +607,7 @@ fn overlay_partial_prefix_iter() -> Result { #[test] /// The prefix `refs/d` should match `refs/d1` but not `refs/heads/d1`. -fn overlay_partial_prefix_iter_reproduce_1934() -> Result { +fn overlay_partial_prefix_iter_reproduce_1934() -> gix_testtools::TestResult { use gix_ref::Target::*; let store = store_at("make_ref_repository.sh")?; @@ -632,7 +627,7 @@ fn overlay_partial_prefix_iter_reproduce_1934() -> Result { } #[test] -fn overlay_partial_prefix_iter_when_prefix_is_dir() -> Result { +fn overlay_partial_prefix_iter_when_prefix_is_dir() -> gix_testtools::TestResult { // Test 'refs/prefix/' with and without trailing slash. use gix_ref::Target::*; diff --git a/gix-ref/tests/refs/file/store/mod.rs b/gix-ref/tests/refs/file/store/mod.rs index 9bb7eeb7b65..3dd9487fc9a 100644 --- a/gix-ref/tests/refs/file/store/mod.rs +++ b/gix-ref/tests/refs/file/store/mod.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_date::parse::TimeBuf; use gix_lock::acquire::Fail; use gix_ref::{ @@ -20,7 +19,7 @@ mod reflog; #[test] #[cfg_attr(not(target_os = "macos"), ignore = "Needs filesystem that folds Unicode composition")] -fn packed_reference_precomposition_is_component_local() -> Result { +fn packed_reference_precomposition_is_component_local() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let store = gix_ref::file::Store::at_opts( tmp.path().to_owned(), @@ -95,7 +94,7 @@ fn packed_reference_precomposition_is_component_local() -> Result { } #[test] -fn precompose_unicode_journey() -> Result { +fn precompose_unicode_journey() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let precomposed_a = "ä"; let decomposed_a = "a\u{308}"; diff --git a/gix-ref/tests/refs/file/store/reflog.rs b/gix-ref/tests/refs/file/store/reflog.rs index f15f725a6d9..58aa6fe39d1 100644 --- a/gix-ref/tests/refs/file/store/reflog.rs +++ b/gix-ref/tests/refs/file/store/reflog.rs @@ -12,12 +12,11 @@ fn store() -> Result<crate::file::Store> { } mod iter_and_iter_rev { - use crate::Result; use crate::file::store::reflog::store; #[cfg(unix)] #[test] - fn read_failures_preserve_context() -> Result { + fn read_failures_preserve_context() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let store = store()?; let name = "refs/heads/main/child"; @@ -69,7 +68,7 @@ mod iter_and_iter_rev { } #[test] - fn non_existing_and_directory_returns_none() -> Result { + fn non_existing_and_directory_returns_none() -> gix_testtools::TestResult { let store = store()?; let mut buf = Vec::new(); for name in &["FAILURE_NONEXISTING", "refs/heads"] { @@ -82,7 +81,7 @@ mod iter_and_iter_rev { } #[test] - fn for_head_and_main() -> Result { + fn for_head_and_main() -> gix_testtools::TestResult { let store = store()?; let mut buf = Vec::new(); @@ -96,11 +95,10 @@ mod iter_and_iter_rev { } mod iter_rev { - use crate::Result; use crate::file::store::reflog::store; #[test] - fn non_existing_and_directory_returns_none() -> Result { + fn non_existing_and_directory_returns_none() -> gix_testtools::TestResult { let store = store()?; let mut buf = [0u8; 256]; for name in &["FAILURE_NONEXISTING", "refs/heads"] { @@ -113,7 +111,7 @@ mod iter_rev { } #[test] - fn for_head_and_main() -> Result { + fn for_head_and_main() -> gix_testtools::TestResult { let store = store()?; let mut buf = [0u8; 256]; diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs index 8c534b1f04c..24d93b2ab19 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/collisions.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_date::parse::TimeBuf; use gix_lock::acquire::Fail; use gix_ref::{ @@ -21,7 +20,7 @@ fn case_sensitive(tmp_dir: &std::path::Path) -> bool { } #[test] -fn conflicting_creation_without_packed_refs() -> Result { +fn conflicting_creation_without_packed_refs() -> gix_testtools::TestResult { let (dir, store) = empty_store()?; let res = store.transaction().prepare( [create_at("refs/a"), create_at("refs/A")], @@ -55,7 +54,7 @@ fn conflicting_creation_without_packed_refs() -> Result { } #[test] -fn non_conflicting_creation_without_packed_refs_work() -> Result { +fn non_conflicting_creation_without_packed_refs_work() -> gix_testtools::TestResult { let (_dir, store) = empty_store()?; let ongoing = store .transaction() @@ -79,7 +78,8 @@ fn non_conflicting_creation_without_packed_refs_work() -> Result { } #[test] -fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_does_not_need_it() -> Result { +fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_does_not_need_it() +-> gix_testtools::TestResult { let (_dir, store) = empty_store()?; let ref_name = "refs/a"; let _t1 = store @@ -109,7 +109,7 @@ fn packed_refs_lock_is_mandatory_for_multiple_ongoing_transactions_even_if_one_d } #[test] -fn conflicting_creation_into_packed_refs() -> Result { +fn conflicting_creation_into_packed_refs() -> gix_testtools::TestResult { let (dir, store) = empty_store()?; let mut buf = TimeBuf::default(); let transaction = store diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs index cdf28724e67..288edb2bce4 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/create_or_update/mod.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_date::parse::TimeBuf; use gix_hash::ObjectId; use gix_lock::acquire::Fail; @@ -12,6 +11,7 @@ use gix_ref::{ store::WriteReflog, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, }; +use gix_testtools::{Result, TestResult}; use crate::{ file::{ @@ -26,7 +26,7 @@ use crate::{ mod collisions; #[test] -fn intermediate_directories_are_removed_on_rollback() -> Result { +fn intermediate_directories_are_removed_on_rollback() -> TestResult { for explicit_rollback in [false, true] { let (dir, store) = empty_store()?; @@ -58,7 +58,7 @@ fn intermediate_directories_are_removed_on_rollback() -> Result { } #[test] -fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_can_potentially_recover() -> Result { +fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_can_potentially_recover() -> TestResult { #[cfg(not(windows))] let mut error_snapshots = Vec::new(); for is_empty in &[true, false] { @@ -114,7 +114,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca } #[test] -fn reference_with_old_value_must_exist_when_creating_it() -> Result { +fn reference_with_old_value_must_exist_when_creating_it() -> TestResult { let (_keep, store) = empty_store()?; let new_target = Target::Object(crate::fixture_hash_kind().null()); @@ -145,7 +145,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> Result { } #[test] -fn target_mismatch_sources_retain_expected_and_actual_content_and_resolved_name() -> gix_error::TestResult { +fn target_mismatch_sources_retain_expected_and_actual_content_and_resolved_name() -> TestResult { for deref in [false, true] { for delete in [false, true] { for must_exist in [false, true] { @@ -218,7 +218,7 @@ fn target_mismatch_sources_retain_expected_and_actual_content_and_resolved_name( } #[test] -fn reference_with_explicit_value_must_match_the_value_on_update() -> Result { +fn reference_with_explicit_value_must_match_the_value_on_update() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -255,7 +255,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> Result { } #[test] -fn the_existing_must_match_constraint_allow_non_existing_references_to_be_created() -> Result { +fn the_existing_must_match_constraint_allow_non_existing_references_to_be_created() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let expected = PreviousValue::ExistingMustMatch(Target::Object(ObjectId::empty_tree(crate::fixture_hash_kind()))); let mut buf = TimeBuf::default(); @@ -289,7 +289,7 @@ fn the_existing_must_match_constraint_allow_non_existing_references_to_be_create #[test] fn the_existing_must_match_constraint_requires_existing_references_to_have_the_given_value_to_cause_failure_on_mismatch() --> Result { +-> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -326,7 +326,7 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g } #[test] -fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_already() -> Result { +fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_already() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -356,7 +356,7 @@ fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_alrea } #[test] -fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> Result { +fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> TestResult { let (_keep, mut store) = empty_store()?; store.namespace = gix_ref::namespace::expand("foo")?.into(); let actual = vec![ @@ -373,7 +373,7 @@ fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> Resul } #[test] -fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> Result { +fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.try_find_loose("HEAD")?.expect("head exists already"); let target = head.target; @@ -452,7 +452,7 @@ fn reference_with_must_not_exist_constraint_may_exist_already_if_the_new_value_m } #[test] -fn cancellation_after_preparation_leaves_no_change() -> Result { +fn cancellation_after_preparation_leaves_no_change() -> TestResult { let (dir, store) = empty_store()?; let tx = store.transaction(); @@ -476,7 +476,7 @@ fn cancellation_after_preparation_leaves_no_change() -> Result { } #[test] -fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> Result { +fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> TestResult { let (_keep, store) = empty_store()?; let referent = "refs/heads/alt-main"; assert!( @@ -514,7 +514,7 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> Result { Some(referent.as_bytes().as_bstr()) ); assert!( - head.log_exists(&store), + head.log_exists(&store)?, "reflog is written for new symbolic ref with information about the peeled target id, \ as special accommodation for the state during clone to allow us to get a peeled id into the log" ); @@ -524,7 +524,7 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> Result { } #[test] -fn windows_device_name_is_illegal_with_enabled_windows_protections() -> Result { +fn windows_device_name_is_illegal_with_enabled_windows_protections() -> TestResult { let mut error_snapshots = Vec::new(); let (_keep, mut store) = empty_store()?; store.prohibit_windows_device_names = true; @@ -599,7 +599,7 @@ fn windows_device_name_is_illegal_with_enabled_windows_protections() -> Result { /// `LockAcquire(PermanentlyLocked)` instead. #[cfg(not(windows))] #[test] -fn windows_device_name_check_runs_before_lock_acquisition() -> Result { +fn windows_device_name_check_runs_before_lock_acquisition() -> TestResult { let (keep, mut store) = empty_store()?; store.prohibit_windows_device_names = true; @@ -633,7 +633,7 @@ fn windows_device_name_check_runs_before_lock_acquisition() -> Result { } #[test] -fn lock_failure_on_symbolic_referent_is_reported_for_the_symbolic_ref() -> Result { +fn lock_failure_on_symbolic_referent_is_reported_for_the_symbolic_ref() -> TestResult { let (keep, store) = empty_store()?; std::fs::write(keep.path().join("HEAD"), b"ref: refs/heads/main\n")?; std::fs::create_dir_all(keep.path().join("refs/heads"))?; @@ -680,7 +680,7 @@ fn lock_failure_on_symbolic_referent_is_reported_for_the_symbolic_ref() -> Resul } #[test] -fn symbolic_head_missing_referent_then_update_referent() -> Result { +fn symbolic_head_missing_referent_then_update_referent() -> TestResult { for reflog_writemode in &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always] { let (_keep, mut store) = empty_store()?; store.write_reflog = *reflog_writemode; @@ -732,7 +732,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> Result { head.target.to_ref().try_name().map(gix_ref::FullNameRef::as_bstr), Some(referent.as_bytes().as_bstr()) ); - assert!(!head.log_exists(&store), "no reflog is written for symbolic ref"); + assert!(!head.log_exists(&store)?, "no reflog is written for symbolic ref"); assert!(store.try_find_loose(referent)?.is_none(), "referent wasn't created"); let new_oid = hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"); @@ -814,7 +814,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> Result { #[test] /// Writing a peeled ref to which head points to doesn't update HEAD on the fly even though that might be what's would /// be needed to keep the reflog consistent -fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_though_it_should() -> Result { +fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_though_it_should() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; let referent = head.target.to_ref().try_name().expect("symbolic ref").to_owned(); @@ -863,7 +863,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho } #[test] -fn packed_refs_are_looked_up_when_checking_existing_values() -> Result { +fn packed_refs_are_looked_up_when_checking_existing_values() -> TestResult { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; assert!( store.try_find_loose("main")?.is_none(), @@ -912,7 +912,7 @@ fn packed_refs_creation_with_tag_loop_are_not_handled_and_cannot_exist_due_to_ob } #[test] -fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs() -> Result { +fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs() -> TestResult { let (_keep, store) = store_writable("make_ref_repository.sh")?; assert!( store.open_packed_buffer()?.is_none(), @@ -959,7 +959,7 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( } #[test] -fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() -> Result { +fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() -> TestResult { let (_keep, store) = store_writable("make_packed_ref_repository_for_overlay.sh")?; let branch = store.find("newer-as-loose")?; let packed = store.open_packed_buffer()?.expect("packed-refs"); @@ -1013,7 +1013,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() } #[test] -fn packed_refs_deletion_in_deletions_and_updates_mode() -> Result { +fn packed_refs_deletion_in_deletions_and_updates_mode() -> TestResult { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; assert!( store.try_find_loose("refs/heads/d1")?.is_none(), diff --git a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs index 9dfd6198747..ba8cb817038 100644 --- a/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs +++ b/gix-ref/tests/refs/file/transaction/prepare_and_commit/delete.rs @@ -1,4 +1,3 @@ -use crate::Result; use crate::{ file::{ store_writable, @@ -13,9 +12,10 @@ use gix_ref::{ file::ReferenceExt, transaction::{PreviousValue, RefEdit, RefLog}, }; +use gix_testtools::TestResult; #[test] -fn delete_a_ref_which_is_gone_succeeds() -> Result { +fn delete_a_ref_which_is_gone_succeeds() -> TestResult { let (_keep, store) = empty_store()?; let edits = store .transaction() @@ -30,7 +30,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> Result { } #[test] -fn delete_a_ref_which_is_gone_but_must_exist_fails() -> Result { +fn delete_a_ref_which_is_gone_but_must_exist_fails() -> TestResult { let (_keep, store) = empty_store()?; let res = store.transaction().prepare( Some(RefEdit::delete("DOES_NOT_EXIST".try_into()?, PreviousValue::MustExist)), @@ -49,10 +49,10 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> Result { } #[test] -fn delete_ref_and_reflog_on_symbolic_no_deref() -> Result { +fn delete_ref_and_reflog_on_symbolic_no_deref() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; - assert!(head.log_exists(&store)); + assert!(head.log_exists(&store)?); let _main = store.find_loose("main")?; let edits = store @@ -82,10 +82,10 @@ fn delete_ref_and_reflog_on_symbolic_no_deref() -> Result { } #[test] -fn delete_ref_with_incorrect_previous_value_fails() -> Result { +fn delete_ref_with_incorrect_previous_value_fails() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; - assert!(head.log_exists(&store)); + assert!(head.log_exists(&store)?); let res = store.transaction().prepare( Some( @@ -123,17 +123,17 @@ fn delete_ref_with_incorrect_previous_value_fails() -> Result { ); // everything stays as is let head = store.find_loose("HEAD")?; - assert!(head.log_exists(&store)); - let main = store.find_loose("main").expect("referent still exists"); - assert!(main.log_exists(&store)); + assert!(head.log_exists(&store)?); + let main = store.find_loose("main")?; + assert!(main.log_exists(&store)?); Ok(()) } #[test] -fn delete_reflog_only_of_symbolic_no_deref() -> Result { +fn delete_reflog_only_of_symbolic_no_deref() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; - assert!(head.log_exists(&store)); + assert!(head.log_exists(&store)?); let edits = store .transaction() @@ -150,9 +150,9 @@ fn delete_reflog_only_of_symbolic_no_deref() -> Result { assert_eq!(edits.len(), 1); let head: Reference = store.find_loose("HEAD")?.into(); - assert!(!head.log_exists(&store)); - let main = store.find_loose("main").expect("referent still exists"); - assert!(main.log_exists(&store), "log is untouched, too"); + assert!(!head.log_exists(&store)?); + let main = store.find_loose("main")?; + assert!(main.log_exists(&store)?, "log is untouched, too"); assert_eq!( main.target, head.follow(&store).expect("a symref")?.target, @@ -162,10 +162,10 @@ fn delete_reflog_only_of_symbolic_no_deref() -> Result { } #[test] -fn delete_reflog_only_of_symbolic_with_deref() -> Result { +fn delete_reflog_only_of_symbolic_with_deref() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let head = store.find_loose("HEAD")?; - assert!(head.log_exists(&store)); + assert!(head.log_exists(&store)?); let edits = store .transaction() @@ -178,9 +178,9 @@ fn delete_reflog_only_of_symbolic_with_deref() -> Result { assert_eq!(edits.len(), 2); let head: Reference = store.find_loose("HEAD")?.into(); - assert!(!head.log_exists(&store)); - let main = store.find_loose("main").expect("referent still exists"); - assert!(!main.log_exists(&store), "log is removed"); + assert!(!head.log_exists(&store)?); + let main = store.find_loose("main")?; + assert!(!main.log_exists(&store)?, "log is removed"); assert_eq!( main.target, head.follow(&store).expect("a symref")?.target, @@ -190,7 +190,7 @@ fn delete_reflog_only_of_symbolic_with_deref() -> Result { } #[test] -fn rename_a_to_a_slash_b_in_one_transaction() -> Result { +fn rename_a_to_a_slash_b_in_one_transaction() -> TestResult { let (_keep, store) = store_writable("make_repo_for_reflog.sh")?; let old = store.find_loose("old")?; @@ -251,7 +251,7 @@ fn rename_a_to_a_slash_b_in_one_transaction() -> Result { #[test] /// Based on https://github.com/git/git/blob/master/refs/files-backend.c#L514:L515 -fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> Result { +fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> TestResult { let (_keep, store) = empty_store()?; std::fs::write(store.git_dir().join("HEAD"), b"broken")?; assert!(store.try_find_loose("HEAD").is_err(), "the ref is truly broken"); @@ -276,7 +276,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> Result { } #[test] -fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> Result { +fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> TestResult { let (_keep, store) = empty_store()?; let previous_value = PreviousValue::ExistingMustMatch(Target::Object(hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"))); @@ -298,7 +298,7 @@ fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> Result { #[test] /// Based on https://github.com/git/git/blob/master/refs/files-backend.c#L514:L515 -fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> Result { +fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> TestResult { let (_keep, store) = empty_store()?; std::fs::write(store.git_dir().join("HEAD"), b"broken")?; assert!(store.try_find_loose("HEAD").is_err(), "the ref is truly broken"); @@ -318,14 +318,14 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> Result { } #[test] -fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> Result { +fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> TestResult { for reflog_writemode in &[ gix_ref::store::WriteReflog::Normal, gix_ref::store::WriteReflog::Disable, ] { let (_keep, mut store) = store_writable("make_repo_for_reflog.sh")?; store.write_reflog = *reflog_writemode; - assert!(store.find_loose("HEAD")?.log_exists(&store)); + assert!(store.find_loose("HEAD")?.log_exists(&store)?); assert!(store.open_packed_buffer()?.is_none(), "there is no pack"); let edits = store @@ -341,7 +341,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> Result { )? .commit(committer().to_ref(&mut TimeBuf::default()))?; assert_eq!(edits.len(), 1); - assert!(!store.find_loose("HEAD")?.log_exists(&store), "log was deleted"); + assert!(!store.find_loose("HEAD")?.log_exists(&store)?, "log was deleted"); assert!(store.open_packed_buffer()?.is_none(), "there still is no pack"); } Ok(()) @@ -349,7 +349,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> Result { #[test] fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_deleted_and_are_deleted_from_packed_ref_file() --> Result { +-> TestResult { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; assert!( store.try_find_loose("main")?.is_none(), @@ -380,7 +380,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete } #[test] -fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_refs() -> Result { +fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_refs() -> TestResult { let (_keep, store) = store_writable("make_packed_ref_repository_for_overlay.sh")?; let packed = store.open_packed_buffer()?.expect("packed-refs"); let branch = store.find("newer-as-loose")?; @@ -416,7 +416,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ } #[test] -fn all_contained_references_deletes_the_packed_ref_file_too() -> Result { +fn all_contained_references_deletes_the_packed_ref_file_too() -> TestResult { for mode in ["must-exist", "may-exist"] { let (_keep, store) = store_writable("make_packed_ref_repository.sh")?; let edits = store diff --git a/gix-ref/tests/refs/file/worktree.rs b/gix-ref/tests/refs/file/worktree.rs index 0be49a721d4..600efb079cc 100644 --- a/gix-ref/tests/refs/file/worktree.rs +++ b/gix-ref/tests/refs/file/worktree.rs @@ -83,11 +83,10 @@ impl From<Mode> for bool { } mod read_only { - use crate::Result; use crate::file::worktree::{Mode, assert_reflog, into_peel, main_store, worktree_store}; #[test] - fn linked() -> Result { + fn linked() -> gix_testtools::TestResult { for packed in [false, true] { let (store, odb, _tmp) = worktree_store(packed, "w1", Mode::Read)?; assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); @@ -136,7 +135,7 @@ mod read_only { } #[test] - fn main() -> Result { + fn main() -> gix_testtools::TestResult { for packed in [false, true] { let (store, odb, _tmp) = main_store(packed, Mode::Read)?; assert_eq!(store.is_pristine("refs/heads/main".try_into()?), Some(false)); @@ -197,7 +196,6 @@ mod read_only { } mod writable { - use crate::Result; use gix_date::parse::TimeBuf; use gix_lock::acquire::Fail; use gix_ref::{ @@ -224,7 +222,7 @@ mod writable { } #[test] - fn main() -> Result { + fn main() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let new_id_main = hex_to_id("11111111111111111162102c6a483440bfda2a03"); let new_id_main_str = new_id_main.to_string(); @@ -259,8 +257,7 @@ mod writable { Fail::Immediately, Fail::Immediately, )? - .commit(committer().to_ref(&mut TimeBuf::default())) - .expect("successful commit as even similar resolved names live in different base locations"); + .commit(committer().to_ref(&mut TimeBuf::default()))?; assert_eq!( store @@ -486,7 +483,7 @@ mod writable { } #[test] - fn linked() -> Result { + fn linked() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let new_id = hex_to_id("134385f6d781b7e97062102c6a483440bfda2a03"); let new_id_str = new_id.to_string(); @@ -534,8 +531,7 @@ mod writable { Fail::Immediately, Fail::Immediately, )? - .commit(committer().to_ref(&mut TimeBuf::default())) - .expect("successful commit as even similar resolved names live in different base locations"); + .commit(committer().to_ref(&mut TimeBuf::default()))?; assert_eq!( store diff --git a/gix-ref/tests/refs/fullname.rs b/gix-ref/tests/refs/fullname.rs index 42289de9e2d..95de0266123 100644 --- a/gix-ref/tests/refs/fullname.rs +++ b/gix-ref/tests/refs/fullname.rs @@ -17,6 +17,29 @@ fn partial_name_from_os_str_retains_concrete_error() { ); } +#[cfg(windows)] +#[test] +fn native_reference_paths_reject_invalid_encoding() -> gix_testtools::TestResult { + use gix_ref::bstr::ByteSlice; + + let name = FullName::try_from(b"refs/heads/\xff".as_bstr())?; + let namespace = gix_ref::namespace::expand(b"\xff".as_bstr())?; + for result in [ + name.to_path(), + name.as_ref().to_path(), + name.as_ref().as_partial_name().to_partial_path(), + namespace.to_path(), + ] { + assert!( + result + .expect_err("the name cannot be represented on Windows") + .is_validation(), + "Git names remain byte-oriented until a native path is requested" + ); + } + Ok(()) +} + #[test] fn file_name() { let name: gix_ref::FullName = "refs/heads/main".try_into().unwrap(); @@ -153,7 +176,7 @@ fn shorten_and_category() { } #[test] -fn to_full_name() -> gix_testtools::Result { +fn to_full_name() -> gix_testtools::TestResult { assert_eq!( Category::LocalBranch.to_full_name("refs/heads/full")?, "refs/heads/full", @@ -170,7 +193,7 @@ fn to_full_name() -> gix_testtools::Result { } #[test] -fn local_branch_head_is_representable_as_full_ref_name() -> gix_testtools::Result { +fn local_branch_head_is_representable_as_full_ref_name() -> gix_testtools::TestResult { assert_eq!( Category::LocalBranch.to_full_name("HEAD")?, "refs/heads/HEAD", diff --git a/gix-ref/tests/refs/main.rs b/gix-ref/tests/refs/main.rs index abda46b0be4..6c6f4b90ce2 100644 --- a/gix-ref/tests/refs/main.rs +++ b/gix-ref/tests/refs/main.rs @@ -68,11 +68,10 @@ mod error; mod file; mod fullname; mod partialname { - use crate::Result; use gix_ref::PartialName; #[test] - fn join() -> Result { + fn join() -> gix_testtools::TestResult { let pn = PartialName::try_from("no-trailing-slash")?; assert_eq!(pn.join("name".into())?, "no-trailing-slash/name"); diff --git a/gix-ref/tests/refs/packed/find.rs b/gix-ref/tests/refs/packed/find.rs index 0c7072156a1..8f981936cc5 100644 --- a/gix-ref/tests/refs/packed/find.rs +++ b/gix-ref/tests/refs/packed/find.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_ref::packed; use gix_testtools::fixture_path; @@ -19,7 +18,7 @@ fn a_lock_file_would_not_be_a_valid_partial_name() { } #[test] -fn capitalized_branch() -> Result { +fn capitalized_branch() -> gix_testtools::TestResult { let store = store_with_packed_refs()?; let packed_refs = store.open_packed_buffer()?.expect("packed-refs exist"); @@ -32,7 +31,7 @@ fn capitalized_branch() -> Result { } #[test] -fn all_iterable_refs_can_be_found() -> Result { +fn all_iterable_refs_can_be_found() -> gix_testtools::TestResult { let store = store_with_packed_refs()?; let packed_refs = store.open_packed_buffer()?.expect("packed-refs exist"); @@ -47,7 +46,7 @@ fn all_iterable_refs_can_be_found() -> Result { } #[test] -fn binary_search_a_name_past_the_end_of_the_packed_refs_file() -> Result { +fn binary_search_a_name_past_the_end_of_the_packed_refs_file() -> gix_testtools::TestResult { let packed_refs = packed::Buffer::open( fixture_path("packed-refs").join("triggers-out-of-bounds"), 32, @@ -58,7 +57,7 @@ fn binary_search_a_name_past_the_end_of_the_packed_refs_file() -> Result { } #[test] -fn find_packed_refs_with_peeled_items_and_full_or_partial_names() -> Result { +fn find_packed_refs_with_peeled_items_and_full_or_partial_names() -> gix_testtools::TestResult { let packed_refs = b"# pack-refs with: peeled fully-peeled sorted 916840c0e2f67d370291042cb5274a597f4fa9bc refs/tags/TEST-0.0.1 c4cebba92af964f2d126be90b8a6298c4cf84d45 refs/tags/gix-actor-v0.1.0 @@ -98,7 +97,7 @@ c4cebba92af964f2d126be90b8a6298c4cf84d45 refs/tags/gix-actor-v0.1.0 } #[test] -fn partial_name_to_full_name_conversion_rules_are_applied() -> Result { +fn partial_name_to_full_name_conversion_rules_are_applied() -> gix_testtools::TestResult { let store = store_at("make_packed_refs_for_lookup_rules.sh")?; let packed = store.open_packed_buffer()?.expect("packed-refs exists"); @@ -156,7 +155,7 @@ fn partial_name_to_full_name_conversion_rules_are_applied() -> Result { } #[test] -fn invalid_refs_within_a_file_do_not_lead_to_incorrect_results() -> Result { +fn invalid_refs_within_a_file_do_not_lead_to_incorrect_results() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let broken_packed_refs = b"# pack-refs with: peeled fully-peeled sorted 916840c0e2f67d370291042cb5274a597f4fa9bc refs/tags/TEST-0.0.1 @@ -202,7 +201,7 @@ bogus refs/tags/gix-actor-v0.1.0 } #[test] -fn find_speed() -> Result { +fn find_speed() -> gix_testtools::TestResult { let store = store_at("make_repository_with_lots_of_packed_refs.sh")?; let packed = store.open_packed_buffer()?.expect("packed-refs present"); let start = std::time::Instant::now(); diff --git a/gix-ref/tests/refs/packed/iter.rs b/gix-ref/tests/refs/packed/iter.rs index d7ca855d42c..a87a120a64e 100644 --- a/gix-ref/tests/refs/packed/iter.rs +++ b/gix-ref/tests/refs/packed/iter.rs @@ -1,4 +1,3 @@ -use crate::Result; use gix_error::{Message, MetadataValue}; use gix_ref::packed; @@ -7,7 +6,7 @@ use crate::file::{store_at, store_with_packed_refs}; const HASH_KIND: gix_hash::Kind = gix_hash::Kind::Sha1; #[test] -fn empty() -> Result { +fn empty() -> gix_testtools::TestResult { assert_eq!( packed::Iter::new(&[], HASH_KIND)?.count(), 0, @@ -60,7 +59,7 @@ fn invalid_header_has_one_classified_diagnostic() { } #[test] -fn packed_refs_with_header() -> Result { +fn packed_refs_with_header() -> gix_testtools::TestResult { let dir = crate::scripted_fixture_read_only("make_packed_ref_repository.sh")?; let buf = std::fs::read(dir.join(".git").join("packed-refs"))?; let iter = packed::Iter::new(&buf, crate::fixture_hash_kind())?; @@ -69,7 +68,7 @@ fn packed_refs_with_header() -> Result { } #[test] -fn iter_prefix() -> Result { +fn iter_prefix() -> gix_testtools::TestResult { let packed = store_with_packed_refs()?.open_packed_buffer()?.expect("packed-refs"); assert_eq!( packed @@ -113,7 +112,7 @@ fn iter_prefix() -> Result { } #[test] -fn packed_refs_without_header() -> Result { +fn packed_refs_without_header() -> gix_testtools::TestResult { let packed_refs = b"916840c0e2f67d370291042cb5274a597f4fa9bc refs/tags/TEST-0.0.1 c4cebba92af964f2d126be90b8a6298c4cf84d45 refs/tags/gix-actor-v0.1.0 ^13da90b54699a6b500ec5cd7d175f2cd5a1bed06 @@ -142,7 +141,7 @@ c4cebba92af964f2d126be90b8a6298c4cf84d45 refs/tags/gix-actor-v0.1.0 } #[test] -fn broken_ref_doesnt_end_the_iteration() -> Result { +fn broken_ref_doesnt_end_the_iteration() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let packed_refs = b"916840c0e2f67d370291042cb5274a597f4fa9bc refs/tags/TEST-0.0.1 buggy-hash refs/wrong @@ -182,7 +181,7 @@ buggy-hash refs/wrong } #[test] -fn performance() -> Result { +fn performance() -> gix_testtools::TestResult { let store = store_at("make_repository_with_lots_of_packed_refs.sh")?; let start = std::time::Instant::now(); let actual = store @@ -202,7 +201,7 @@ fn performance() -> Result { } #[test] -fn error_metadata_counts_peeled_lines_and_retains_unterminated_input() -> Result { +fn error_metadata_counts_peeled_lines_and_retains_unterminated_input() -> gix_testtools::TestResult { let input = format!("{0} refs/tags/one\n^{0}\nbroken", HASH_KIND.null()); let mut iter = packed::Iter::new(input.as_bytes(), HASH_KIND)?; iter.next().expect("peeled tag")?; diff --git a/gix-ref/tests/refs/packed/open.rs b/gix-ref/tests/refs/packed/open.rs index 0656c9b4507..1f8c2c11560 100644 --- a/gix-ref/tests/refs/packed/open.rs +++ b/gix-ref/tests/refs/packed/open.rs @@ -1,4 +1,3 @@ -use crate::Result; use std::path::Path; use gix_testtools::fixture_path; @@ -14,7 +13,7 @@ fn sorted_buffer_works() { } #[test] -fn empty_buffers_should_not_exist_but_are_fine_to_open() -> Result { +fn empty_buffers_should_not_exist_but_are_fine_to_open() -> gix_testtools::TestResult { let (_keep, path) = write_packed_refs_with(&[])?; assert_eq!(gix_ref::packed::Buffer::open(path, 512, HASH_KIND)?.iter()?.count(), 0); Ok(()) @@ -42,7 +41,7 @@ fn unsorted_buffers_or_those_without_a_header_can_be_opened_and_searched() { } #[test] -fn bogus_content_triggers_an_error() -> Result { +fn bogus_content_triggers_an_error() -> gix_testtools::TestResult { let packed_refs_data = b"starts with a bogus record, not a header anyway"; let (_keep, path) = write_packed_refs_with(packed_refs_data)?; diff --git a/gix-ref/tests/refs/transaction.rs b/gix-ref/tests/refs/transaction.rs index 5467e4162de..c74412554df 100644 --- a/gix-ref/tests/refs/transaction.rs +++ b/gix-ref/tests/refs/transaction.rs @@ -1,7 +1,6 @@ mod refedit { - use crate::Result; #[test] - fn constructors_apply_common_defaults() -> Result { + fn constructors_apply_common_defaults() -> gix_testtools::TestResult { use gix_ref::{ FullName, Target, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, @@ -64,7 +63,6 @@ mod refedit { } mod refedit_ext { - use crate::Result; use std::{cell::RefCell, collections::BTreeMap}; use gix_object::bstr::{BString, ByteSlice}; @@ -101,7 +99,7 @@ mod refedit_ext { } #[test] - fn preprocessing_checks_duplicates_after_splits() -> Result { + fn preprocessing_checks_duplicates_after_splits() -> gix_testtools::TestResult { let store = MockStore::with(Some(("HEAD", Target::Symbolic("refs/heads/main".try_into()?)))); let mut edits = vec![ @@ -139,7 +137,6 @@ mod refedit_ext { } mod splitting { - use crate::Result; use std::cell::Cell; use gix_ref::{ @@ -154,7 +151,7 @@ mod refedit_ext { } #[test] - fn non_symbolic_refs_are_ignored_or_if_the_deref_flag_is_not_set() -> Result { + fn non_symbolic_refs_are_ignored_or_if_the_deref_flag_is_not_set() -> gix_testtools::TestResult { let store = MockStore::with(Some(( "refs/heads/anything-but-not-symbolic", Target::Object(gix_hash::Kind::Sha1.null()), @@ -189,7 +186,7 @@ mod refedit_ext { Ok(()) } #[test] - fn empty_inputs_are_ok() -> Result { + fn empty_inputs_are_ok() -> gix_testtools::TestResult { let store = MockStore::default(); Vec::<RefEdit>::new() .extend_with_splits_of_symbolic_refs(&mut |n| store.find_existing(n), &mut |_, e| e) @@ -197,7 +194,7 @@ mod refedit_ext { } #[test] - fn symbolic_refs_cycles_are_handled_gracefully() -> Result { + fn symbolic_refs_cycles_are_handled_gracefully() -> gix_testtools::TestResult { #[derive(Default)] struct Cycler { next_item: Cell<bool>, @@ -243,7 +240,8 @@ mod refedit_ext { } #[test] - fn symbolic_refs_are_split_into_referents_handling_the_reflog_and_previous_values_recursively() -> Result { + fn symbolic_refs_are_split_into_referents_handling_the_reflog_and_previous_values_recursively() + -> gix_testtools::TestResult { let store = MockStore::with(vec![ ( "refs/heads/delete-symbolic-1", diff --git a/gix-ref/tests/transaction_fd_limit.rs b/gix-ref/tests/transaction_fd_limit.rs index d98417084ff..6b121aa52c5 100644 --- a/gix-ref/tests/transaction_fd_limit.rs +++ b/gix-ref/tests/transaction_fd_limit.rs @@ -8,7 +8,7 @@ use gix_ref::{ /// Preparing a transaction must retain only a `gix_lock::Marker` per edit, not an open file descriptor. #[test] -fn large_transactions_hold_a_constant_number_of_file_descriptors() -> gix_testtools::Result { +fn large_transactions_hold_a_constant_number_of_file_descriptors() -> gix_testtools::TestResult { let limit = libc::rlimit { rlim_cur: 16, rlim_max: 16, diff --git a/gix-refspec/tests/refspec/parse/mod.rs b/gix-refspec/tests/refspec/parse/mod.rs index bf3458a54be..62572c72921 100644 --- a/gix-refspec/tests/refspec/parse/mod.rs +++ b/gix-refspec/tests/refspec/parse/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::panic::catch_unwind; use bstr::ByteSlice; @@ -6,9 +6,9 @@ use gix_refspec::parse::Operation; use gix_testtools::scripted_fixture_read_only; #[test] -fn baseline() { - let dir = scripted_fixture_read_only("parse_baseline.sh").unwrap(); - let baseline = std::fs::read(dir.join("baseline.git")).unwrap(); +fn baseline() -> TestResult { + let dir = scripted_fixture_read_only("parse_baseline.sh")?; + let baseline = std::fs::read(dir.join("baseline.git"))?; let mut lines = baseline.lines(); let mut panics = 0; let mut mismatch = 0; @@ -17,13 +17,7 @@ fn baseline() { count += 1; let (kind, spec) = kind_spec.split_at(kind_spec.find_byte(b' ').expect("space between kind and spec")); let spec = &spec[1..]; - let err_code: usize = lines - .next() - .expect("err code") - .to_str() - .unwrap() - .parse() - .expect("number"); + let err_code: usize = lines.next().expect("err code").to_str()?.parse()?; let op = match kind { b"fetch" => Operation::Fetch, b"push" => Operation::Push, @@ -56,10 +50,11 @@ fn baseline() { panics ); } + Ok(()) } #[test] -fn local_and_remote() -> Result { +fn local_and_remote() -> TestResult { let spec = gix_refspec::parse("remote:local".into(), Operation::Fetch)?; assert_eq!(spec.remote(), spec.source()); assert_eq!(spec.local(), spec.destination()); diff --git a/gix-revision/tests/revision/describe/mod.rs b/gix-revision/tests/revision/describe/mod.rs index 74bc2067821..b5c93870983 100644 --- a/gix-revision/tests/revision/describe/mod.rs +++ b/gix-revision/tests/revision/describe/mod.rs @@ -28,20 +28,20 @@ fn run_test( } #[test] -fn option_none_if_no_tag_found() -> Result<(), gix_error::Error> { - run_test( +fn option_none_if_no_tag_found() -> gix_testtools::TestResult { + Ok(run_test( std::convert::identity, |_| Default::default(), |res, _id| { assert!(res?.is_none(), "cannot find anything if there's no candidate"); Ok(()) }, - ) + )?) } #[test] -fn fallback_if_configured_in_options_but_no_candidate_or_names() -> Result<(), gix_error::Error> { - run_test( +fn fallback_if_configured_in_options_but_no_candidate_or_names() -> gix_testtools::TestResult { + Ok(run_test( std::convert::identity, |_| describe::Options { fallback_to_oid: true, @@ -58,12 +58,12 @@ fn fallback_if_configured_in_options_but_no_candidate_or_names() -> Result<(), g assert_eq!(res.into_format(7).to_string(), id.to_hex_with_len(7).to_string()); Ok(()) }, - ) + )?) } #[test] -fn fallback_if_configured_in_options_and_max_candidates_zero() -> Result<(), gix_error::Error> { - run_test( +fn fallback_if_configured_in_options_and_max_candidates_zero() -> gix_testtools::TestResult { + Ok(run_test( std::convert::identity, |_| describe::Options { fallback_to_oid: true, @@ -78,13 +78,13 @@ fn fallback_if_configured_in_options_and_max_candidates_zero() -> Result<(), gix assert_eq!(res.into_format(7).to_string(), id.to_hex_with_len(7).to_string()); Ok(()) }, - ) + )?) } #[test] -fn not_enough_candidates() -> Result<(), gix_error::Error> { +fn not_enough_candidates() -> gix_testtools::TestResult { let name = Cow::Borrowed(b"at-c5".as_bstr()); - run_test( + Ok(run_test( std::convert::identity, |_| describe::Options { name_by_oid: vec![ @@ -110,11 +110,11 @@ fn not_enough_candidates() -> Result<(), gix_error::Error> { ); Ok(()) }, - ) + )?) } #[test] -fn typical_usecases() -> Result<(), gix_error::Error> { +fn typical_usecases() -> gix_testtools::TestResult { let name = Cow::Borrowed(b"main".as_bstr()); run_test( std::convert::identity, @@ -166,7 +166,7 @@ fn typical_usecases() -> Result<(), gix_error::Error> { }, )?; - run_test( + Ok(run_test( std::convert::identity, |_| describe::Options { name_by_oid: vec![ @@ -193,12 +193,12 @@ fn typical_usecases() -> Result<(), gix_error::Error> { ); Ok(()) }, - ) + )?) } #[test] -fn shallow_yields_no_result_if_provided_refs_are_in_truncated_part_of_history() -> Result<(), gix_error::Error> { - run_test( +fn shallow_yields_no_result_if_provided_refs_are_in_truncated_part_of_history() -> gix_testtools::TestResult { + Ok(run_test( |_| branches_odb_at("shallow-1-clone"), |_| describe::Options { name_by_oid: vec![( @@ -218,13 +218,13 @@ fn shallow_yields_no_result_if_provided_refs_are_in_truncated_part_of_history() ); Ok(()) }, - ) + )?) } #[test] -fn shallow_yields_result_if_refs_are_available() -> Result<(), gix_error::Error> { +fn shallow_yields_result_if_refs_are_available() -> gix_testtools::TestResult { let name = Cow::Borrowed(b"at-c5".as_bstr()); - run_test( + Ok(run_test( |_| branches_odb_at("shallow-2-clone"), |_| describe::Options { name_by_oid: vec![(hex_to_id("efd9a841189668f1bab5b8ebade9cd0a1b139a37"), name.clone())] @@ -245,7 +245,7 @@ fn shallow_yields_result_if_refs_are_available() -> Result<(), gix_error::Error> ); Ok(()) }, - ) + )?) } fn branches_odb_at(name: &str) -> gix_odb::Handle { diff --git a/gix-revision/tests/revision/merge_base.rs b/gix-revision/tests/revision/merge_base.rs index f03f5acc653..736ebc25cbc 100644 --- a/gix-revision/tests/revision/merge_base.rs +++ b/gix-revision/tests/revision/merge_base.rs @@ -49,7 +49,7 @@ fn lookup_failures_retain_their_causes() { } #[test] -fn validate() -> Result { +fn validate() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; let mut count = 0; let odb = odb_at(root.join(".git/objects"))?; @@ -87,7 +87,7 @@ fn validate() -> Result { } #[test] -fn exhausted_side_skips_unrelated_history() -> Result { +fn exhausted_side_skips_unrelated_history() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; let odb = odb_at(root.join(".git/objects"))?; let tip_commit_id = tag_commit_id(&root, "PL")?; @@ -124,7 +124,7 @@ fn exhausted_side_skips_unrelated_history() -> Result { } #[test] -fn unreliable_generations_do_not_allow_side_exhaustion() -> Result { +fn unreliable_generations_do_not_allow_side_exhaustion() -> gix_testtools::TestResult { let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?; let odb = odb_at(root.join(".git/objects"))?; // G and H share B, but clock skew visits B's ancestor E first. Missing, @@ -166,7 +166,7 @@ mod octopus { use crate::{hex_to_id, odb_at}; #[test] - fn three_sequential_commits() -> Result { + fn three_sequential_commits() -> gix_testtools::TestResult { let odb = octopus_odb_at("three-sequential-commits")?; let mut graph = gix_revision::Graph::new(&odb, None); let first_commit = hex_to_id("e5d0542bd38431f105a8de8e982b3579647feb9f"); @@ -186,7 +186,7 @@ mod octopus { } #[test] - fn three_parallel_commits() -> Result { + fn three_parallel_commits() -> gix_testtools::TestResult { let odb = octopus_odb_at("three-parallel-commits")?; let mut graph = gix_revision::Graph::new(&odb, None); let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528"); @@ -206,7 +206,7 @@ mod octopus { } #[test] - fn three_forked_commits() -> Result { + fn three_forked_commits() -> gix_testtools::TestResult { let odb = octopus_odb_at("three-forked-commits")?; let mut graph = gix_revision::Graph::new(&odb, None); let base = hex_to_id("3ca3e3dd12585fabbef311d524a5e54678090528"); diff --git a/gix-status/src/fscache.rs b/gix-status/src/fscache.rs index ad04e1bcea0..b61dc09a434 100644 --- a/gix-status/src/fscache.rs +++ b/gix-status/src/fscache.rs @@ -316,7 +316,7 @@ mod windows { let dir = if rel_dir.is_empty() { worktree.to_owned() } else { - worktree.join(gix_path::from_bstr(rel_dir)) + worktree.join(gix_path::from_bstr(rel_dir).ok()?) }; let dir_path = utf16_null_terminated(&dir); #[expect(unsafe_code)] diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 27044e25cd7..3274678ae43 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -380,7 +380,7 @@ impl<'index> State<'_, 'index> { where Find: gix_object::Find, { - let worktree_path = match self.path_stack.verified_path(gix_path::from_bstr(rela_path).as_ref()) { + let worktree_path = match self.path_stack.verified_path(gix_path::from_bstr(rela_path)?.as_ref()) { Ok(path) => path, Err(err) if crate::stack::is_symlink_step_error(&err) => return Ok(Some(Change::Removed.into())), Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => { @@ -611,7 +611,7 @@ where let out = if is_symlink && self.core_symlinks { let symlink_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr( std::fs::read_link(self.path).map_err(gix_hash::io::from_std_io)?, - )); + )?); self.buf.extend_from_slice(&symlink_path); self.worktree_bytes.fetch_add(self.buf.len() as u64, Ordering::Relaxed); Stream { diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index b49ebc3737d..299e63ee507 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -536,7 +536,7 @@ pub(super) mod function { let platform = attrs.at_entry(rela_path, None, objects).or_raise(|| { message!("Failed to change the attribute context for worktree path {rela_path:?}") })?; - let rela_path = gix_path::from_bstr(rela_path); + let rela_path = gix_path::from_bstr(rela_path)?; let file_path = worktree_root.join(rela_path.as_ref()); let file = match std::fs::File::open(&file_path) { Ok(f) => f, @@ -598,11 +598,11 @@ pub(super) mod function { } } Kind::Symlink => { - let path = worktree_root.join(gix_path::from_bstr(rela_path)); + let path = worktree_root.join(gix_path::from_bstr(rela_path)?); let target = gix_path::into_bstr( std::fs::read_link(&path) .or_raise(|| message!("Could not read worktree link \"{}\"", path.display()))?, - ); + )?; gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &target) .or_raise(|| message!("Could not hash worktree link \"{}\"", path.display()))? } diff --git a/gix-status/src/stack.rs b/gix-status/src/stack.rs index 7f5da40d9ab..d2c05b8d69e 100644 --- a/gix-status/src/stack.rs +++ b/gix-status/src/stack.rs @@ -45,7 +45,8 @@ impl SymlinkCheck { /// to `relative_path`. Instead. /// For convenience, this incarnation is tuned to be easy to use with Git paths, i.e. slash-separated `BString` path. pub fn verified_path_allow_nonexisting(&mut self, relative_path: &BStr) -> std::io::Result<Cow<'_, Path>> { - let rela_path = gix_path::try_from_bstr(relative_path).map_err(std::io::Error::other)?; + let rela_path = gix_path::from_bstr(relative_path) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; if let Err(err) = self.verified_path(rela_path.as_ref()) { if err.kind() == std::io::ErrorKind::NotFound { Ok(Cow::Owned(self.inner.root().join(rela_path))) diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index 74d6075a1e1..1ae26dd15c2 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -1215,17 +1215,17 @@ fn modified() { } #[test] -fn racy_git() { +fn racy_git() -> gix_testtools::TestResult { let timestamp = 940040400; // we need a writable fixture because we have to mess with `mtimes` manually, because touch -d // respects the locale so the test wouldn't work depending on the timezone you // run your test in. - let dir = crate::scripted_fixture_writable("racy_git.sh").expect("script works"); + let dir = crate::scripted_fixture_writable("racy_git.sh")?; let worktree = dir.path(); let git_dir = worktree.join(".git"); let fs = gix_fs::Capabilities::probe(&git_dir); let object_hash = gix_testtools::object_hash(); - let mut index = gix_index::File::at(git_dir.join("index"), object_hash, false, Default::default()).unwrap(); + let mut index = gix_index::File::at(git_dir.join("index"), object_hash, false, Default::default())?; #[derive(Clone)] struct CountCalls(Arc<AtomicUsize>, FastEq); @@ -1254,8 +1254,7 @@ fn racy_git() { set_file_mtime( worktree.join("content"), FileTime::from_unix_time(i64::from(timestamp), 0), - ) - .expect("changing filetime works"); + )?; let mut recorder = Recorder::default(); let count = Arc::new(AtomicUsize::new(0)); @@ -1287,8 +1286,7 @@ fn racy_git() { stat: TEST_OPTIONS, ..Options::default() }, - ) - .unwrap(); + )?; assert_eq!( out, Outcome { @@ -1324,8 +1322,7 @@ fn racy_git() { stat: TEST_OPTIONS, ..Options::default() }, - ) - .unwrap(); + )?; assert_eq!( out, Outcome { @@ -1357,6 +1354,7 @@ fn racy_git() { )], "racy change is correctly detected" ); + Ok(()) } fn default_pathspec() -> gix_pathspec::Search { diff --git a/gix-status/tests/status/stack.rs b/gix-status/tests/status/stack.rs index da428e0ad5a..2c6637592e4 100644 --- a/gix-status/tests/status/stack.rs +++ b/gix-status/tests/status/stack.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; fn stack() -> gix_status::SymlinkCheck { stack_in("base") @@ -13,7 +13,7 @@ fn stack_in(dir: &str) -> gix_status::SymlinkCheck { } #[test] -fn paths_not_going_through_symlink_directories_are_ok_and_point_to_correct_item() -> Result { +fn paths_not_going_through_symlink_directories_are_ok_and_point_to_correct_item() -> TestResult { for root in ["base", "symlink-base"] { let mut stack = stack_in(root); for (rela_path, expectation) in [ @@ -36,14 +36,14 @@ fn paths_not_going_through_symlink_directories_are_ok_and_point_to_correct_item( } #[test] -fn leaf_file_does_not_have_to_exist() -> Result { +fn leaf_file_does_not_have_to_exist() -> TestResult { assert!(!stack().verified_path("dir/does-not-exist")?.exists()); Ok(()) } #[test] #[cfg(not(windows))] -fn intermediate_directories_have_to_exist_or_not_found_error() -> Result { +fn intermediate_directories_have_to_exist_or_not_found_error() -> TestResult { let err = stack() .verified_path("nonexisting-dir/file") .expect_err("the operation must fail"); @@ -54,7 +54,7 @@ fn intermediate_directories_have_to_exist_or_not_found_error() -> Result { #[test] #[cfg(windows)] -fn intermediate_directories_do_not_have_exist_for_success() -> Result { +fn intermediate_directories_do_not_have_exist_for_success() -> TestResult { assert!(stack().verified_path("nonexisting-dir/file").is_ok()); Ok(()) } diff --git a/gix-submodule/src/access.rs b/gix-submodule/src/access.rs index e4409e10cb3..304f6d81788 100644 --- a/gix-submodule/src/access.rs +++ b/gix-submodule/src/access.rs @@ -121,7 +121,7 @@ impl File { if path_bstr.is_empty() { bail!("The submodule '{name}' was missing its 'path' field or it was empty".validation()); } - let path = gix_path::from_bstr(path_bstr.as_bstr()); + let path = gix_path::from_bstr(path_bstr.as_bstr())?; if path.is_absolute() { bail!(validation(format!("The path of submodule '{name}' needs to be relative")).with_input(path_bstr)); } diff --git a/gix-submodule/tests/file/baseline.rs b/gix-submodule/tests/file/baseline.rs index 576f52841b2..d3a4d741a4a 100644 --- a/gix-submodule/tests/file/baseline.rs +++ b/gix-submodule/tests/file/baseline.rs @@ -1,10 +1,10 @@ -use crate::Result; +use gix_testtools::TestResult; use std::{ffi::OsStr, path::PathBuf}; use bstr::ByteSlice; #[test] -fn common_values_and_names_by_path() -> Result { +fn common_values_and_names_by_path() -> TestResult { let modules = module_files() .map(|(path, stripped)| { gix_submodule::File::from_bytes(&std::fs::read(path).unwrap(), stripped, &Default::default()) diff --git a/gix-submodule/tests/file/mod.rs b/gix-submodule/tests/file/mod.rs index 7cf4d559427..9b4ceefe149 100644 --- a/gix-submodule/tests/file/mod.rs +++ b/gix-submodule/tests/file/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; fn submodule(bytes: &str) -> gix_submodule::File { gix_submodule::File::from_bytes(bytes.as_bytes(), None, &Default::default()).expect("valid module") @@ -60,7 +60,7 @@ mod is_active_platform { } #[test] - fn without_submodule_in_index() -> Result { + fn without_submodule_in_index() -> gix_testtools::TestResult { let module = module_file("not-a-submodule")?; assert_eq!( module.names().map(ToOwned::to_owned).collect::<Vec<_>>(), @@ -71,7 +71,7 @@ mod is_active_platform { } #[test] - fn without_any_additional_settings_all_are_inactive_if_they_have_a_url() -> Result { + fn without_any_additional_settings_all_are_inactive_if_they_have_a_url() -> gix_testtools::TestResult { let module = multi_modules()?; assert_eq!( assume_valid_active_state(&module, &Default::default(), Default::default())?, @@ -87,7 +87,7 @@ mod is_active_platform { } #[test] - fn submodules_with_active_config_are_considered_active_or_inactive() -> crate::Result { + fn submodules_with_active_config_are_considered_active_or_inactive() -> gix_testtools::TestResult { let module = multi_modules()?; assert_eq!( assume_valid_active_state( @@ -109,7 +109,7 @@ mod is_active_platform { } #[test] - fn submodules_with_active_config_override_pathspecs() -> crate::Result { + fn submodules_with_active_config_override_pathspecs() -> gix_testtools::TestResult { let module = multi_modules()?; assert_eq!( assume_valid_active_state( @@ -131,7 +131,7 @@ mod is_active_platform { } #[test] - fn pathspecs_matter_even_if_they_do_not_match() -> crate::Result { + fn pathspecs_matter_even_if_they_do_not_match() -> gix_testtools::TestResult { let module = multi_modules()?; assert_eq!( assume_valid_active_state( @@ -175,7 +175,7 @@ mod path { } #[test] - fn valid() -> crate::Result { + fn valid() -> gix_testtools::TestResult { let module = submodule("[submodule.a]\n path = relative/path/submodule"); assert_eq!(module.path("a".into())?, "relative/path/submodule"); Ok(()) @@ -244,7 +244,7 @@ mod url { } #[test] - fn valid() -> crate::Result { + fn valid() -> gix_testtools::TestResult { let module = submodule("[submodule.a]\n url = path-to-repo"); assert_eq!(module.url("a".into())?.to_bstring(), "path-to-repo"); Ok(()) @@ -309,7 +309,7 @@ mod update { } #[test] - fn valid() -> crate::Result { + fn valid() -> gix_testtools::TestResult { for (valid, expected) in [ ("checkout", Update::Checkout), ("rebase", Update::Rebase), @@ -323,13 +323,11 @@ mod update { } #[test] - fn valid_in_overrides() -> crate::Result { + fn valid_in_overrides() -> gix_testtools::TestResult { let mut module = submodule("[submodule.a]\n update = merge"); let repo_config = gix_config::File::from_str("[submodule.a]\n update = !dangerous")?; let prev_names = module.names().map(ToOwned::to_owned).collect::<Vec<_>>(); - module - .append_submodule_overrides(&repo_config) - .expect("the fixture fits into the backing buffer"); + module.append_submodule_overrides(&repo_config)?; assert_eq!( module.update("a".into())?.expect("present"), @@ -374,7 +372,7 @@ mod update { /// `update` value makes `File::update()` treat the command as trusted and expose it as /// `Update::Command`. #[test] - fn modules_command_is_authorized_by_unrelated_same_named_override() -> crate::Result { + fn modules_command_is_authorized_by_unrelated_same_named_override() -> gix_testtools::TestResult { let mut module = submodule("[submodule.a]\n update = !dangerous"); let repo_config = gix_config::File::from_str("[submodule.a]\n url = trusted-local-override")?; module @@ -415,7 +413,7 @@ mod fetch_recurse { } #[test] - fn valid() -> crate::Result { + fn valid() -> gix_testtools::TestResult { for (valid, expected) in [ ("yes", FetchRecurse::Always), ("true", FetchRecurse::Always), @@ -437,7 +435,7 @@ mod fetch_recurse { } #[test] - fn validate_upon_retrieval() -> crate::Result { + fn validate_upon_retrieval() -> gix_testtools::TestResult { for invalid in ["foo", "ney", "On-demand"] { let module = submodule(&format!("[submodule.a]\n fetchRecurseSubmodules = \"{invalid}\"")); assert!(module.fetch_recurse("a".into()).is_err()); @@ -447,8 +445,8 @@ mod fetch_recurse { } mod ignore { - use crate::Result; use gix_submodule::config::Ignore; + use gix_testtools::TestResult; use crate::file::submodule; @@ -458,7 +456,7 @@ mod ignore { } #[test] - fn valid() -> Result { + fn valid() -> TestResult { for (valid, expected) in [ ("all", Ignore::All), ("dirty", Ignore::Dirty), @@ -477,7 +475,7 @@ mod ignore { } #[test] - fn validate_upon_retrieval() -> Result { + fn validate_upon_retrieval() -> TestResult { for invalid in ["All", ""] { let module = submodule(&format!("[submodule.a]\n ignore = \"{invalid}\"")); assert!(module.ignore("a".into()).is_err()); @@ -487,13 +485,13 @@ mod ignore { } mod branch { - use crate::Result; use gix_submodule::config::Branch; + use gix_testtools::TestResult; use crate::file::submodule; #[test] - fn valid() -> Result { + fn valid() -> TestResult { for (valid, expected) in [ (".", Branch::CurrentInSuperproject), ("", Branch::Name("HEAD".into())), @@ -516,7 +514,7 @@ mod branch { } #[test] - fn validate_upon_retrieval() -> Result { + fn validate_upon_retrieval() -> TestResult { let module = submodule("[submodule.a]\n branch = /invalid"); assert!(module.branch("a".into()).is_err()); Ok(()) @@ -524,7 +522,7 @@ mod branch { } #[test] -fn shallow() -> Result { +fn shallow() -> TestResult { let module = submodule("[submodule.a]\n shallow"); assert_eq!( module.shallow("a".into())?, @@ -535,13 +533,13 @@ fn shallow() -> Result { } mod append_submodule_overrides { - use crate::Result; + use gix_testtools::TestResult; use std::str::FromStr; use crate::file::submodule; #[test] - fn overrides_preserve_their_section_metadata() -> Result { + fn overrides_preserve_their_section_metadata() -> TestResult { use gix_config::{Source, file::Metadata}; let mut module = submodule( @@ -581,14 +579,12 @@ mod append_submodule_overrides { } #[test] - fn last_of_multiple_values_wins() -> Result { + fn last_of_multiple_values_wins() -> TestResult { let mut module = submodule("[submodule.a] url = from-module"); let repo_config = gix_config::File::from_str( "[submodule.a]\n url = a\n url = b\n ignore = x\n [submodule.a]\n url = c\n[submodule.b] url = not-relevant", )?; - module - .append_submodule_overrides(&repo_config) - .expect("the fixture fits into the backing buffer"); + module.append_submodule_overrides(&repo_config)?; Ok(()) } } diff --git a/gix-tempfile/src/signal.rs b/gix-tempfile/src/signal.rs index d55844973aa..c1dfbedbef2 100644 --- a/gix-tempfile/src/signal.rs +++ b/gix-tempfile/src/signal.rs @@ -78,7 +78,7 @@ pub mod handler { } #[test] - fn various_termination_signals_remove_tempfiles_unconditionally() -> Result<(), Box<dyn std::error::Error>> { + fn various_termination_signals_remove_tempfiles_unconditionally() -> gix_testtools::TestResult { crate::signal::setup(Default::default()); let dir = tempfile::tempdir()?; for sig in signal_hook::consts::TERM_SIGNALS { diff --git a/gix-tempfile/tests/all.rs b/gix-tempfile/tests/all.rs index 6332850b3aa..7d0a18af1d8 100644 --- a/gix-tempfile/tests/all.rs +++ b/gix-tempfile/tests/all.rs @@ -1,3 +1,3 @@ -type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; +use gix_testtools::TestResult; mod tempfile; diff --git a/gix-tempfile/tests/registry.rs b/gix-tempfile/tests/registry.rs index beb0f96d057..a066f67207b 100644 --- a/gix-tempfile/tests/registry.rs +++ b/gix-tempfile/tests/registry.rs @@ -8,7 +8,7 @@ fn filecount_in(path: impl AsRef<Path>) -> usize { } #[test] -fn cleanup_tempfiles() -> Result<(), Box<dyn std::error::Error>> { +fn cleanup_tempfiles() -> gix_testtools::TestResult { let dir = tempfile::tempdir()?; let mut tempfile = gix_tempfile::new(dir.path(), ContainingDirectory::Exists, AutoRemove::Tempfile)?; assert_eq!( diff --git a/gix-tempfile/tests/tempfile/handle.rs b/gix-tempfile/tests/tempfile/handle.rs index ee393034a38..b51fa0ce33d 100644 --- a/gix-tempfile/tests/tempfile/handle.rs +++ b/gix-tempfile/tests/tempfile/handle.rs @@ -1,9 +1,9 @@ mod mark_path { - use crate::Result; + use crate::TestResult; use gix_tempfile::{AutoRemove, ContainingDirectory}; #[test] - fn it_persists_markers_along_with_newly_created_directories() -> Result { + fn it_persists_markers_along_with_newly_created_directories() -> TestResult { let dir = tempfile::tempdir()?; let target = dir.path().join("a").join("b").join("file.tmp"); let new_filename = target.parent().unwrap().join("file.ext"); @@ -39,7 +39,7 @@ mod mark_path { } #[test] - fn it_can_create_the_containing_directory_and_remove_it_on_drop() -> Result { + fn it_can_create_the_containing_directory_and_remove_it_on_drop() -> TestResult { let dir = tempfile::tempdir()?; let first_dir = "dir"; let filename = dir.path().join(first_dir).join("subdir").join("file.tmp"); @@ -67,11 +67,11 @@ mod mark_path { } } mod at_path { - use crate::Result; + use crate::TestResult; use gix_tempfile::{AutoRemove, ContainingDirectory}; #[test] - fn reduce_resource_usage_by_converting_files_to_markers_and_persist_them() -> Result { + fn reduce_resource_usage_by_converting_files_to_markers_and_persist_them() -> TestResult { let dir = tempfile::tempdir()?; let target = dir.path().join("a").join("file.tmp"); let new_filename = target.parent().unwrap().join("file.ext"); @@ -103,7 +103,7 @@ mod at_path { use std::io::{ErrorKind, Write}; #[test] - fn it_persists_tempfiles_along_with_newly_created_directories() -> Result { + fn it_persists_tempfiles_along_with_newly_created_directories() -> TestResult { let dir = tempfile::tempdir()?; let target = dir.path().join("a").join("b").join("file.tmp"); let new_filename = target.parent().unwrap().join("file.ext"); @@ -150,7 +150,7 @@ mod at_path { #[test] #[cfg(windows)] - fn persistence_replaces_readonly_files_and_retains_the_tempfiles_permissions() -> Result { + fn persistence_replaces_readonly_files_and_retains_the_tempfiles_permissions() -> TestResult { let dir = tempfile::tempdir()?; let tempfile_path = dir.path().join("file.lock"); let destination = dir.path().join("file"); @@ -179,7 +179,7 @@ mod at_path { } #[test] - fn it_can_create_the_containing_directory_and_remove_it_on_drop() -> Result { + fn it_can_create_the_containing_directory_and_remove_it_on_drop() -> TestResult { let dir = tempfile::tempdir()?; let first_dir = "dir"; let filename = dir.path().join(first_dir).join("subdir").join("file.tmp"); @@ -208,7 +208,7 @@ mod at_path { } #[test] - fn it_names_files_correctly_and_similarly_named_tempfiles_cannot_be_created() -> Result { + fn it_names_files_correctly_and_similarly_named_tempfiles_cannot_be_created() -> TestResult { let dir = tempfile::tempdir()?; let filename = dir.path().join("something-specific.ext"); let tempfile = gix_tempfile::writable_at(&filename, ContainingDirectory::Exists, AutoRemove::Tempfile)?; @@ -236,7 +236,7 @@ mod at_path { } mod new { - use crate::Result; + use crate::TestResult; use std::{ io::{ErrorKind, Write}, path::Path, @@ -249,7 +249,7 @@ mod new { } #[test] - fn it_can_be_kept() -> Result { + fn it_can_be_kept() -> TestResult { let dir = tempfile::tempdir()?; drop( gix_tempfile::new(dir.path(), ContainingDirectory::Exists, AutoRemove::Tempfile)? @@ -262,7 +262,7 @@ mod new { } #[test] - fn it_is_removed_if_it_goes_out_of_scope() -> Result { + fn it_is_removed_if_it_goes_out_of_scope() -> TestResult { let dir = tempfile::tempdir()?; { let _keep = gix_tempfile::new(dir.path(), ContainingDirectory::Exists, AutoRemove::Tempfile)?; @@ -273,7 +273,7 @@ mod new { } #[test] - fn it_can_create_the_containing_directory_and_remove_it_when_dropped() -> Result { + fn it_can_create_the_containing_directory_and_remove_it_when_dropped() -> TestResult { let dir = tempfile::tempdir()?; let containing_dir = dir.path().join("dir"); assert!(!containing_dir.exists()); @@ -303,9 +303,7 @@ mod new { ) "); assert_eq!(err.kind(), ErrorKind::Other, "errors are propagated"); - writable - .with_mut(|tf| assert!(tf.path().is_file())) - .expect("after seeing an error before the file still exists"); + writable.with_mut(|tf| assert!(tf.path().is_file()))?; } assert!(!containing_dir.is_dir(), "the now empty directory was deleted as well"); assert!(dir.path().is_dir(), "it won't touch the containing directory"); diff --git a/gix-tix/spec.md b/gix-tix/spec.md index 31f1d145470..87b5e9c5b18 100644 --- a/gix-tix/spec.md +++ b/gix-tix/spec.md @@ -262,7 +262,9 @@ without trading responsiveness for metadata that is not visible. option may be repeated. - `-h/--help` prints Clap's standard help for `tix` and every subcommand. - Diagnostics retain underlying causes when adding command or argument context, - including encoding failures when OS-string conversions fail. + including encoding failures when OS-string conversions fail. Git paths, + reference names, and configured commands that cannot be represented natively + produce errors before their associated command is launched. - `--quit-on-finish[=INPUTS]` exits after traversal, lane computation, and one completed frame, for measurement and non-interactive inspection. Optional characters are replayed as read-only keyboard input before the retained final @@ -2279,6 +2281,8 @@ views. - The worktree watcher exists only while the combined worktree block is enabled. It observes the index and ignore-aware directories that Git status would walk, using non-recursive registrations so ignored build trees do not generate work. + Unrepresentable directory or index paths fail the refresh before updating + registrations or the saved index projection; partial directory sets are not used. - Access-only and incomplete `.lock` activity are ignored. Completed atomic renames, index/HEAD updates, relevant worktree paths, and backend rescan requests invalidate the appropriate cache. diff --git a/gix-tix/src/edit/create.rs b/gix-tix/src/edit/create.rs index 7ec815912d2..3df465c86be 100644 --- a/gix-tix/src/edit/create.rs +++ b/gix-tix/src/edit/create.rs @@ -209,7 +209,7 @@ fn prepare_inner( document.push(b'\n'); } reword::write_missing_agent_trailers(&mut document, &repo, &initial_message)?; - reword::write_config_source(&mut document, MESSAGE_KEY, source); + reword::write_config_source(&mut document, MESSAGE_KEY, source)?; document.extend_from_slice(b"\n; Changes to be committed:\n"); reword::write_diff_summary(&mut document, &repo, changes)?; drop(new_tree); @@ -630,7 +630,7 @@ mod tests { fn configured_messages_prefill_normal_and_empty_commits() -> gix_testtools::Result { let fixture = gix_testtools::scripted_fixture_writable("create_commit.sh")?; let mut source = b"; tix.new.message is configured in ".to_vec(); - source.extend_from_slice(gix::path::into_bstr(fixture.path().join(".git").join("config")).as_ref()); + source.extend_from_slice(gix::path::into_bstr(fixture.path().join(".git").join("config"))?.as_ref()); source.extend_from_slice(b".\n"); for (message, expected) in [ ("RĂŠsumĂŠ\n\nExplain why.\n", "RĂŠsumĂŠ\n\nExplain why.\n"), diff --git a/gix-tix/src/edit/discard.rs b/gix-tix/src/edit/discard.rs index 38d1d3a4542..84bb4cce69e 100644 --- a/gix-tix/src/edit/discard.rs +++ b/gix-tix/src/edit/discard.rs @@ -35,11 +35,15 @@ pub(crate) fn perform(repo: &gix::Repository, change: &PathChange) -> Result<()> "discarding submodule changes is not supported" ); let run = |args: &[&str], paths: &[&BStr]| -> Result<()> { + let paths = paths + .iter() + .map(|path| gix::path::from_byte_slice(path.as_bytes())) + .collect::<Result<Vec<_>>>()?; let output = crate::git_command(workdir) .arg("--literal-pathspecs") .args(args) .arg("--") - .args(paths.iter().map(|path| gix::path::from_byte_slice(path.as_bytes()))) + .args(paths) .output() .or_raise(|| message("could not launch Git to discard the selected change"))?; gix::error::ensure!(output.status.success(), "{}", output.stderr.trim().to_str_lossy()); @@ -334,7 +338,7 @@ mod tests { let selected = BStr::new(b"file-\xff"); let other = BStr::new(b"file-\xfe"); for name in [selected, other] { - std::fs::write(path.join(gix::path::from_bstr(name)), "contents\n")?; + std::fs::write(path.join(gix::path::from_bstr(name)?), "contents\n")?; } let repo = crate::test_repository::open(path)?; let changes = crate::load_worktree_changes_without_lines(&repo, gix::status::UntrackedFiles::Collapsed)?; @@ -346,8 +350,8 @@ mod tests { perform(&repo, change)?; - assert!(!path.join(gix::path::from_bstr(selected)).exists()); - assert_eq!(std::fs::read(path.join(gix::path::from_bstr(other)))?, b"contents\n"); + assert!(!path.join(gix::path::from_bstr(selected)?).exists()); + assert_eq!(std::fs::read(path.join(gix::path::from_bstr(other)?))?, b"contents\n"); Ok(()) } } diff --git a/gix-tix/src/edit/mod.rs b/gix-tix/src/edit/mod.rs index be036eb12ff..f5847de3040 100644 --- a/gix-tix/src/edit/mod.rs +++ b/gix-tix/src/edit/mod.rs @@ -32,7 +32,7 @@ pub(super) fn loaded_graph(repo: &gix::Repository) -> Result<crate::history::His continue; } revisions.push( - gix::path::from_bstr(reference.name().as_bstr()) + gix::path::from_bstr(reference.name().as_bstr())? .into_owned() .into_os_string(), ); @@ -139,7 +139,7 @@ pub(crate) fn edit_document_without_terminal( .or_raise(|| message("could not close commit message file"))?; let editor_display = editor.command.to_string_lossy().into_owned(); - let mut command = Command::from(editor.arg(&path)); + let mut command = Command::try_from(editor.arg(&path))?; let editor_display = if editor_display == command.get_program().to_string_lossy() { String::new() } else { diff --git a/gix-tix/src/edit/replay_refs/mod.rs b/gix-tix/src/edit/replay_refs/mod.rs index 21e23ff0876..5fd77768641 100644 --- a/gix-tix/src/edit/replay_refs/mod.rs +++ b/gix-tix/src/edit/replay_refs/mod.rs @@ -359,7 +359,9 @@ fn canonical_name(repo: &gix::Repository, name: &FullNameRef) -> Result<FullName return Ok( match repo .worktree() - .and_then(|worktree| worktree.id().map(ToOwned::to_owned)) + .map(|worktree| worktree.id().map(|id| id.map(ToOwned::to_owned))) + .transpose()? + .flatten() { Some(worktree) => Category::LinkedPseudoRef { name: worktree.as_bstr(), diff --git a/gix-tix/src/edit/reword.rs b/gix-tix/src/edit/reword.rs index 7fde1edf264..33243878b0f 100644 --- a/gix-tix/src/edit/reword.rs +++ b/gix-tix/src/edit/reword.rs @@ -196,18 +196,22 @@ pub(super) fn write_missing_agent_trailers(out: &mut Vec<u8>, repo: &gix::Reposi out.push(b'\n'); } for trailer in &trailers { - write_config_source(out, trailer.key, trailer.source.as_ref()); + write_config_source(out, trailer.key, trailer.source.as_ref())?; } Ok(()) } -pub(super) fn write_config_source(out: &mut Vec<u8>, key: &str, source: Option<&gix::config::file::Metadata>) { +pub(super) fn write_config_source( + out: &mut Vec<u8>, + key: &str, + source: Option<&gix::config::file::Metadata>, +) -> Result<()> { out.extend_from_slice(b"; "); out.extend_from_slice(key.as_bytes()); if let Some(source) = source { if let Some(path) = &source.path { out.extend_from_slice(b" is configured in "); - out.extend_from_slice(gix::path::into_bstr(path).as_ref()); + out.extend_from_slice(gix::path::into_bstr(path)?.as_ref()); } else { out.extend_from_slice(b" is configured via "); out.extend_from_slice(config_source_name(source.source)); @@ -216,6 +220,7 @@ pub(super) fn write_config_source(out: &mut Vec<u8>, key: &str, source: Option<& out.extend_from_slice(b" is unset; using the built-in default"); } out.extend_from_slice(b".\n"); + Ok(()) } fn agent_trailer( @@ -846,7 +851,7 @@ mod tests { document.as_bstr() ); let mut configured = b"; tix.trailer.assistedBy is configured in ".to_vec(); - configured.extend_from_slice(gix::path::into_bstr(fixture.path().join(".git").join("config")).as_ref()); + configured.extend_from_slice(gix::path::into_bstr(fixture.path().join(".git").join("config"))?.as_ref()); configured.extend_from_slice(b".\n"); assert!( document.windows(configured.len()).any(|window| window == configured), diff --git a/gix-tix/src/edit/time_travel.rs b/gix-tix/src/edit/time_travel.rs index 6f65930cd4a..c6866977d2e 100644 --- a/gix-tix/src/edit/time_travel.rs +++ b/gix-tix/src/edit/time_travel.rs @@ -704,7 +704,7 @@ pub(super) fn ensure_branch_is_available(repository: &gix::Repository, branch: & let current = repository .worktree() .ok_or_raise(|| message("attaching requires a current worktree"))?; - let current_id = current.id().map(ToOwned::to_owned); + let current_id = current.id()?.map(ToOwned::to_owned); if current_id.is_some() { ensure_worktree_does_not_own_branch( repository @@ -717,9 +717,10 @@ pub(super) fn ensure_branch_is_available(repository: &gix::Repository, branch: & .worktrees() .or_raise(|| message("could not enumerate worktrees while checking the remembered branch"))? { + let proxy_id = proxy.id()?; if current_id .as_ref() - .is_some_and(|current| current.as_slice() == proxy.id().as_bytes()) + .is_some_and(|current| current.as_slice() == proxy_id.as_bytes()) { continue; } @@ -1506,7 +1507,7 @@ fn checkout_branch(workdir: &Path, name: &gix::refs::FullNameRef) -> Result<()> workdir, [ OsString::from("--no-guess"), - gix::path::from_bstr(branch.as_bstr()).into_owned().into_os_string(), + gix::path::from_bstr(branch.as_bstr())?.into_owned().into_os_string(), ], ) } diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index b47713715e4..7fc0afc1f64 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -1522,7 +1522,7 @@ pub(crate) fn ref_tree_revisions(repo: &gix::Repository, include_tags: bool) -> if repo.find_header(id)?.kind() != gix::object::Kind::Commit { continue; } - out.push(gix::path::from_bstr(&name).into_owned().into_os_string()); + out.push(gix::path::from_bstr(&name)?.into_owned().into_os_string()); } if repo.head().is_ok_and(|head| head.referent_name().is_none()) { out.push("HEAD".into()); @@ -1560,7 +1560,7 @@ fn snapshot_inner( .filter(|pin| ignored_pin != Some(pin.name.as_bstr())) .collect::<Vec<_>>(); let worktrees = if collect_worktrees { - worktree_checkouts(repo) + worktree_checkouts(repo)? } else { Vec::new() }; @@ -1593,9 +1593,12 @@ fn snapshot_inner( }) } -pub(crate) fn worktree_checkouts(repo: &gix::Repository) -> Vec<WorktreeCheckout> { +pub(crate) fn worktree_checkouts(repo: &gix::Repository) -> Result<Vec<WorktreeCheckout>> { let mut out = Vec::new(); - let current_worktree = repo.worktree().map(|worktree| worktree.id().map(ToOwned::to_owned)); + let current_worktree = repo + .worktree() + .map(|worktree| worktree.id().map(|id| id.map(ToOwned::to_owned))) + .transpose()?; match repo.main_repo() { Ok(main) if !main.is_bare() => { let name = main.workdir().and_then(worktree_basename); @@ -1607,7 +1610,7 @@ pub(crate) fn worktree_checkouts(repo: &gix::Repository) -> Vec<WorktreeCheckout match repo.worktrees() { Ok(worktrees) => { for proxy in worktrees { - let worktree = proxy.id().to_owned(); + let worktree = proxy.id()?.to_owned(); let name = proxy.base().ok().as_deref().and_then(worktree_basename); let is_current = current_worktree .as_ref() @@ -1632,7 +1635,7 @@ pub(crate) fn worktree_checkouts(repo: &gix::Repository) -> Vec<WorktreeCheckout .then_with(|| a.reference.cmp(&b.reference)) }); out.dedup(); - out + Ok(out) } fn add_worktree_checkout( @@ -2112,10 +2115,10 @@ fn auto_hidden_revisions(repo: &gix::Repository) -> Result<Vec<OsString>> { branches.insert(upstream); } } - Ok(branches + branches .into_iter() - .map(|name| gix::path::from_bstr(name.as_bstr()).into_owned().into_os_string()) - .collect()) + .map(|name| Ok(gix::path::from_bstr(name.as_bstr())?.into_owned().into_os_string())) + .collect() } fn attribution_kind(trailer: &gix::objs::commit::message::body::TrailerRef<'_>) -> Option<AttributionKind> { @@ -2846,7 +2849,7 @@ mod tests { let topic = repo.rev_parse_single("topic")?.detach(); let root = repo.rev_parse_single("main~2")?.detach(); let remembered = repo.rev_parse_single("remembered")?.detach(); - let worktrees = worktree_checkouts(&repo); + let worktrees = worktree_checkouts(&repo)?; assert!(worktrees.iter().any(|worktree| { worktree.id == main && worktree.label_id == main @@ -2935,7 +2938,7 @@ mod tests { let linked_path = fixture.path().join("topic-wt"); let linked_repo = crate::test_repository::open(&linked_path)?; - let linked_worktrees = worktree_checkouts(&linked_repo); + let linked_worktrees = worktree_checkouts(&linked_repo)?; assert!(linked_worktrees.iter().any(|worktree| worktree.id == topic && worktree.is_current && worktree.head_reference == "worktrees/topic-wt/HEAD")); @@ -2964,7 +2967,7 @@ mod tests { .status()?; assert!(status.success(), "git detaches the current linked worktree"); let detached_repo = crate::test_repository::open(&linked_path)?; - let detached_worktrees = worktree_checkouts(&detached_repo); + let detached_worktrees = worktree_checkouts(&detached_repo)?; let current = detached_worktrees .iter() .find(|worktree| worktree.is_current) @@ -2988,7 +2991,7 @@ mod tests { .status()?; assert!(symbolic.success(), "git remembers the detached worktree's branch"); let remembered_repo = crate::test_repository::open(&linked_path)?; - let remembered_worktrees = worktree_checkouts(&remembered_repo); + let remembered_worktrees = worktree_checkouts(&remembered_repo)?; let current = remembered_worktrees .iter() .find(|worktree| worktree.is_current) @@ -4124,7 +4127,7 @@ mod tests { !snapshot(&repo, &[], &[], false)?.view_tips.contains(&review), "review resources do not retain history" ); - let decorations = decorations(&repo, &[], &worktree_checkouts(&repo))?; + let decorations = decorations(&repo, &[], &worktree_checkouts(&repo)?)?; assert!( decorations.get(&review).is_some_and(|decorations| decorations .iter() @@ -4155,7 +4158,7 @@ mod tests { )?; assert!(!snapshot(&repo, &[], &[], false)?.view_tips.contains(&stash)); - let decorations = decorations(&repo, &[], &worktree_checkouts(&repo))?; + let decorations = decorations(&repo, &[], &worktree_checkouts(&repo)?)?; assert!( decorations.get(&head).is_some_and(|decorations| decorations .iter() diff --git a/gix-tix/src/lib.rs b/gix-tix/src/lib.rs index 5d065ce5215..e410fca4a26 100644 --- a/gix-tix/src/lib.rs +++ b/gix-tix/src/lib.rs @@ -320,6 +320,7 @@ impl RefWatcher { struct WorktreeDirectories { root: PathBuf, paths: HashSet<PathBuf>, + error: Option<gix::Error>, } impl gix::dir::walk::Delegate for WorktreeDirectories { @@ -328,7 +329,11 @@ impl gix::dir::walk::Delegate for WorktreeDirectories { _entry: gix::dir::EntryRef<'_>, _collapsed_directory_status: Option<gix::dir::entry::Status>, ) -> gix::dir::walk::Action { - std::ops::ControlFlow::Continue(()) + if self.error.is_some() { + std::ops::ControlFlow::Break(()) + } else { + std::ops::ControlFlow::Continue(()) + } } fn can_recurse( @@ -337,6 +342,9 @@ impl gix::dir::walk::Delegate for WorktreeDirectories { for_deletion: Option<gix::dir::walk::ForDeletionMode>, worktree_root_is_repository: bool, ) -> bool { + if self.error.is_some() { + return false; + } let recurse = entry.status.can_recurse( entry.disk_kind, entry.pathspec_match, @@ -344,8 +352,15 @@ impl gix::dir::walk::Delegate for WorktreeDirectories { worktree_root_is_repository, ); if recurse { - self.paths - .insert(self.root.join(gix::path::from_bstr(entry.rela_path.as_ref()))); + match gix::path::from_bstr(entry.rela_path.as_ref()) { + Ok(path) => { + self.paths.insert(self.root.join(path)); + } + Err(err) => { + self.error = Some(err); + return false; + } + } } recurse } @@ -401,7 +416,7 @@ fn worktree_status_event_scopes( { return None; } - let relative = gix::path::try_into_bstr(relative).ok()?; + let relative = gix::path::into_bstr(relative).ok()?; out.push(gix::path::to_unix_separators_on_windows(relative).into_owned()); } (!out.is_empty()).then_some(out) @@ -5623,7 +5638,7 @@ fn push_branch( }; locks.push( gix::lock::Marker::acquire_to_hold_resource( - directory.join(gix::path::from_bstr(relative)), + directory.join(gix::path::from_bstr(relative)?), gix::lock::acquire::Fail::Immediately, Some(directory.to_owned()), 0, @@ -5658,8 +5673,8 @@ fn push_branch( } let output = command .arg("--") - .arg(gix::path::from_bstr(remote).as_ref()) - .arg(gix::path::from_bstr(branch).as_ref()) + .arg(gix::path::from_bstr(remote)?.as_ref()) + .arg(gix::path::from_bstr(branch)?.as_ref()) .env("GIT_TERMINAL_PROMPT", "0") .stdin(Stdio::null()) .stdout(Stdio::piped()) @@ -6065,10 +6080,13 @@ fn worktree_watch_directories_with_index( let mut directories = WorktreeDirectories { root: root.clone(), paths: HashSet::from([root]), + error: None, }; - repository - .dirwalk(index, None::<&str>, &AtomicBool::default(), options, &mut directories) - .or_raise(|| message("could not enumerate worktree directories"))?; + let result = repository.dirwalk(index, None::<&str>, &AtomicBool::default(), options, &mut directories); + if let Some(err) = directories.error { + return Err(err.and_raise(message("could not represent a directory path for worktree watching"))); + } + result.or_raise(|| message("could not enumerate worktree directories"))?; Ok(directories.paths) } @@ -6090,16 +6108,17 @@ fn changed_index_watch_scopes( before: &[IndexWatchEntry], after: &[IndexWatchEntry], workdir: &Path, -) -> HashSet<PathBuf> { - fn add_scope(entry: &IndexWatchEntry, workdir: &Path, out: &mut HashSet<PathBuf>) { +) -> Result<HashSet<PathBuf>> { + fn add_scope(entry: &IndexWatchEntry, workdir: &Path, out: &mut HashSet<PathBuf>) -> Result<()> { let path = entry.path.as_bstr(); let scope = path .find_byte(b'/') .map(|pos| &path[..pos]) .or_else(|| (entry.mode == gix::index::entry::Mode::DIR.bits()).then_some(path.as_ref())); if let Some(scope) = scope { - out.insert(workdir.join(gix::path::from_bstr(scope))); + out.insert(workdir.join(gix::path::from_bstr(scope)?)); } + Ok(()) } let mut out = HashSet::new(); @@ -6108,11 +6127,11 @@ fn changed_index_watch_scopes( match (before.get(left), after.get(right)) { (Some(a), Some(b)) => match a.cmp(b) { std::cmp::Ordering::Less => { - add_scope(a, workdir, &mut out); + add_scope(a, workdir, &mut out)?; left += 1; } std::cmp::Ordering::Greater => { - add_scope(b, workdir, &mut out); + add_scope(b, workdir, &mut out)?; right += 1; } std::cmp::Ordering::Equal => { @@ -6121,17 +6140,17 @@ fn changed_index_watch_scopes( } }, (Some(a), None) => { - add_scope(a, workdir, &mut out); + add_scope(a, workdir, &mut out)?; left += 1; } (None, Some(b)) => { - add_scope(b, workdir, &mut out); + add_scope(b, workdir, &mut out)?; right += 1; } (None, None) => break, } } - out + Ok(out) } fn minimize_worktree_scopes(scopes: HashSet<PathBuf>) -> Vec<PathBuf> { @@ -6169,7 +6188,7 @@ fn reconcile_worktree_watcher( &watcher.index_projection, &next_projection, &watcher.workdir, - )); + )?); } if !refresh.full && refresh.scopes.is_empty() { if update_projection { @@ -7475,7 +7494,7 @@ fn prepare_file_diff_content( } let global_command = repository .config_snapshot() - .trusted_program(gix::config::tree::Diff::EXTERNAL) + .trusted_program(gix::config::tree::Diff::EXTERNAL)? .map(gix::path::os_string_into_bstring) .transpose() .or_raise(|| message("external diff command is not representable on this platform"))?; @@ -7556,7 +7575,7 @@ fn prepare_file_diff_content( } fn prepare_pager(repository: &gix::Repository, diff: BuiltInDiff) -> Result<FileDiff> { - let Some(program) = repository.config_snapshot().trusted_program("core.pager") else { + let Some(program) = repository.config_snapshot().trusted_program("core.pager")? else { return Ok(FileDiff::BuiltIn(diff)); }; if program.is_empty() || program == "cat" { @@ -7573,7 +7592,7 @@ fn prepare_pager(repository: &gix::Repository, diff: BuiltInDiff) -> Result<File .stdin(Stdio::piped()) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) - .into(); + .try_into()?; Ok(FileDiff::Pager { command, diff }) } @@ -7996,7 +8015,7 @@ fn stage_resolved_conflict_paths(repository: &gix::Repository) -> Result<()> { let mut command = git_command(workdir); command.arg("--literal-pathspecs").args(["add", "-A", "--"]); for path in &paths { - command.arg(gix::path::from_bstr(path.as_bstr()).as_ref()); + command.arg(gix::path::from_bstr(path.as_bstr())?.as_ref()); } let output = command .output() @@ -8474,16 +8493,19 @@ fn push_remote_deletions(repository_path: &Path, groups: &[ref_tree::RemoteDelet failures: Vec::new(), }; for group in groups { - let mut command = git_command(repository_path); - command - .arg("push") - .arg(gix::path::from_bstr(group.remote.as_bstr()).as_ref()); - for reference in &group.references { - let mut refspec = b":".to_vec(); - refspec.extend_from_slice(reference.as_bstr()); - command.arg(gix::path::from_bstr(refspec.as_bstr()).as_ref()); - } - match command.status() { + let result = (|| -> Result<ExitStatus> { + let mut command = git_command(repository_path); + command + .arg("push") + .arg(gix::path::from_bstr(group.remote.as_bstr())?.as_ref()); + for reference in &group.references { + let mut refspec = b":".to_vec(); + refspec.extend_from_slice(reference.as_bstr()); + command.arg(gix::path::from_bstr(refspec.as_bstr())?.as_ref()); + } + command.status().or_error() + })(); + match result { Ok(status) if status.success() => outcome.deleted += group.references.len(), Ok(status) => outcome.failures.push(format!("{} exited with {status}", group.remote)), Err(err) => outcome.failures.push(format!("{}: {err}", group.remote)), @@ -10952,7 +10974,7 @@ mod tests { matches!( push_branch( repository.git_dir(), - gix::path::into_bstr(remote.path()).as_ref(), + gix::path::into_bstr(remote.path())?.as_ref(), branch.into(), &hidden_tips, force_with_lease, @@ -11009,7 +11031,7 @@ mod tests { matches!( push_branch( repository.git_dir(), - gix::path::into_bstr(remote.path()).as_ref(), + gix::path::into_bstr(remote.path())?.as_ref(), branch.into(), &unfinished.parents, false, @@ -11090,7 +11112,7 @@ mod tests { matches!( push_branch( repository.git_dir(), - gix::path::into_bstr(remote.path()).as_ref(), + gix::path::into_bstr(remote.path())?.as_ref(), branch.as_str().into(), &original.parents, force_with_lease, @@ -14726,7 +14748,7 @@ mod tests { let index = repository.index_or_empty()?; let after_path = index_watch_projection(&index); assert_eq!( - changed_index_watch_scopes(&after_content, &after_path, root), + changed_index_watch_scopes(&after_content, &after_path, root)?, HashSet::from([root.join("new")]), "index topology changes identify the affected top-level directory" ); diff --git a/gix-tix/src/ref_tree.rs b/gix-tix/src/ref_tree.rs index 2cf1aa7023e..5963df8b1fb 100644 --- a/gix-tix/src/ref_tree.rs +++ b/gix-tix/src/ref_tree.rs @@ -914,7 +914,7 @@ pub(crate) fn pin_references_reporting( } if kinds.contains(&DecorationKind::WorktreeDetached) { names.extend( - crate::history::worktree_checkouts(repository) + crate::history::worktree_checkouts(repository)? .into_iter() .filter(|worktree| !worktree.is_current && worktree.is_detached && worktree.id == id) .map(|worktree| worktree.head_reference), diff --git a/gix-tix/src/worktrunk/mod.rs b/gix-tix/src/worktrunk/mod.rs index 87004b57c1e..97ecc2edab3 100644 --- a/gix-tix/src/worktrunk/mod.rs +++ b/gix-tix/src/worktrunk/mod.rs @@ -484,7 +484,8 @@ impl Drop for Worktrees { fn inventory(repository: &gix::Repository) -> Result<Vec<Row>> { let current = repository .worktree() - .map(|worktree| worktree.id().map(ToOwned::to_owned)); + .map(|worktree| worktree.id().map(|id| id.map(ToOwned::to_owned))) + .transpose()?; let mut entries = Vec::new(); let main = repository .main_repo() @@ -499,7 +500,7 @@ fn inventory(repository: &gix::Repository) -> Result<Vec<Row>> { .worktrees() .or_raise(|| message("could not list linked worktrees"))? { - let id = proxy.id().to_owned(); + let id = proxy.id()?.to_owned(); let locked = proxy.is_locked(); let path = absolute( &proxy @@ -882,7 +883,7 @@ where .worktree() .ok_or_raise(|| message("created worktree has no worktree directory"))?; let name = linked - .id() + .id()? .ok_or_raise(|| message("created worktree has no linked worktree ID"))?; let target = gix::refs::Category::LinkedPseudoRef { name } .to_full_name("HEAD") @@ -920,7 +921,7 @@ fn default_path(repository: &gix::Repository, suffix: &gix::bstr::BStr) -> Resul *byte = b'-'; } } - destination.push(gix::path::from_bstr(suffix.as_bstr()).as_ref()); + destination.push(gix::path::from_bstr(suffix.as_bstr())?.as_ref()); Ok(parent.join(destination)) } @@ -1629,7 +1630,7 @@ mod tests { ); let linked = worktree.worktree().expect("the offspring has a worktree"); let target = gix::refs::Target::Symbolic( - format!("worktrees/{}/HEAD", linked.id().expect("the offspring is linked")).try_into()?, + format!("worktrees/{}/HEAD", linked.id()?.expect("the offspring is linked")).try_into()?, ); let pins = crate::history::all_pins(&source)?; assert_eq!( diff --git a/gix-trace/tests/forest.rs b/gix-trace/tests/forest.rs index 354735045cf..fe5b39f17e8 100644 --- a/gix-trace/tests/forest.rs +++ b/gix-trace/tests/forest.rs @@ -28,7 +28,7 @@ fn collecting_processor() -> (impl Processor, mpsc::Receiver<Tree>) { } #[test] -fn in_thread_keeps_nested_workers_in_the_captured_tree() -> Result<(), Box<dyn Error>> { +fn in_thread_keeps_nested_workers_in_the_captured_tree() -> TestResult { let (dispatch, receiver) = collector(); let work = tracing::dispatcher::with_default(&dispatch, || { let _root = gix_trace::coarse!("root"); @@ -46,24 +46,25 @@ fn in_thread_keeps_nested_workers_in_the_captured_tree() -> Result<(), Box<dyn E matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)), "the captured parent keeps the tree open after its original guard has dropped" ); - thread::spawn(move || { + thread::spawn(move || -> TestResult { let (other_dispatch, other_receiver) = collector(); tracing::dispatcher::with_default(&other_dispatch, || { let _other = gix_trace::coarse!("other"); work(); gix_trace::info!("restored"); }); - let tree = other_receiver.try_recv().expect("the other root has closed"); - let other = tree.span().expect("the other subscriber receives its own root"); + let tree = other_receiver.try_recv()?; + let other = tree.span()?; assert_eq!(other.nodes().len(), 1, "worker spans stay with the captured subscriber"); assert_eq!( - other.nodes()[0].event().expect("the only child is an event").message(), + other.nodes()[0].event()?.message(), Some("restored"), "the worker's prior subscriber and current span are restored" ); + Ok(()) }) .join() - .expect("workers finish without panicking"); + .expect("workers finish without panicking")?; let tree = receiver.try_recv()?; let root = tree.span()?; assert_eq!(root.name(), "root", "workers retain the captured parent"); @@ -82,7 +83,7 @@ fn in_thread_keeps_nested_workers_in_the_captured_tree() -> Result<(), Box<dyn E } #[test] -fn recorded_fields_replace_values_and_fill_empty_fields() -> Result<(), Box<dyn Error>> { +fn recorded_fields_replace_values_and_fill_empty_fields() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let span = gix_trace::coarse!( @@ -109,7 +110,7 @@ fn recorded_fields_replace_values_and_fill_empty_fields() -> Result<(), Box<dyn } #[test] -fn unentered_spans_format_with_zero_percentages() -> Result<(), Box<dyn Error>> { +fn unentered_spans_format_with_zero_percentages() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let root = tracing::info_span!("unentered"); @@ -126,7 +127,7 @@ fn unentered_spans_format_with_zero_percentages() -> Result<(), Box<dyn Error>> } #[test] -fn recording_a_value_can_emit_an_event_from_its_debug_formatter() -> Result<(), Box<dyn Error>> { +fn recording_a_value_can_emit_an_event_from_its_debug_formatter() -> TestResult { struct LogsWhenFormatted; impl std::fmt::Debug for LogsWhenFormatted { @@ -137,18 +138,17 @@ fn recording_a_value_can_emit_an_event_from_its_debug_formatter() -> Result<(), } let (completed, completion) = mpsc::channel(); - let worker = thread::spawn(move || { + let worker = thread::spawn(move || -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let span = gix_trace::coarse!("root", value = tracing::field::Empty); span.record("value", tracing::field::debug(LogsWhenFormatted)); }); - completed - .send(receiver.try_recv().expect("the root span has closed")) - .expect("the test is waiting"); + completed.send(receiver.try_recv()?)?; + Ok(()) }); let tree = completion.recv_timeout(Duration::from_secs(10))?; - worker.join().expect("recording and formatting must not panic"); + worker.join().expect("recording and formatting must not panic")?; let root = tree.span()?; assert_eq!( root.fields()[0].value(), @@ -164,7 +164,7 @@ fn recording_a_value_can_emit_an_event_from_its_debug_formatter() -> Result<(), } #[test] -fn global_level_filter_after_forest_filters_collection() -> Result<(), Box<dyn Error>> { +fn global_level_filter_after_forest_filters_collection() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with(ForestLayer::from(processor)) @@ -190,7 +190,7 @@ fn global_level_filter_after_forest_filters_collection() -> Result<(), Box<dyn E } #[test] -fn global_level_filter_before_forest_filters_collection() -> Result<(), Box<dyn Error>> { +fn global_level_filter_before_forest_filters_collection() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with(LevelFilter::INFO) @@ -216,7 +216,7 @@ fn global_level_filter_before_forest_filters_collection() -> Result<(), Box<dyn } #[test] -fn forest_with_a_filter_skips_hidden_ancestors() -> Result<(), Box<dyn Error>> { +fn forest_with_a_filter_skips_hidden_ancestors() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with(tracing_subscriber::fmt::layer().with_writer(std::io::sink)) @@ -264,7 +264,7 @@ fn forest_with_a_filter_skips_hidden_ancestors() -> Result<(), Box<dyn Error>> { } #[test] -fn forest_with_a_filter_promotes_children_of_a_hidden_root() -> Result<(), Box<dyn Error>> { +fn forest_with_a_filter_promotes_children_of_a_hidden_root() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with(tracing_subscriber::fmt::layer().with_writer(std::io::sink)) @@ -301,7 +301,7 @@ fn forest_with_a_filter_promotes_children_of_a_hidden_root() -> Result<(), Box<d } #[test] -fn unfiltered_forest_after_a_filtered_layer_retains_all_nodes() -> Result<(), Box<dyn Error>> { +fn unfiltered_forest_after_a_filtered_layer_retains_all_nodes() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with( @@ -327,7 +327,7 @@ fn unfiltered_forest_after_a_filtered_layer_retains_all_nodes() -> Result<(), Bo } #[test] -fn unfiltered_forest_before_a_filtered_layer_retains_all_nodes() -> Result<(), Box<dyn Error>> { +fn unfiltered_forest_before_a_filtered_layer_retains_all_nodes() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default().with(ForestLayer::from(processor)).with( tracing_subscriber::fmt::layer() @@ -351,7 +351,7 @@ fn unfiltered_forest_before_a_filtered_layer_retains_all_nodes() -> Result<(), B } #[test] -fn dynamic_env_filter_can_enable_a_span_by_its_fields() -> Result<(), Box<dyn Error>> { +fn dynamic_env_filter_can_enable_a_span_by_its_fields() -> TestResult { let (processor, receiver) = collecting_processor(); let subscriber = Registry::default() .with(tracing_subscriber::EnvFilter::try_new("[work{enabled=true}]=trace")?) @@ -382,7 +382,7 @@ fn dynamic_env_filter_can_enable_a_span_by_its_fields() -> Result<(), Box<dyn Er } #[test] -fn reloading_a_filter_during_field_formatting_keeps_existing_span_state() -> Result<(), Box<dyn Error>> { +fn reloading_a_filter_during_field_formatting_keeps_existing_span_state() -> TestResult { struct ReloadWhenFormatted<F>(F); impl<F: Fn()> std::fmt::Debug for ReloadWhenFormatted<F> { @@ -432,7 +432,7 @@ fn reloading_a_filter_during_field_formatting_keeps_existing_span_state() -> Res } #[test] -fn formatting_filters_nodes_and_promotes_visible_descendants() -> Result<(), Box<dyn Error>> { +fn formatting_filters_nodes_and_promotes_visible_descendants() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let _root = tracing::info_span!("root").entered(); @@ -490,7 +490,7 @@ fn formatting_filters_nodes_and_promotes_visible_descendants() -> Result<(), Box } #[test] -fn filtering_a_root_preserves_its_timing_baseline() -> Result<(), Box<dyn Error>> { +fn filtering_a_root_preserves_its_timing_baseline() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let _root = tracing::debug_span!("hidden root").entered(); @@ -530,7 +530,7 @@ fn filtering_a_root_preserves_its_timing_baseline() -> Result<(), Box<dyn Error> } #[test] -fn nested_trees_preserve_fields_and_messages() -> Result<(), Box<dyn Error>> { +fn nested_trees_preserve_fields_and_messages() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let _root = tracing::info_span!("root", operation = "scan").entered(); @@ -623,7 +623,7 @@ fn nested_trees_preserve_fields_and_messages() -> Result<(), Box<dyn Error>> { } #[test] -fn explicit_parents_override_context_and_follows_from_is_not_a_parent() -> Result<(), Box<dyn Error>> { +fn explicit_parents_override_context_and_follows_from_is_not_a_parent() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || { let target = tracing::info_span!("target"); @@ -681,7 +681,7 @@ fn explicit_parents_override_context_and_follows_from_is_not_a_parent() -> Resul } #[test] -fn interleaved_future_polls_keep_independent_trees() -> Result<(), Box<dyn Error>> { +fn interleaved_future_polls_keep_independent_trees() -> TestResult { use std::{ future::Future, task::{Context, Poll, Waker}, @@ -754,9 +754,9 @@ fn interleaved_future_polls_keep_independent_trees() -> Result<(), Box<dyn Error } #[test] -fn workers_keep_the_root_open_and_attach_in_completion_order() -> Result<(), Box<dyn Error>> { +fn workers_keep_the_root_open_and_attach_in_completion_order() -> TestResult { let (dispatch, receiver) = collector(); - tracing::dispatcher::with_default(&dispatch, || { + tracing::dispatcher::with_default(&dispatch, || -> TestResult { let root = tracing::info_span!("root").entered(); let (ready, readiness) = mpsc::channel(); let mut releases = Vec::new(); @@ -768,22 +768,21 @@ fn workers_keep_the_root_open_and_attach_in_completion_order() -> Result<(), Box let (release, wait) = mpsc::channel(); releases.push(release); workers.push(thread::spawn(move || { - tracing::dispatcher::with_default(&dispatch, move || { + tracing::dispatcher::with_default(&dispatch, move || -> TestResult { let worker = tracing::info_span!(parent: &parent, "worker", worker_id).entered(); - ready.send(()).expect("the main thread waits for both workers to enter"); + ready.send(())?; drop(ready); - wait.recv().expect("the main thread must release each worker"); + wait.recv()?; tracing::info!("finished"); drop(worker); drop(parent); - }); + Ok(()) + }) })); } drop(ready); for _ in 0..2 { - readiness - .recv_timeout(Duration::from_secs(10)) - .expect("both workers must enter before their parent is dropped"); + readiness.recv_timeout(Duration::from_secs(10))?; } drop(root); assert!( @@ -791,24 +790,25 @@ fn workers_keep_the_root_open_and_attach_in_completion_order() -> Result<(), Box "worker references retain the root after its owner drops it" ); - releases[1].send(()).expect("the second worker is waiting"); + releases[1].send(())?; workers .pop() .expect("two workers were spawned") .join() - .expect("the second worker must not panic"); + .expect("the second worker must not panic")?; assert!( receiver.try_recv().is_err(), "one remaining worker still retains the entire tree" ); - releases[0].send(()).expect("the first worker is waiting"); + releases[0].send(())?; workers .pop() .expect("one worker remains") .join() - .expect("the first worker must not panic"); - }); + .expect("the first worker must not panic")?; + Ok(()) + })?; let tree = receiver.try_recv()?; let root = tree.span()?; @@ -829,13 +829,9 @@ fn workers_keep_the_root_open_and_attach_in_completion_order() -> Result<(), Box } assert_eq!( root.inner_duration(), - root.nodes() - .iter() - .map(|node| node - .span() - .expect("all root children are worker spans") - .total_duration()) - .sum::<std::time::Duration>(), + root.nodes().iter().try_fold(std::time::Duration::ZERO, |total, node| { + Ok::<_, gix_error::TestError>(total + node.span()?.total_duration()) + })?, "concurrent child durations are aggregated" ); assert!( @@ -934,7 +930,7 @@ fn processor_errors_preserve_existing_error_context() -> TestResult { } #[test] -fn a_failed_formatter_passes_the_intact_tree_to_its_fallback() -> Result<(), Box<dyn Error>> { +fn a_failed_formatter_passes_the_intact_tree_to_its_fallback() -> TestResult { let (sender, receiver) = mpsc::channel(); let primary = Printer::new().formatter(|_: &Tree| Err::<String, _>(std::io::Error::other("formatter unavailable"))); let fallback = processor::from_fn(move |tree| { @@ -969,7 +965,7 @@ fn a_failed_formatter_passes_the_intact_tree_to_its_fallback() -> Result<(), Box } #[test] -fn pretty_event_output_preserves_the_selected_color_mode() -> Result<(), Box<dyn Error>> { +fn pretty_event_output_preserves_the_selected_color_mode() -> TestResult { let (dispatch, receiver) = collector(); tracing::dispatcher::with_default(&dispatch, || tracing::info!(answer = 42, "hello")); let output = Pretty.fmt(&receiver.try_recv()?)?; diff --git a/gix-trace/tests/forest_init.rs b/gix-trace/tests/forest_init.rs index fb28d375947..cffade71557 100644 --- a/gix-trace/tests/forest_init.rs +++ b/gix-trace/tests/forest_init.rs @@ -2,7 +2,7 @@ // Keep global initialization in its own test executable: the subscriber cannot be reset. #[test] -fn repeated_initialization_returns_errors() -> Result<(), Box<dyn std::error::Error>> { +fn repeated_initialization_returns_errors() -> gix_error::TestResult { gix_trace::forest::init()?; assert!( gix_trace::forest::init().is_err(), diff --git a/gix-transport/src/client/blocking_io/file.rs b/gix-transport/src/client/blocking_io/file.rs index 9ea20b00762..256ba3bc67c 100644 --- a/gix-transport/src/client/blocking_io/file.rs +++ b/gix-transport/src/client/blocking_io/file.rs @@ -271,19 +271,22 @@ impl client::blocking_io::Transport for SpawnProcessOnDemand { ) -> std::result::Result<SetServiceResponse<'_>, client::Error> { let (cmd, ssh_kind, cmd_name) = self.prepare_command(service)?; let envs = std::mem::take(&mut self.envs); - let into_std_command = |mut cmd: gix_command::Prepare| { + let into_std_command = |mut cmd: gix_command::Prepare, command: &std::ffi::OsStr| { cmd.stdin = Stdio::piped(); cmd.stdout = Stdio::piped(); - let mut cmd = std::process::Command::from(cmd); + let mut cmd = std::process::Command::try_from(cmd).map_err(|err| client::Error::InvokeProgram { + source: std::io::Error::new(std::io::ErrorKind::InvalidInput, err), + command: command.to_owned(), + })?; for env_to_remove in ENV_VARS_TO_REMOVE { cmd.env_remove(env_to_remove); } cmd.envs(envs.iter().map(|(k, v)| (k, v))); - cmd + Ok::<_, client::Error>(cmd) }; - let mut cmd = into_std_command(cmd); + let mut cmd = into_std_command(cmd, &cmd_name)?; gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand"); let mut child = match cmd.spawn() { Ok(child) => child, @@ -291,7 +294,7 @@ impl client::blocking_io::Transport for SpawnProcessOnDemand { // The service program wasn't found in `PATH`, but as `git` itself can be found, // the service can still be run through it (#2313). let (cmd, cmd_name) = self.prepare_fallback_command(service); - let mut cmd = into_std_command(cmd); + let mut cmd = into_std_command(cmd, &cmd_name)?; gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand (fallback)"); cmd.spawn().map_err(|err| client::Error::InvokeProgram { source: err, @@ -420,7 +423,7 @@ mod tests { } #[test] - fn upload_pack_invocation_preserves_scp_like_path_distinction() { + fn upload_pack_invocation_preserves_scp_like_path_distinction() -> gix_testtools::TestResult { for (url, expected) in [ ( "git@forge.com:/path/repo", @@ -435,18 +438,15 @@ mod tests { &["ssh", "git@forge.com", "git-upload-pack", "'/path/repo'"][..], ), ] { - let url = gix_url::parse(url).expect("valid url"); - let cmd = ssh::connect(url, Protocol::V1, Default::default(), false).expect("parse success"); + let url = gix_url::parse(url)?; + let cmd = ssh::connect(url, Protocol::V1, Default::default(), false)?; assert_eq!( - command_and_args( - cmd.prepare_command(crate::Service::UploadPack) - .expect("valid command") - .0 - ), + command_and_args(cmd.prepare_command(crate::Service::UploadPack)?.0), expected, "the remote shell command must match Git's parsed repository path" ); } + Ok(()) } #[test] @@ -485,8 +485,10 @@ mod tests { fn command_and_args(cmd: gix_command::Prepare) -> Vec<String> { let program = cmd.command.clone(); - let cmd = std::process::Command::from(cmd); - let expected_program = std::process::Command::from(gix_command::prepare(&program)); + let cmd = + std::process::Command::try_from(cmd).expect("command fixture can be represented by the platform"); + let expected_program = std::process::Command::try_from(gix_command::prepare(&program)) + .expect("command fixture can be represented by the platform"); assert_eq!( cmd.get_program(), expected_program.get_program(), diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index e3b391f4b1e..e551c26e33d 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -923,15 +923,14 @@ mod tests { } #[test] - fn aborted_uploads_preserve_the_callback_error() { + fn aborted_uploads_preserve_the_callback_error() -> gix_testtools::TestResult { let (writer, reader) = pipe::unidirectional(1); writer .channel .send(Err(io::Error::other(gix_error::ClassificationMarker::with_source( gix_error::Class::Retryable, gix_error::not_found("custom upload source is unavailable"), - )))) - .expect("the upload reader is alive"); + ))))?; let mut handler = Handler { receive_body: Some(StreamOrBuffer::Stream(reader)), ..Default::default() @@ -953,6 +952,7 @@ mod tests { assert!(err.is_not_found(), "other callback classifications survive too"); assert!(err.downcast_any_ref::<curl::Error>().is_some()); assert!(handler.io_error.is_none(), "a later request cannot reuse this failure"); + Ok(()) } #[test] diff --git a/gix-transport/src/client/blocking_io/ssh/mod.rs b/gix-transport/src/client/blocking_io/ssh/mod.rs index 4cad4dbe1f8..675f2d79fbf 100644 --- a/gix-transport/src/client/blocking_io/ssh/mod.rs +++ b/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -13,6 +13,7 @@ use crate::{Protocol, client::blocking_io::file::SpawnProcessOnDemand}; pub enum Error { UnsupportedScheme(gix_url::Url), AmbiguousHostName { host: String }, + PrepareCommand(gix_error::Error), } impl std::fmt::Display for Error { @@ -26,6 +27,7 @@ impl std::fmt::Display for Error { Error::AmbiguousHostName { host } => { write!(f, "Host name '{host}' could be mistaken for a command-line argument") } + Error::PrepareCommand(_) => f.write_str("Could not prepare the ssh command"), } } } @@ -35,6 +37,7 @@ impl std::error::Error for Error { match self { Error::AmbiguousHostName { .. } => Some(const { &gix_error::ClassificationMarker::VALIDATION }), Error::UnsupportedScheme(_) => Some(const { &gix_error::ClassificationMarker::UNSUPPORTED }), + Error::PrepareCommand(err) => Some(err), } } } @@ -218,7 +221,7 @@ fn build_client_feature_check_command(ssh_cmd: &OsStr, url: &Url, disallow_shell if disallow_shell { prepare.use_shell = false; } - Ok(prepare.into()) + prepare.try_into().map_err(Error::PrepareCommand) } #[cfg(test)] diff --git a/gix-transport/src/client/blocking_io/ssh/tests.rs b/gix-transport/src/client/blocking_io/ssh/tests.rs index d9d4b1d103a..69eccc66716 100644 --- a/gix-transport/src/client/blocking_io/ssh/tests.rs +++ b/gix-transport/src/client/blocking_io/ssh/tests.rs @@ -315,8 +315,10 @@ mod program_kind { fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> Vec<String> { let prepare = call(kind, url, version); let program = prepare.command.clone(); - let cmd = std::process::Command::from(prepare); - let expected_program = std::process::Command::from(gix_command::prepare(&program)); + let cmd = + std::process::Command::try_from(prepare).expect("command fixture can be represented by the platform"); + let expected_program = std::process::Command::try_from(gix_command::prepare(&program)) + .expect("command fixture can be represented by the platform"); assert_eq!( cmd.get_program(), expected_program.get_program(), diff --git a/gix-transport/src/client/git/mod.rs b/gix-transport/src/client/git/mod.rs index 11e2a727bef..053357566a8 100644 --- a/gix-transport/src/client/git/mod.rs +++ b/gix-transport/src/client/git/mod.rs @@ -92,23 +92,23 @@ mod message { use crate::{Protocol, Service, client::git}; #[test] - fn version_1_without_host_and_version() { + fn version_1_without_host_and_version() -> gix_testtools::TestResult { assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]) - .expect("the request is valid"), + git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[])?, "git-upload-pack hello/world\0" ); + Ok(()) } #[test] - fn version_2_without_host_and_version() { + fn version_2_without_host_and_version() -> gix_testtools::TestResult { assert_eq!( - git::message::connect(Service::UploadPack, Protocol::V2, br"hello\world", None, &[]) - .expect("the request is valid"), + git::message::connect(Service::UploadPack, Protocol::V2, br"hello\world", None, &[])?, "git-upload-pack hello\\world\0\0version=2\0" ); + Ok(()) } #[test] - fn version_2_without_host_and_version_and_extra_parameters() { + fn version_2_without_host_and_version_and_extra_parameters() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -116,13 +116,13 @@ mod message { b"/path/project.git", None, &[("key", Some("value")), ("value-only", None)] - ) - .expect("the request is valid"), + )?, "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0" ); + Ok(()) } #[test] - fn with_host_without_port() { + fn with_host_without_port() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -130,13 +130,13 @@ mod message { br"hello\world", Some(&("host".into(), None)), &[] - ) - .expect("the request is valid"), + )?, "git-upload-pack hello\\world\0host=host\0" ); + Ok(()) } #[test] - fn with_host_without_port_and_extra_parameters() { + fn with_host_without_port_and_extra_parameters() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -144,13 +144,13 @@ mod message { br"hello\world", Some(&("host".into(), None)), &[("key", Some("value")), ("value-only", None)] - ) - .expect("the request is valid"), + )?, "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0" ); + Ok(()) } #[test] - fn with_host_with_port() { + fn with_host_with_port() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -158,14 +158,14 @@ mod message { br"hello\world", Some(&("host".into(), Some(404))), &[] - ) - .expect("the request is valid"), + )?, "git-upload-pack hello\\world\0host=host:404\0" ); + Ok(()) } #[test] - fn with_strange_host_and_port() { + fn with_strange_host_and_port() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -173,15 +173,15 @@ mod message { b"--upload-pack=attack", Some(&("--proxy=other-attack".into(), Some(404))), &[] - ) - .expect("the request is valid"), + )?, "git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0", "we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases" ); + Ok(()) } #[test] - fn carriage_returns_are_allowed_like_in_git() { + fn carriage_returns_are_allowed_like_in_git() -> gix_testtools::TestResult { assert_eq!( git::message::connect( Service::UploadPack, @@ -189,10 +189,10 @@ mod message { b"hello\rworld", Some(&("ho\rst".into(), None)), &[] - ) - .expect("carriage returns are allowed like in Git"), + )?, "git-upload-pack hello\rworld\0host=ho\rst\0" ); + Ok(()) } } } diff --git a/gix-transport/tests/async-transport.rs b/gix-transport/tests/async-transport.rs index 2d08a223823..e7f4c52423c 100644 --- a/gix-transport/tests/async-transport.rs +++ b/gix-transport/tests/async-transport.rs @@ -5,7 +5,7 @@ pub mod bisync { use std::path::PathBuf; pub type Error = Box<dyn std::error::Error + Send + Sync>; -pub type Result<T = ()> = std::result::Result<T, Error>; +pub use gix_testtools::TestResult; pub fn fixture_bytes(path: &str) -> Vec<u8> { fn fixture_path(path: &str) -> PathBuf { diff --git a/gix-transport/tests/blocking-transport-http-reqwest.rs b/gix-transport/tests/blocking-transport-http-reqwest.rs index 1d3e7def98d..df451e7513c 100644 --- a/gix-transport/tests/blocking-transport-http-reqwest.rs +++ b/gix-transport/tests/blocking-transport-http-reqwest.rs @@ -25,17 +25,16 @@ use http_helpers::{observe_connection_within_deadline, read_request_lines, respo /// without an HTTP status (for example a connection or TLS failure), the underlying error must be /// kept as `source()` instead of being stringified, so callers can see the real cause. #[test] -fn request_failure_without_status_preserves_error_source() { +fn request_failure_without_status_preserves_error_source() -> gix_testtools::TestResult { // Bind then immediately drop a listener so the port reliably refuses connections: a failure // with no HTTP status, exercising the path that previously stringified the error. let addr = { - let server = std::net::TcpListener::bind("127.0.0.1:0").expect("can bind an ephemeral port"); - server.local_addr().expect("listener has a local address") + let server = std::net::TcpListener::bind("127.0.0.1:0")?; + server.local_addr()? }; let url = format!("http://{addr}/repo"); - let mut client = - http::connect::<http::reqwest::Remote>(url.as_str().try_into().expect("the url is valid"), Protocol::V1, false); + let mut client = http::connect::<http::reqwest::Remote>(url.as_str().try_into()?, Protocol::V1, false); let error = client .handshake(Service::UploadPack, &[]) @@ -49,10 +48,11 @@ fn request_failure_without_status_preserves_error_source() { io_error.source().is_some(), "the underlying error must be preserved as source(), not stringified: {io_error:?}" ); + Ok(()) } #[test] -fn redirects_are_not_followed_with_configure_request_hook() -> Result<(), Box<dyn Error + Send + Sync>> { +fn redirects_are_not_followed_with_configure_request_hook() -> gix_testtools::TestResult { let redirected_listener = std::net::TcpListener::bind("127.0.0.1:0")?; let redirected_addr = redirected_listener.local_addr()?; let redirect_listener = std::net::TcpListener::bind("127.0.0.1:0")?; @@ -131,7 +131,7 @@ fn redirects_are_not_followed_with_configure_request_hook() -> Result<(), Box<dy } #[test] -fn relative_redirects_normalize_the_updated_base_url() -> Result<(), Box<dyn Error + Send + Sync>> { +fn relative_redirects_normalize_the_updated_base_url() -> gix_testtools::TestResult { let listener = std::net::TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?; let port = addr.port(); @@ -192,7 +192,7 @@ fn relative_redirects_normalize_the_updated_base_url() -> Result<(), Box<dyn Err } #[test] -fn cross_authority_redirects_are_not_followed_without_matching_tail() -> Result<(), Box<dyn Error + Send + Sync>> { +fn cross_authority_redirects_are_not_followed_without_matching_tail() -> gix_testtools::TestResult { let redirected_listener = std::net::TcpListener::bind("127.0.0.1:0")?; let redirected_addr = redirected_listener.local_addr()?; let redirect_listener = std::net::TcpListener::bind("127.0.0.1:0")?; diff --git a/gix-transport/tests/blocking-transport-http.rs b/gix-transport/tests/blocking-transport-http.rs index 77bfda1cc5a..3d958076cb8 100644 --- a/gix-transport/tests/blocking-transport-http.rs +++ b/gix-transport/tests/blocking-transport-http.rs @@ -7,7 +7,7 @@ pub mod bisync { use std::path::PathBuf; pub type Error = Box<dyn std::error::Error + Send + Sync>; -pub type Result<T = ()> = std::result::Result<T, Error>; +pub use gix_testtools::TestResult; pub fn fixture_bytes(path: &str) -> Vec<u8> { fn fixture_path(path: &str) -> PathBuf { diff --git a/gix-transport/tests/blocking-transport.rs b/gix-transport/tests/blocking-transport.rs index b9f79c89ee4..085c5dd1629 100644 --- a/gix-transport/tests/blocking-transport.rs +++ b/gix-transport/tests/blocking-transport.rs @@ -5,7 +5,7 @@ pub mod bisync { use std::path::PathBuf; pub type Error = Box<dyn std::error::Error + Send + Sync>; -pub type Result<T = ()> = std::result::Result<T, Error>; +pub use gix_testtools::TestResult; pub fn fixture_bytes(path: &str) -> Vec<u8> { fn fixture_path(path: &str) -> PathBuf { diff --git a/gix-transport/tests/client/blocking_io/http/mod.rs b/gix-transport/tests/client/blocking_io/http/mod.rs index 1df2265a15a..896c0255135 100644 --- a/gix-transport/tests/client/blocking_io/http/mod.rs +++ b/gix-transport/tests/client/blocking_io/http/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use std::{ cell::RefCell, collections::HashSet, @@ -55,7 +55,7 @@ fn assert_error_status( } #[test] -fn http_statuses_distinguish_authentication_authorization_and_capabilities() -> gix_error::TestResult { +fn http_statuses_distinguish_authentication_authorization_and_capabilities() -> gix_testtools::TestResult { use gix_error::Class; for (status, expected) in [ (401, Some(Class::Unauthenticated)), @@ -110,7 +110,7 @@ fn http_statuses_distinguish_authentication_authorization_and_capabilities() -> } #[test] -fn http_status_500_is_communicated_via_special_io_error() -> Result { +fn http_status_500_is_communicated_via_special_io_error() -> TestResult { let (_, _, err) = assert_error_status(500, std::io::ErrorKind::ConnectionAborted)?; if cfg!(feature = "http-client-curl") { insta::assert_debug_snapshot!(err, "HTTP server errors report the status and retain the retryable I/O kind", @" @@ -134,7 +134,7 @@ fn http_status_500_is_communicated_via_special_io_error() -> Result { } #[test] -fn http_identity_is_picked_up_from_url() -> Result { +fn http_identity_is_picked_up_from_url() -> TestResult { let transport = gix_transport::client::blocking_io::http::connect::<Remote>( "https://user:pass@example.com/repo".try_into()?, Protocol::V2, @@ -270,7 +270,7 @@ fn http_will_use_pipelining() { } #[test] -fn http_authentication_error_can_be_differentiated_and_identity_is_transmitted() -> Result { +fn http_authentication_error_can_be_differentiated_and_identity_is_transmitted() -> TestResult { let (server, mut client, err) = assert_error_status(401, std::io::ErrorKind::PermissionDenied)?; insta::assert_debug_snapshot!(err, "HTTP authentication failures retain the status and permission-denied cause", @" An IO error occurred when talking to the server @@ -350,7 +350,7 @@ Authorization: Basic dXNlcjpwYXNzd29yZA== } #[test] -fn authentication_challenges_are_preserved_per_response() -> Result { +fn authentication_challenges_are_preserved_per_response() -> TestResult { let mut error_snapshots = Vec::new(); let server = mock::Server::new( b"HTTP/1.1 401 Unauthorized\r\n\ @@ -454,7 +454,7 @@ fn authentication_challenges_are_preserved_per_response() -> Result { /// `Authorization: Basic dmljdGltLXVzZXI6c3VwZXItc2VjcmV0LXRva2Vu`, leaking them to the attacker. /// ``` #[test] -fn redirected_post_does_not_forward_basic_auth_to_the_new_host() -> Result { +fn redirected_post_does_not_forward_basic_auth_to_the_new_host() -> TestResult { fn has_authorization(lines: &[String]) -> bool { lines .iter() @@ -556,19 +556,11 @@ fn redirected_post_does_not_forward_basic_auth_to_the_new_host() -> Result { oauth_refresh_token: None, })?; - client - .handshake(Service::UploadPack, &[]) - .map(drop) - .expect("redirected handshake should succeed"); + client.handshake(Service::UploadPack, &[]).map(drop)?; let url_after_handshake = client.to_url().as_ref().to_owned(); - let mut request = client - .request(client::WriteMode::Binary, client::MessageKind::Flush, false) - .expect("follow-up POST request can be created after redirected handshake"); - request.write_all(b"0000").expect("flush packet can be written"); - request - .into_read() - .map(drop) - .expect("follow-up POST response can be read"); + let mut request = client.request(client::WriteMode::Binary, client::MessageKind::Flush, false)?; + request.write_all(b"0000")?; + request.into_read().map(drop)?; let original_get = redirect.join().expect("thread"); let (redirected_get, redirected_post) = redirected.join().expect("thread"); @@ -605,7 +597,7 @@ fn redirected_post_does_not_forward_basic_auth_to_the_new_host() -> Result { } #[test] -fn redirected_unauthorized_handshake_updates_url_before_returning() -> Result { +fn redirected_unauthorized_handshake_updates_url_before_returning() -> TestResult { let redirected_listener = std::net::TcpListener::bind("127.0.0.1:0")?; let redirected_addr = redirected_listener.local_addr()?; let redirected_port = redirected_addr.port(); @@ -704,7 +696,7 @@ fn redirected_unauthorized_handshake_updates_url_before_returning() -> Result { } #[test] -fn relative_redirected_handshake_updates_url_before_returning() -> Result { +fn relative_redirected_handshake_updates_url_before_returning() -> TestResult { let listener = std::net::TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?; let port = addr.port(); @@ -763,7 +755,7 @@ fn relative_redirected_handshake_updates_url_before_returning() -> Result { } #[test] -fn chained_relative_redirected_unauthorized_handshake_updates_url_from_previous_hop() -> Result { +fn chained_relative_redirected_unauthorized_handshake_updates_url_from_previous_hop() -> TestResult { let listener = std::net::TcpListener::bind("127.0.0.1:0")?; let addr = listener.local_addr()?; let port = addr.port(); @@ -871,7 +863,7 @@ fn chained_relative_redirected_unauthorized_handshake_updates_url_from_previous_ } #[test] -fn redirects_are_not_followed_with_configured_extra_headers() -> Result { +fn redirects_are_not_followed_with_configured_extra_headers() -> TestResult { let redirected_listener = std::net::TcpListener::bind("127.0.0.1:0")?; let redirected_addr = redirected_listener.local_addr()?; let redirect_listener = std::net::TcpListener::bind("127.0.0.1:0")?; @@ -953,7 +945,7 @@ fn redirects_are_not_followed_with_configured_extra_headers() -> Result { } #[test] -fn http_error_results_in_observable_error() -> Result { +fn http_error_results_in_observable_error() -> TestResult { let (_, _, err) = assert_error_status(404, std::io::ErrorKind::Other)?; if cfg!(feature = "http-client-curl") { insta::assert_debug_snapshot!(err, "HTTP not-found responses retain their status diagnostic", @" @@ -977,7 +969,7 @@ fn http_error_results_in_observable_error() -> Result { } #[test] -fn handshake_v1() -> Result { +fn handshake_v1() -> TestResult { let (server, mut c) = mock::serve_and_connect( "v1/http-handshake.response", "path/not/important/due/to/mock", @@ -1112,7 +1104,7 @@ User-Agent: git/oxide-{} } #[test] -fn clone_v1() -> Result { +fn clone_v1() -> TestResult { let (server, mut c) = mock::serve_and_connect( "v1/http-handshake.response", "path/not/important/due/to/mock", @@ -1193,12 +1185,12 @@ Accept: application/x-git-upload-pack-result } #[test] -fn handshake_and_lsrefs_and_fetch_v2() -> Result { +fn handshake_and_lsrefs_and_fetch_v2() -> TestResult { handshake_and_lsrefs_and_fetch_v2_impl("v2/http-handshake.response") } #[test] -fn handshake_and_lsrefs_and_fetch_v2_googlesource() -> Result { +fn handshake_and_lsrefs_and_fetch_v2_googlesource() -> TestResult { let (_server, mut c) = mock::serve_and_connect( "v2/http-no-newlines-handshake.response", "path/not/important/due/to/mock", @@ -1257,12 +1249,12 @@ fn handshake_and_lsrefs_and_fetch_v2_googlesource() -> Result { } #[test] -fn handshake_and_lsrefs_and_fetch_v2_service_announced() -> Result { +fn handshake_and_lsrefs_and_fetch_v2_service_announced() -> TestResult { handshake_and_lsrefs_and_fetch_v2_impl("v2/http-handshake-service-announced.response") } #[test] -fn handshake_v2_surfaces_sha256_object_format() -> Result { +fn handshake_v2_surfaces_sha256_object_format() -> TestResult { let (_server, mut c) = mock::serve_and_connect( "v2/http-handshake-sha256.response", "path/not/important/due/to/mock", @@ -1298,7 +1290,7 @@ fn handshake_v2_surfaces_sha256_object_format() -> Result { Ok(()) } -fn handshake_and_lsrefs_and_fetch_v2_impl(handshake_fixture: &str) -> Result { +fn handshake_and_lsrefs_and_fetch_v2_impl(handshake_fixture: &str) -> TestResult { let (server, mut c) = mock::serve_and_connect(handshake_fixture, "path/not/important/due/to/mock", Protocol::V2)?; assert!( !c.connection_persists_across_multiple_requests(), @@ -1464,7 +1456,7 @@ Content-Length: 22 } #[test] -fn check_content_type_is_case_insensitive() -> Result { +fn check_content_type_is_case_insensitive() -> TestResult { let (_server, mut client) = mock::serve_and_connect( "v2/http-handshake-lowercase-headers.response", "path/not/important/due/to/mock", diff --git a/gix-transport/tests/client/capabilities.rs b/gix-transport/tests/client/capabilities.rs index 1e5d867033b..f16b14792d5 100644 --- a/gix-transport/tests/client/capabilities.rs +++ b/gix-transport/tests/client/capabilities.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; use bstr::ByteSlice; #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] use gix_packetline::async_io::{StreamingPeekableIter, encode}; @@ -11,7 +11,7 @@ use gix_transport::client::capabilities::async_recv::Handshake; use gix_transport::client::capabilities::blocking_recv::Handshake; #[test] -fn from_bytes() -> Result { +fn from_bytes() -> TestResult { let (caps, delim_pos) = Capabilities::from_bytes(&b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/master object-format=sha1 agent=git/2.28.0"[..]) ?; assert_eq!(delim_pos, 45); @@ -103,7 +103,7 @@ fn malformed_advertisements_are_corruption() { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn malformed_advertisements_keep_the_capabilities_error() -> gix_error::TestResult { +async fn malformed_advertisements_keep_the_capabilities_error() -> gix_testtools::TestResult { for line in [b"HEAD".as_slice(), b"HEAD\0"] { let mut buf = Vec::new(); encode::data_to_write(line, &mut buf).await?; @@ -134,7 +134,7 @@ async fn malformed_advertisements_keep_the_capabilities_error() -> gix_error::Te #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn unsupported_versions_are_classified() -> gix_error::TestResult { +async fn unsupported_versions_are_classified() -> gix_testtools::TestResult { for line in ["version 1", "version 3", "version 42"] { let err = Capabilities::from_lines(line.into()).expect_err("only version 2 is supported by this parser"); assert!(err.is_unsupported(), "unsupported versions are not malformed data"); @@ -171,7 +171,7 @@ async fn unsupported_versions_are_classified() -> gix_error::TestResult { } #[test] -fn from_bytes_with_sha256_object_format() -> Result { +fn from_bytes_with_sha256_object_format() -> TestResult { let (caps, _delim_pos) = Capabilities::from_bytes( &b"7814e8a05a59c0cf5fb186661d1551c75d1299b5 HEAD\0side-band-64k object-format=sha256 agent=git/2.40.0"[..], )?; @@ -190,13 +190,12 @@ fn from_bytes_with_sha256_object_format() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn from_lines_with_version_detection_v0() -> Result { +async fn from_lines_with_version_detection_v0() -> TestResult { let mut buf = Vec::<u8>::new(); encode::flush_to_write(&mut buf).await?; let mut stream = StreamingPeekableIter::new(buf.as_slice(), &[gix_packetline::PacketLineRef::Flush], false); let caps = Handshake::from_lines_with_version_detection(&mut stream) - .await - .expect("we can parse V0 as very special case, useful for testing stateful connections in other crates") + .await? .capabilities; assert!(caps.contains("multi_ack_detailed")); assert!(caps.contains("side-band-64k")); diff --git a/gix-transport/tests/client/git.rs b/gix-transport/tests/client/git.rs index b2cf12744d0..93bc80be840 100644 --- a/gix-transport/tests/client/git.rs +++ b/gix-transport/tests/client/git.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::TestResult; #[cfg(feature = "blocking-client")] use std::io::{BufRead, Write}; use std::{ops::Deref, sync::Arc}; @@ -26,7 +26,7 @@ use crate::fixture_bytes; #[cfg(any(feature = "blocking-client", feature = "async-std"))] mod connect { - use gix_error::TestResult; + use gix_testtools::TestResult; #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] use gix_transport::client::async_io::connect::connect; #[cfg(feature = "blocking-client")] @@ -132,7 +132,7 @@ mod connect { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn refused_connections_remain_retryable() -> Result { +async fn refused_connections_remain_retryable() -> TestResult { #[cfg(all(feature = "async-client", not(feature = "blocking-client")))] use client::async_io::connect::connect; #[cfg(feature = "blocking-client")] @@ -181,7 +181,7 @@ async fn refused_connections_remain_retryable() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn handshake_v1_and_request() -> Result { +async fn handshake_v1_and_request() -> TestResult { let mut out = Vec::new(); let server_response = fixture_bytes("v1/clone.response"); let c = Connection::new( @@ -311,7 +311,7 @@ async fn handshake_v1_and_request() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn git_daemon_request_rejects_nul_and_lf() -> Result { +async fn git_daemon_request_rejects_nul_and_lf() -> TestResult { let mut error_snapshots = Vec::new(); for (control, name) in [(b'\0', "NUL"), (b'\n', "newline")] { let mut invalid_path = b"/foo.git".to_vec(); @@ -402,7 +402,7 @@ async fn git_daemon_request_rejects_nul_and_lf() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn push_v1_simulated() -> Result { +async fn push_v1_simulated() -> TestResult { let mut out = Vec::new(); let server_response = fixture_bytes("v1/push.response"); let mut c = Connection::new( @@ -470,7 +470,7 @@ async fn push_v1_simulated() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn handshake_v1_process_mode() -> Result { +async fn handshake_v1_process_mode() -> TestResult { let mut out = Vec::new(); let server_response = fixture_bytes("v1/clone.response"); let mut c = Connection::new( @@ -495,7 +495,7 @@ async fn handshake_v1_process_mode() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn handshake_v2_downgrade_to_v1() -> Result { +async fn handshake_v2_downgrade_to_v1() -> TestResult { let mut out = Vec::new(); let input = fixture_bytes("v1/clone.response"); let mut c = Connection::new( @@ -526,14 +526,14 @@ async fn handshake_v2_downgrade_to_v1() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn handshake_v2_and_request() -> Result { +async fn handshake_v2_and_request() -> TestResult { #[crate::bisync::only_sync] - fn run() -> Result { + fn run() -> TestResult { handshake_v2_and_request_inner() } #[crate::bisync::only_async] - async fn run() -> Result { + async fn run() -> TestResult { // This simulates processing a pack received with async I/O as blocking `BufRead` without blocking the executor. blocking::unblock(|| futures_lite::future::block_on(handshake_v2_and_request_inner()).expect("no failure")) .await; @@ -544,7 +544,7 @@ async fn handshake_v2_and_request() -> Result { } #[crate::bisync::bisync] -async fn handshake_v2_and_request_inner() -> Result { +async fn handshake_v2_and_request_inner() -> TestResult { let mut out = Vec::new(); let input = fixture_bytes("v2/clone.response"); let mut c = Connection::new( @@ -704,7 +704,7 @@ async fn handshake_v2_and_request_inner() -> Result { #[crate::bisync::bisync] #[cfg_attr(feature = "blocking-client", test)] #[cfg_attr(all(feature = "async-client", not(feature = "blocking-client")), async_std::test)] -async fn handshake_v2_with_sha256_object_format() -> Result { +async fn handshake_v2_with_sha256_object_format() -> TestResult { let mut out = Vec::new(); let input = fixture_bytes("v2/handshake-sha256.response"); let mut c = Connection::new( diff --git a/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs b/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs index a7a005bd379..c8516935e19 100644 --- a/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs +++ b/gix-traverse/tests/traverse/commit/simple/adjusted_dates.rs @@ -11,7 +11,7 @@ fn adjusted_dates_repo() -> Result<(std::path::PathBuf, gix_odb::Handle)> { } #[test] -fn head_breadth_first() -> Result { +fn head_breadth_first() -> gix_testtools::TestResult { let (repo_dir, odb) = adjusted_dates_repo()?; // Timestamps show b1c1 (978393600) is a year newer than c2 (946771200), @@ -42,7 +42,7 @@ fn head_breadth_first() -> Result { } #[test] -fn head_date_order() -> Result { +fn head_date_order() -> gix_testtools::TestResult { let (_repo_dir, odb) = adjusted_dates_repo()?; // Graph with timestamps shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 @@ -83,7 +83,7 @@ fn head_date_order() -> Result { } #[test] -fn head_date_order_with_cutoff() -> Result { +fn head_date_order_with_cutoff() -> gix_testtools::TestResult { let (_repo_dir, odb) = adjusted_dates_repo()?; // Graph shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 @@ -110,7 +110,7 @@ fn head_date_order_with_cutoff() -> Result { } #[test] -fn head_date_order_with_cutoff_disabled() -> Result { +fn head_date_order_with_cutoff_disabled() -> gix_testtools::TestResult { let (_repo_dir, odb) = adjusted_dates_repo()?; // Graph shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 @@ -158,7 +158,7 @@ fn head_date_order_with_cutoff_disabled() -> Result { } #[test] -fn date_order_with_cutoff_is_applied_to_starting_position() -> Result { +fn date_order_with_cutoff_is_applied_to_starting_position() -> gix_testtools::TestResult { let (_repo_dir, odb) = adjusted_dates_repo()?; // Graph shown in `head_breadth_first` let tip = hex_to_id("9902e3c3e8f0c569b4ab295ddf473e6de763e1e7"); // c2 @@ -181,7 +181,7 @@ fn date_order_with_cutoff_is_applied_to_starting_position() -> Result { } #[test] -fn head_date_order_first_parent_only() -> Result { +fn head_date_order_first_parent_only() -> gix_testtools::TestResult { let (_repo_dir, odb) = adjusted_dates_repo()?; // Graph shown in `head_breadth_first` let tip = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170"); // m1b1 diff --git a/gix-traverse/tests/traverse/commit/simple/different_date.rs b/gix-traverse/tests/traverse/commit/simple/different_date.rs index 801baed6d61..f182746ddda 100644 --- a/gix-traverse/tests/traverse/commit/simple/different_date.rs +++ b/gix-traverse/tests/traverse/commit/simple/different_date.rs @@ -7,7 +7,7 @@ fn simple_repo() -> Result<(std::path::PathBuf, gix_odb::Handle)> { } #[test] -fn head_breadth_first() -> Result { +fn head_breadth_first() -> gix_testtools::TestResult { let (repo_dir, odb) = simple_repo()?; // Timestamps show branch1 commits are newer than branch2, with c5 being the newest. @@ -50,7 +50,7 @@ fn head_breadth_first() -> Result { } #[test] -fn head_date_order() -> Result { +fn head_date_order() -> gix_testtools::TestResult { let (_repo_dir, odb) = simple_repo()?; // Graph with timestamps shown in `head_breadth_first` let tip = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); // merge diff --git a/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs b/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs index f81cb126aa0..f5f7fe6e602 100644 --- a/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs +++ b/gix-traverse/tests/traverse/commit/simple/different_date_intermixed.rs @@ -7,7 +7,7 @@ fn intermixed_repo() -> Result<(std::path::PathBuf, gix_odb::Handle)> { } #[test] -fn head_breadth_first() -> Result { +fn head_breadth_first() -> gix_testtools::TestResult { let (repo_dir, odb) = intermixed_repo()?; // Timestamps show the intermixed ordering: b1 and b2 commits are interleaved @@ -47,7 +47,7 @@ fn head_breadth_first() -> Result { } #[test] -fn head_date_order() -> Result { +fn head_date_order() -> gix_testtools::TestResult { let (_repo_dir, odb) = intermixed_repo()?; // Graph with timestamps shown in `head_breadth_first` let tip = hex_to_id("58912d92944087dcb09dca79cdd2a937cc158bed"); // merge diff --git a/gix-traverse/tests/traverse/commit/simple/hide.rs b/gix-traverse/tests/traverse/commit/simple/hide.rs index 8591c389493..434b4b65f6e 100644 --- a/gix-traverse/tests/traverse/commit/simple/hide.rs +++ b/gix-traverse/tests/traverse/commit/simple/hide.rs @@ -28,7 +28,7 @@ fn assert_simple_repo_graph(repo_dir: &std::path::Path) -> Result { } #[test] -fn disjoint_hidden_and_interesting() -> Result { +fn disjoint_hidden_and_interesting() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; insta::assert_snapshot!(git_graph(&repo_dir)?, @r" @@ -56,7 +56,7 @@ fn disjoint_hidden_and_interesting() -> Result { } #[test] -fn all_hidden() -> Result { +fn all_hidden() -> gix_testtools::TestResult { let (_repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; let tips = [ hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"), // b3 @@ -73,7 +73,7 @@ fn all_hidden() -> Result { } #[test] -fn some_hidden_and_all_hidden() -> Result { +fn some_hidden_and_all_hidden() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; @@ -124,7 +124,7 @@ fn hidden_bug_repo(name: &str) -> Result<(std::path::PathBuf, gix_odb::Handle)> } #[test] -fn hidden_tip_with_longer_path_to_shared_ancestor() -> Result { +fn hidden_tip_with_longer_path_to_shared_ancestor() -> gix_testtools::TestResult { // Graph: // A(tip) --> shared // / @@ -167,7 +167,7 @@ fn hidden_tip_with_longer_path_to_shared_ancestor() -> Result { } #[test] -fn interesting_tip_with_longer_path_to_shared_ancestor() -> Result { +fn interesting_tip_with_longer_path_to_shared_ancestor() -> gix_testtools::TestResult { // Graph: // A(tip) --> B --> C --> D(shared) // / @@ -209,7 +209,7 @@ fn interesting_tip_with_longer_path_to_shared_ancestor() -> Result { } #[test] -fn without_commit_graph_still_hides_single_visible_tip_correctly() -> Result { +fn without_commit_graph_still_hides_single_visible_tip_correctly() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tip_c2 = hex_to_id("ad33ff2d0c4fc77d56b5fbff6f86f332fe792d83"); @@ -228,7 +228,7 @@ fn without_commit_graph_still_hides_single_visible_tip_correctly() -> Result { } #[test] -fn commit_graph_reduces_odb_lookups_when_hidden_tips_cover_visible_tips() -> Result { +fn commit_graph_reduces_odb_lookups_when_hidden_tips_cover_visible_tips() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tips = [ @@ -283,7 +283,7 @@ fn commit_graph_reduces_odb_lookups_when_hidden_tips_cover_visible_tips() -> Res } #[test] -fn hide_and_commit_graph_call_order_do_not_matter() -> Result { +fn hide_and_commit_graph_call_order_do_not_matter() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "simple")?; assert_simple_repo_graph(&repo_dir)?; let tip_merge = hex_to_id("f49838d84281c3988eeadd988d97dd358c9f9dc4"); diff --git a/gix-traverse/tests/traverse/commit/simple/same_date.rs b/gix-traverse/tests/traverse/commit/simple/same_date.rs index d20b506f080..ed9acbb3676 100644 --- a/gix-traverse/tests/traverse/commit/simple/same_date.rs +++ b/gix-traverse/tests/traverse/commit/simple/same_date.rs @@ -12,7 +12,7 @@ fn same_date_repo() -> Result<(std::path::PathBuf, gix_odb::Handle)> { } #[test] -fn c4_breadth_first() -> Result { +fn c4_breadth_first() -> gix_testtools::TestResult { let (repo_dir, odb) = same_date_repo()?; insta::assert_snapshot!(git_graph(&repo_dir)?, @r" @@ -43,7 +43,7 @@ fn c4_breadth_first() -> Result { } #[test] -fn head_breadth_first() -> Result { +fn head_breadth_first() -> gix_testtools::TestResult { let (_repo_dir, odb) = same_date_repo()?; // Graph shown in `c4_breadth_first` let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 @@ -68,7 +68,7 @@ fn head_breadth_first() -> Result { } #[test] -fn head_date_order() -> Result { +fn head_date_order() -> gix_testtools::TestResult { let (_repo_dir, odb) = same_date_repo()?; // Graph shown in `c4_breadth_first` let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 @@ -106,7 +106,7 @@ fn head_date_order() -> Result { } #[test] -fn head_first_parent_only_breadth_first() -> Result { +fn head_first_parent_only_breadth_first() -> gix_testtools::TestResult { let (_repo_dir, odb) = same_date_repo()?; // Graph shown in `c4_breadth_first` let tip = hex_to_id("01ec18a3ebf2855708ad3c9d244306bc1fae3e9b"); // m1b1 @@ -126,7 +126,7 @@ fn head_first_parent_only_breadth_first() -> Result { } #[test] -fn head_c4_breadth_first() -> Result { +fn head_c4_breadth_first() -> gix_testtools::TestResult { let (_repo_dir, odb) = same_date_repo()?; // Graph shown in `c4_breadth_first` let tips = [ @@ -151,7 +151,7 @@ fn head_c4_breadth_first() -> Result { } #[test] -fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> Result { +fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> gix_testtools::TestResult { // I don't see a use case for the predicate returning false for a commit but return true for // at least one of its ancestors, so this test is kind of dubious. But we do want // `Ancestors` to not eagerly blacklist all of a commit's ancestors when blacklisting that @@ -185,7 +185,7 @@ fn filtered_commit_does_not_block_ancestors_reachable_from_another_commit() -> R } #[test] -fn predicate_only_called_once_even_if_fork_point() -> Result { +fn predicate_only_called_once_even_if_fork_point() -> gix_testtools::TestResult { // The `self.seen` check should come before the `self.predicate` check, as we don't know how // expensive calling `self.predicate` may be. let (_repo_dir, odb) = same_date_repo()?; diff --git a/gix-traverse/tests/traverse/commit/topo.rs b/gix-traverse/tests/traverse/commit/topo.rs index a83e9b94750..f097563f2bf 100644 --- a/gix-traverse/tests/traverse/commit/topo.rs +++ b/gix-traverse/tests/traverse/commit/topo.rs @@ -93,10 +93,9 @@ fn read_baseline(fixture_dir: &std::path::Path, name: &str) -> Result<Vec<String mod basic { use super::*; - use crate::Result; #[test] - fn simple() -> Result { + fn simple() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); @@ -133,7 +132,7 @@ mod basic { } #[test] - fn duplicate_tips_are_ignored() -> Result { + fn duplicate_tips_are_ignored() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); @@ -146,7 +145,7 @@ mod basic { } #[test] - fn one_end() -> Result { + fn one_end() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); let end = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); @@ -172,7 +171,7 @@ mod basic { } #[test] - fn empty_range() -> Result { + fn empty_range() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); let end = hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785"); @@ -183,7 +182,7 @@ mod basic { } #[test] - fn two_tips_two_ends() -> Result { + fn two_tips_two_ends() -> gix_testtools::TestResult { let odb = odb()?; let tips = [ hex_to_id("d09384f312b03e4a1413160739805ff25e8fe99d"), @@ -211,7 +210,7 @@ mod basic { } #[test] - fn with_dummy_predicate() -> Result { + fn with_dummy_predicate() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); let filter_out = hex_to_id("eeab3243aad67bc838fc4425f759453bf0b47785"); @@ -244,7 +243,7 @@ mod basic { } #[test] - fn end_along_first_parent() -> Result { + fn end_along_first_parent() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("d09384f312b03e4a1413160739805ff25e8fe99d"); let end = hex_to_id("33eb18340e4eaae3e3dcf80222b02f161cd3f966"); @@ -265,10 +264,9 @@ mod basic { mod first_parent { use super::*; - use crate::Result; #[test] - fn basic() -> Result { + fn basic() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); @@ -301,7 +299,7 @@ mod first_parent { } #[test] - fn with_end() -> Result { + fn with_end() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); let end = hex_to_id("f1cce1b5c7efcdfa106e95caa6c45a2cae48a481"); @@ -323,7 +321,7 @@ mod first_parent { } #[test] - fn end_is_second_parent() -> Result { + fn end_is_second_parent() -> gix_testtools::TestResult { let odb = odb()?; let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); let end = hex_to_id("3be0c4c793c634c8fd95054345d4935d10a0879a"); @@ -347,10 +345,9 @@ mod first_parent { mod date_order { use super::*; - use crate::Result; #[test] - fn with_ends() -> Result { + fn with_ends() -> gix_testtools::TestResult { let odb = odb()?; // Same tip and end as basic::one_end() but the order should be different. let tip = hex_to_id("62ed296d9986f50477e9f7b7e81cd0258939a43d"); diff --git a/gix-traverse/tests/traverse/tree.rs b/gix-traverse/tests/traverse/tree.rs index 6c38394ff9d..095c2a343b2 100644 --- a/gix-traverse/tests/traverse/tree.rs +++ b/gix-traverse/tests/traverse/tree.rs @@ -7,7 +7,6 @@ fn odb() -> Result<gix_odb::Handle> { } mod depthfirst { - use crate::Result; use gix_object::FindExt; use gix_testtools::normalize_debug_snapshot; use gix_traverse::{tree, tree::recorder::Location}; @@ -16,7 +15,7 @@ mod depthfirst { use crate::util::fixture_odb; #[test] - fn full_path_and_filename() -> Result { + fn full_path_and_filename() -> gix_testtools::TestResult { let db = odb()?; let mut state = gix_traverse::tree::depthfirst::State::default(); let mut buf = state.pop_buf(); @@ -168,7 +167,7 @@ mod depthfirst { } #[test] - fn more_difficult_fixture() -> Result { + fn more_difficult_fixture() -> gix_testtools::TestResult { let db = fixture_odb("make_traversal_repo_for_trees_depthfirst.sh")?; let mut state = gix_traverse::tree::depthfirst::State::default(); let mut buf = state.pop_buf(); @@ -245,7 +244,6 @@ mod depthfirst { } mod breadthfirst { - use crate::Result; use gix_object::bstr::BString; use gix_odb::pack::FindExt; use gix_traverse::{tree, tree::recorder::Location}; @@ -253,7 +251,7 @@ mod breadthfirst { use super::*; #[test] - fn delegate_cancellation_is_classified() -> gix_error::TestResult { + fn delegate_cancellation_is_classified() -> gix_testtools::TestResult { struct Cancel; impl tree::Visit for Cancel { fn pop_back_tracked_path_and_set_current(&mut self) {} @@ -295,7 +293,7 @@ mod breadthfirst { } #[test] - fn full_path() -> Result { + fn full_path() -> gix_testtools::TestResult { let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); @@ -305,8 +303,7 @@ mod breadthfirst { // Full paths - that's the default. let mut recorder = tree::Recorder::default(); gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, + db.find_tree_iter(&commit.tree_id()?, &mut buf2)?.0, tree::breadthfirst::State::default(), &db, &mut recorder, @@ -383,7 +380,7 @@ mod breadthfirst { } #[test] - fn filename_only() -> Result<()> { + fn filename_only() -> gix_testtools::TestResult { let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); @@ -392,8 +389,7 @@ mod breadthfirst { .0; let mut recorder = tree::Recorder::default().track_location(Some(Location::FileName)); gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, + db.find_tree_iter(&commit.tree_id()?, &mut buf2)?.0, tree::breadthfirst::State::default(), &db, &mut recorder, @@ -410,7 +406,7 @@ mod breadthfirst { } #[test] - fn no_location() -> Result<()> { + fn no_location() -> gix_testtools::TestResult { let db = odb()?; let mut buf = Vec::new(); let mut buf2 = Vec::new(); @@ -419,8 +415,7 @@ mod breadthfirst { .0; let mut recorder = tree::Recorder::default().track_location(None); gix_traverse::tree::breadthfirst( - db.find_tree_iter(&commit.tree_id().expect("a tree is available in a commit"), &mut buf2)? - .0, + db.find_tree_iter(&commit.tree_id()?, &mut buf2)?.0, tree::breadthfirst::State::default(), &db, &mut recorder, diff --git a/gix-url/src/expand_path.rs b/gix-url/src/expand_path.rs index f0d3a89b65a..ef8c6eb3a7d 100644 --- a/gix-url/src/expand_path.rs +++ b/gix-url/src/expand_path.rs @@ -103,7 +103,7 @@ pub fn with( fn make_relative(path: &Path) -> PathBuf { path.components().skip(1).collect() } - let path = gix_path::try_from_byte_slice(path) + let path = gix_path::from_byte_slice(path) .or_raise(|| gix_error::validation("UTF8 conversion on non-unix system failed for path").with_input(path))?; Ok(match user { Some(user) => home_for_user(user) diff --git a/gix-url/src/impls.rs b/gix-url/src/impls.rs index dfbf6dd0c60..9aa7711b609 100644 --- a/gix-url/src/impls.rs +++ b/gix-url/src/impls.rs @@ -39,7 +39,7 @@ impl TryFrom<PathBuf> for Url { type Error = gix_error::Error; fn try_from(value: PathBuf) -> Result<Self, Self::Error> { - gix_path::into_bstr(value).try_into() + gix_path::into_bstr(value)?.try_into() } } @@ -47,7 +47,7 @@ impl TryFrom<&Path> for Url { type Error = gix_error::Error; fn try_from(value: &Path) -> Result<Self, Self::Error> { - gix_path::into_bstr(value).try_into() + gix_path::into_bstr(value)?.try_into() } } @@ -55,9 +55,7 @@ impl TryFrom<&std::ffi::OsStr> for Url { type Error = gix_error::Error; fn try_from(value: &std::ffi::OsStr) -> Result<Self, Self::Error> { - gix_path::os_str_into_bstr(value) - .expect("no illformed UTF-8 on Windows") - .try_into() + gix_path::os_str_into_bstr(value)?.try_into() } } diff --git a/gix-url/src/lib.rs b/gix-url/src/lib.rs index b4968654156..1e41beb7296 100644 --- a/gix-url/src/lib.rs +++ b/gix-url/src/lib.rs @@ -398,9 +398,9 @@ impl Url { /// Other schemes are unchanged. pub fn canonicalize(&mut self, current_dir: &std::path::Path) -> Result { if self.scheme == Scheme::File { - let path = gix_path::from_bstr(Cow::Borrowed(self.path.as_ref())); + let path = gix_path::from_bstr(Cow::Borrowed(self.path.as_ref()))?; let abs_path = gix_path::realpath_opts(path.as_ref(), current_dir, gix_path::realpath::MAX_SYMLINKS)?; - self.path = gix_path::into_bstr(abs_path).into_owned(); + self.path = gix_path::into_bstr(abs_path)?.into_owned(); } Ok(()) } @@ -912,7 +912,7 @@ impl Url { #[cfg(all(test, feature = "serde"))] mod serde_tests { #[test] - fn legacy_encoded_public_path_is_migrated() -> gix_error::TestResult { + fn legacy_encoded_public_path_is_migrated() -> gix_testtools::TestResult { for (input, legacy_path, decoded_path) in [ ("https://example.com/a%2Fb", "/a%2Fb", "/a/b"), ("https://example.com/%20%25", "/ %25", "/ %"), diff --git a/gix-url/src/simple_url.rs b/gix-url/src/simple_url.rs index 71c5adbfa05..9157f71fa59 100644 --- a/gix-url/src/simple_url.rs +++ b/gix-url/src/simple_url.rs @@ -407,13 +407,13 @@ mod tests { } #[test] - fn git_schemes_allow_unbracketed_ipv6() { + fn git_schemes_allow_unbracketed_ipv6() -> gix_testtools::TestResult { for scheme in ["git", "ssh", "git+ssh", "ssh+git"] { - let url = ParsedUrl::parse(&format!("{scheme}://user@::1/repo")) - .expect("Git schemes allow unbracketed IPv6 hosts"); + let url = ParsedUrl::parse(&format!("{scheme}://user@::1/repo"))?; assert_eq!(url.host.as_deref(), Some("::1"), "the IPv6 address is the host"); assert_eq!(url.path, "/repo", "the path remains separate from the IPv6 host"); } + Ok(()) } #[test] diff --git a/gix-url/tests/url/access.rs b/gix-url/tests/url/access.rs index 2939ab4b3c3..361d6ac5483 100644 --- a/gix-url/tests/url/access.rs +++ b/gix-url/tests/url/access.rs @@ -21,11 +21,11 @@ mod canonicalized { #[test] fn file_that_is_current_dir_is_absolutized() -> gix_testtools::TestResult { let url = gix_url::parse(".")?; - assert!(gix_path::from_bstr(Cow::Borrowed(url.path.as_ref())).is_relative()); + assert!(gix_path::from_bstr(Cow::Borrowed(url.path.as_ref()))?.is_relative()); assert!( gix_path::from_bstr(Cow::Borrowed( url.canonicalized(&std::env::current_dir()?)?.path.as_ref() - )) + ))? .is_absolute() ); Ok(()) @@ -34,6 +34,24 @@ mod canonicalized { use gix_url::ArgumentSafety; +#[test] +#[cfg(windows)] +fn native_paths_with_unpaired_surrogates_return_encoding_errors() { + use std::os::windows::ffi::OsStringExt; + + let path = std::path::PathBuf::from(std::ffi::OsString::from_wide(&[0xd800])); + for err in [ + gix_url::Url::try_from(path.clone()).expect_err("owned paths must be representable as URL bytes"), + gix_url::Url::try_from(path.as_path()).expect_err("borrowed paths must be representable as URL bytes"), + gix_url::Url::try_from(path.as_os_str()).expect_err("native strings must be representable as URL bytes"), + ] { + assert!( + err.is_validation(), + "path encoding failures are returned instead of panicking" + ); + } +} + mod path_query_fragment { use std::borrow::Cow; @@ -55,7 +73,7 @@ mod path_query_fragment { } #[test] - fn http_delimiters_are_recognized_before_decoding() -> gix_error::TestResult { + fn http_delimiters_are_recognized_before_decoding() -> gix_testtools::TestResult { for scheme in ["http", "https"] { for (suffix, expected) in [ ("/repo%23one?query=value#fragment", "/repo#one"), @@ -102,7 +120,7 @@ mod path_query_fragment { } #[test] - fn other_schemes_keep_their_stored_paths() -> gix_error::TestResult { + fn other_schemes_keep_their_stored_paths() -> gix_testtools::TestResult { for (input, expected) in [ ("git@host:repo#one?two%23three", "repo#one?two%23three"), ("ssh://git@host/repo%23one?two", "/repo#one?two"), @@ -127,7 +145,7 @@ mod path_query_fragment { } #[test] - fn constructed_and_mutated_http_paths_are_already_decoded() -> gix_error::TestResult { + fn constructed_and_mutated_http_paths_are_already_decoded() -> gix_testtools::TestResult { for scheme in [Scheme::Http, Scheme::Https] { for (path, expected) in [ ("/repo%23one?query=value#fragment", "/repo%23one"), @@ -168,7 +186,7 @@ mod path_query_fragment { } #[test] - fn mutation_keeps_paths_byte_oriented_and_uses_the_current_scheme() -> gix_error::TestResult { + fn mutation_keeps_paths_byte_oriented_and_uses_the_current_scheme() -> gix_testtools::TestResult { let mut url = gix_url::parse("https://host/repo%23one?query=value#fragment")?; url.scheme = Scheme::Ssh; assert_eq!( @@ -210,7 +228,7 @@ mod path_query_fragment { } #[test] - fn query_pairs_and_fragments_are_split_before_decoding() -> gix_error::TestResult { + fn query_pairs_and_fragments_are_split_before_decoding() -> gix_testtools::TestResult { for scheme in ["http", "https"] { for (suffix, path, query, fragment) in [ ( @@ -296,7 +314,7 @@ mod path_query_fragment { } #[test] - fn empty_and_absent_components_remain_distinct() -> gix_error::TestResult { + fn empty_and_absent_components_remain_distinct() -> gix_testtools::TestResult { for (suffix, has_query, fragment) in [ ("", false, None), ("/repo", false, None), @@ -323,7 +341,7 @@ mod path_query_fragment { } #[test] - fn constructed_and_mutated_components_keep_literal_percent_escapes() -> gix_error::TestResult { + fn constructed_and_mutated_components_keep_literal_percent_escapes() -> gix_testtools::TestResult { let path = "/repo%23one?x=a%26b&space=+%2B#frag%23+"; let constructed = Url::from_parts(Scheme::Https, None, None, Some("host".into()), None, path.into(), false)?; let mut mutated = gix_url::parse("https://host/old%23repo?x=%26#old%23fragment")?; @@ -354,7 +372,7 @@ mod path_query_fragment { #[cfg(feature = "serde")] #[test] - fn serde_preserves_component_boundaries() -> gix_error::TestResult { + fn serde_preserves_component_boundaries() -> gix_testtools::TestResult { let mut mutated = gix_url::parse("https://host/old%23repo?x=%26#old%23fragment")?; mutated.path = "/repo%23one?x=a%26b&space=+%2B#frag%23+".into(); for url in [ @@ -373,7 +391,7 @@ mod path_query_fragment { } #[test] -fn user() -> gix_error::TestResult { +fn user() -> gix_testtools::TestResult { let mut url = gix_url::parse("https://user:password@host/path")?; assert_eq!(url.user(), Some("user")); @@ -384,7 +402,7 @@ fn user() -> gix_error::TestResult { } #[test] -fn password() -> gix_error::TestResult { +fn password() -> gix_testtools::TestResult { let mut url = gix_url::parse("https://user:password@host/path")?; assert_eq!(url.password(), Some("password")); @@ -395,7 +413,7 @@ fn password() -> gix_error::TestResult { } #[test] -fn mutation_roundtrip() -> gix_error::TestResult { +fn mutation_roundtrip() -> gix_testtools::TestResult { let mut url = gix_url::parse("https://user@host/path")?; url.set_user(Some("newuser".into())); url.set_password(Some("secret".into())); @@ -411,7 +429,7 @@ fn mutation_roundtrip() -> gix_error::TestResult { } #[test] -fn from_bytes_roundtrip() -> gix_error::TestResult { +fn from_bytes_roundtrip() -> gix_testtools::TestResult { let original = "https://user:password@example.com:8080/path/to/repo"; let url = gix_url::parse(original)?; @@ -425,7 +443,7 @@ fn from_bytes_roundtrip() -> gix_error::TestResult { } #[test] -fn from_bytes_with_non_utf8_path() -> gix_error::TestResult { +fn from_bytes_with_non_utf8_path() -> gix_testtools::TestResult { let url = gix_url::parse(b"/path/to\xff/repo".as_slice())?; let bytes = url.to_bstring(); let from_bytes = gix_url::Url::from_bytes(bytes.as_ref())?; @@ -437,7 +455,7 @@ fn from_bytes_with_non_utf8_path() -> gix_error::TestResult { } #[test] -fn user_argument_safety() -> gix_error::TestResult { +fn user_argument_safety() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://-Fconfigfile@foo/bar")?; assert_eq!(url.user(), Some("-Fconfigfile")); @@ -455,7 +473,7 @@ fn user_argument_safety() -> gix_error::TestResult { } #[test] -fn host_argument_safety() -> gix_error::TestResult { +fn host_argument_safety() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://-oProxyCommand=open$IFS-aCalculator/foo")?; assert_eq!(url.user(), None); @@ -480,7 +498,7 @@ fn host_argument_safety() -> gix_error::TestResult { } #[test] -fn path_argument_safety() -> gix_error::TestResult { +fn path_argument_safety() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://foo/-oProxyCommand=open$IFS-aCalculator")?; assert_eq!(url.user(), None); @@ -511,7 +529,7 @@ fn path_argument_safety() -> gix_error::TestResult { } #[test] -fn all_argument_safety_safe() -> gix_error::TestResult { +fn all_argument_safety_safe() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://user.name@example.com/path/to/file")?; assert_eq!(url.user(), Some("user.name")); @@ -529,7 +547,7 @@ fn all_argument_safety_safe() -> gix_error::TestResult { } #[test] -fn all_argument_safety_not_safe() -> gix_error::TestResult { +fn all_argument_safety_not_safe() -> gix_testtools::TestResult { let all_bad = "ssh://-Fconfigfile@-oProxyCommand=open$IFS-aCalculator/-oProxyCommand=open$IFS-aCalculator"; let url = gix_url::parse(all_bad)?; diff --git a/gix-url/tests/url/baseline.rs b/gix-url/tests/url/baseline.rs index 12df5e27bdf..b9f5c5d9c63 100644 --- a/gix-url/tests/url/baseline.rs +++ b/gix-url/tests/url/baseline.rs @@ -1,9 +1,9 @@ use bstr::ByteSlice; #[test] -fn parse_remote_helpers_like_git() { - let base = gix_testtools::scripted_fixture_read_only("make_baseline.sh").expect("fixture is generated"); - let baseline = std::fs::read(base.join("git-baseline.remote-helper")).expect("baseline exists"); +fn parse_remote_helpers_like_git() -> gix_testtools::TestResult { + let base = gix_testtools::scripted_fixture_read_only("make_baseline.sh")?; + let baseline = std::fs::read(base.join("git-baseline.remote-helper"))?; let mut count = 0; for line in baseline.lines() { let mut fields = line.split(|b| b == &b'\t'); @@ -12,8 +12,8 @@ fn parse_remote_helpers_like_git() { let address = fields.next().expect("remote-helper address is recorded"); assert!(fields.next().is_none(), "the helper receives exactly two arguments"); - let actual = gix_url::parse(input).expect("Git-accepted remote-helper syntax parses"); - let helper = helper.to_str().expect("helper names are UTF-8").to_owned(); + let actual = gix_url::parse(input)?; + let helper = helper.to_str()?.to_owned(); assert_eq!( actual.scheme, if input.starts_with_str("ext::") { @@ -29,9 +29,7 @@ fn parse_remote_helpers_like_git() { assert_eq!(actual.to_bstring(), input, "remote-helper form roundtrips exactly"); } else { assert_eq!( - gix_url::parse(actual.to_bstring()) - .expect("serialized URL parses") - .scheme, + gix_url::parse(actual.to_bstring())?.scheme, actual.scheme, "Git's case-sensitive helper name survives serialization" ); @@ -39,6 +37,7 @@ fn parse_remote_helpers_like_git() { count += 1; } assert_eq!(count, 28, "all helper, address and syntax combinations ran"); + Ok(()) } #[test] diff --git a/gix-url/tests/url/expand_path.rs b/gix-url/tests/url/expand_path.rs index aca86bf6702..362c67c5ed1 100644 --- a/gix-url/tests/url/expand_path.rs +++ b/gix-url/tests/url/expand_path.rs @@ -25,7 +25,7 @@ fn user_home(name: &str) -> std::path::PathBuf { } #[test] -fn without_username() -> gix_error::TestResult { +fn without_username() -> gix_testtools::TestResult { let (user, resolved_path) = expand_path::parse(b"/~/hello/git".as_bstr())?; let resolved_path = expand_path::with(user.as_ref(), resolved_path.as_ref(), |user: &ForUser| match user { ForUser::Current => Some(user_home("byron")), @@ -36,7 +36,7 @@ fn without_username() -> gix_error::TestResult { } #[test] -fn with_username() -> gix_error::TestResult { +fn with_username() -> gix_testtools::TestResult { let (user, resolved_path) = expand_path::parse(b"/~byron/hello/git".as_bstr())?; let resolved_path = expand_path::with(user.as_ref(), resolved_path.as_ref(), |user: &ForUser| match user { ForUser::Current => unreachable!("we have a name"), diff --git a/gix-url/tests/url/parse/file.rs b/gix-url/tests/url/parse/file.rs index 7167a4922c7..01ea1f1c424 100644 --- a/gix-url/tests/url/parse/file.rs +++ b/gix-url/tests/url/parse/file.rs @@ -4,7 +4,7 @@ use gix_url::Scheme; use crate::parse::{assert_url_roundtrip, url, url_alternate}; #[test] -fn file_path_with_protocol() -> gix_error::TestResult { +fn file_path_with_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file:///path/to/git", url(Scheme::File, None, None, None, b"/path/to/git"), @@ -12,7 +12,7 @@ fn file_path_with_protocol() -> gix_error::TestResult { } #[test] -fn file_to_root() -> gix_error::TestResult { +fn file_to_root() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file:///", url(Scheme::File, None, None, None, b"/"), @@ -20,7 +20,7 @@ fn file_to_root() -> gix_error::TestResult { } #[test] -fn file_path_without_protocol() -> gix_error::TestResult { +fn file_path_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "/path/to/git", url_alternate(Scheme::File, None, None, None, b"/path/to/git"), @@ -28,7 +28,7 @@ fn file_path_without_protocol() -> gix_error::TestResult { } #[test] -fn file_path_with_whitespace() -> gix_error::TestResult { +fn file_path_with_whitespace() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "/path/to/git with spaces ", url_alternate(Scheme::File, None, None, None, b"/path/to/git with spaces "), @@ -36,7 +36,7 @@ fn file_path_with_whitespace() -> gix_error::TestResult { } #[test] -fn no_username_expansion_for_file_paths_without_protocol() -> gix_error::TestResult { +fn no_username_expansion_for_file_paths_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "~/path/to/git", url_alternate(Scheme::File, None, None, None, b"~/path/to/git"), @@ -44,7 +44,7 @@ fn no_username_expansion_for_file_paths_without_protocol() -> gix_error::TestRes } #[test] -fn no_username_expansion_for_file_paths_with_protocol() -> gix_error::TestResult { +fn no_username_expansion_for_file_paths_with_protocol() -> gix_testtools::TestResult { assert_url_roundtrip( "file:///~username/path/to/git", url(Scheme::File, None, None, None, b"/~username/path/to/git"), @@ -56,7 +56,7 @@ fn no_username_expansion_for_file_paths_with_protocol() -> gix_error::TestResult } #[test] -fn non_utf8_file_path_without_protocol() -> gix_error::TestResult { +fn non_utf8_file_path_without_protocol() -> gix_testtools::TestResult { let url = gix_url::parse(b"/path/to\xff/git".as_bstr())?; assert_eq!(url, url_alternate(Scheme::File, None, None, None, b"/path/to\xff/git")); let url_lossless = url.to_bstring(); @@ -70,7 +70,7 @@ fn non_utf8_file_path_without_protocol() -> gix_error::TestResult { } #[test] -fn relative_file_path_without_protocol() -> gix_error::TestResult { +fn relative_file_path_without_protocol() -> gix_testtools::TestResult { assert_url_roundtrip( "../../path/to/git", url_alternate(Scheme::File, None, None, None, b"../../path/to/git"), @@ -82,7 +82,7 @@ fn relative_file_path_without_protocol() -> gix_error::TestResult { } #[test] -fn shortest_possible_absolute_path() -> gix_error::TestResult { +fn shortest_possible_absolute_path() -> gix_testtools::TestResult { assert_url_roundtrip("/", url_alternate(Scheme::File, None, None, None, b"/"))?; Ok(assert_url_roundtrip( "file:///", @@ -91,7 +91,7 @@ fn shortest_possible_absolute_path() -> gix_error::TestResult { } #[test] -fn shortest_possible_relative_path() -> gix_error::TestResult { +fn shortest_possible_relative_path() -> gix_testtools::TestResult { assert_url_roundtrip("a", url_alternate(Scheme::File, None, None, None, b"a"))?; assert_url_roundtrip("../", url_alternate(Scheme::File, None, None, None, b"../"))?; assert_url_roundtrip(r"..\", url_alternate(Scheme::File, None, None, None, br"..\"))?; @@ -102,7 +102,7 @@ fn shortest_possible_relative_path() -> gix_error::TestResult { } #[test] -fn no_relative_paths_if_protocol() -> gix_error::TestResult { +fn no_relative_paths_if_protocol() -> gix_testtools::TestResult { assert_url_roundtrip("file://../", url(Scheme::File, None, "..", None, b"/"))?; assert_url_roundtrip("file://./", url(Scheme::File, None, ".", None, b"/"))?; assert_url_roundtrip("file://a/", url(Scheme::File, None, "a", None, b"/"))?; @@ -120,7 +120,7 @@ fn no_relative_paths_if_protocol() -> gix_error::TestResult { } #[test] -fn interior_relative_file_path_without_protocol() -> gix_error::TestResult { +fn interior_relative_file_path_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "/abs/path/../../path/to/git", url_alternate(Scheme::File, None, None, None, b"/abs/path/../../path/to/git"), @@ -128,7 +128,7 @@ fn interior_relative_file_path_without_protocol() -> gix_error::TestResult { } #[test] -fn url_from_relative_path_with_colon_in_name() -> gix_error::TestResult { +fn url_from_relative_path_with_colon_in_name() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "./weird/directory/na:me", url_alternate(Scheme::File, None, None, None, b"./weird/directory/na:me"), @@ -142,7 +142,7 @@ mod windows { use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate}; #[test] - fn reproduce_1063() -> gix_error::TestResult { + fn reproduce_1063() -> gix_testtools::TestResult { let input = r"C:\Users\RUNNER~1\AppData\Local\Temp\tmp.vIa4tyjv17"; let url_input = r"file://C:\Users\RUNNER~1\AppData\Local\Temp\tmp.vIa4tyjv17"; assert_url(url_input, url(Scheme::File, None, None, None, input.as_bytes()))?; @@ -151,7 +151,7 @@ mod windows { } #[test] - fn url_from_absolute_path() -> gix_error::TestResult { + fn url_from_absolute_path() -> gix_testtools::TestResult { // Test with a Windows path directly instead of using url::Url::from_directory_path assert_url( r"C:\users\1\", @@ -164,7 +164,7 @@ mod windows { } #[test] - fn file_path_without_protocol() -> gix_error::TestResult { + fn file_path_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "x:/path/to/git", url_alternate(Scheme::File, None, None, None, b"x:/path/to/git"), @@ -172,7 +172,7 @@ mod windows { } #[test] - fn file_path_with_backslashes_without_protocol() -> gix_error::TestResult { + fn file_path_with_backslashes_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( r"x:\path\to\git", url_alternate(Scheme::File, None, None, None, br"x:\path\to\git"), @@ -180,7 +180,7 @@ mod windows { } #[test] - fn file_path_with_protocol() -> gix_error::TestResult { + fn file_path_with_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file://x:/path/to/git", url(Scheme::File, None, None, None, b"x:/path/to/git"), @@ -195,7 +195,7 @@ mod unix { use crate::parse::{assert_url_roundtrip, url, url_alternate}; #[test] - fn url_from_absolute_path() -> gix_error::TestResult { + fn url_from_absolute_path() -> gix_testtools::TestResult { // Test with a simple file path instead of using url::Url::from_directory_path Ok(assert_url_roundtrip( "/users/foo/", @@ -204,7 +204,7 @@ mod unix { } #[test] - fn file_path_without_protocol() -> gix_error::TestResult { + fn file_path_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "x:/path/to/git", url_alternate(Scheme::Ssh, None, "x", None, b"/path/to/git"), @@ -212,7 +212,7 @@ mod unix { } #[test] - fn file_path_with_backslashes_without_protocol() -> gix_error::TestResult { + fn file_path_with_backslashes_without_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( r"x:\path\to\git", url_alternate(Scheme::Ssh, None, "x", None, br"\path\to\git"), @@ -220,7 +220,7 @@ mod unix { } #[test] - fn file_path_with_protocol() -> gix_error::TestResult { + fn file_path_with_protocol() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file://x:/path/to/git", url(Scheme::File, None, "x:", None, b"/path/to/git"), @@ -228,7 +228,7 @@ mod unix { } #[test] - fn file_url_with_ipv6_and_user() -> gix_error::TestResult { + fn file_url_with_ipv6_and_user() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file://User@[::1]/repo", gix_url::Url::from_parts( @@ -244,7 +244,7 @@ mod unix { } #[test] - fn file_url_with_ipv6() -> gix_error::TestResult { + fn file_url_with_ipv6() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "file://[::1]/repo", url(Scheme::File, None, "[::1]", None, b"/repo"), diff --git a/gix-url/tests/url/parse/http.rs b/gix-url/tests/url/parse/http.rs index f6b222d4bf3..ba5e88f822a 100644 --- a/gix-url/tests/url/parse/http.rs +++ b/gix-url/tests/url/parse/http.rs @@ -12,7 +12,7 @@ fn test_password(with_dot: bool) -> String { } #[test] -fn username_expansion_is_unsupported() -> gix_error::TestResult { +fn username_expansion_is_unsupported() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "http://example.com/~byron/hello", url(Scheme::Http, None, "example.com", None, b"/~byron/hello"), @@ -20,7 +20,7 @@ fn username_expansion_is_unsupported() -> gix_error::TestResult { } #[test] -fn empty_user_cannot_roundtrip() -> gix_error::TestResult { +fn empty_user_cannot_roundtrip() -> gix_testtools::TestResult { let actual = gix_url::parse("http://@example.com/~byron/hello")?; let expected = url(Scheme::Http, None, "example.com", None, b"/~byron/hello"); assert_eq!(actual, expected); @@ -33,7 +33,7 @@ fn empty_user_cannot_roundtrip() -> gix_error::TestResult { } #[test] -fn username_and_password() -> gix_error::TestResult { +fn username_and_password() -> gix_testtools::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/~byron/hello"), @@ -42,7 +42,7 @@ fn username_and_password() -> gix_error::TestResult { } #[test] -fn colon_in_username_roundtrips() -> gix_error::TestResult { +fn colon_in_username_roundtrips() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "http://a%3Ab@example.com/", url(Scheme::Http, "a:b", "example.com", None, b"/"), @@ -50,7 +50,7 @@ fn colon_in_username_roundtrips() -> gix_error::TestResult { } #[test] -fn colon_in_password_roundtrips() -> gix_error::TestResult { +fn colon_in_password_roundtrips() -> gix_testtools::TestResult { let password = format!("a:{}", std::process::id()); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/"), @@ -59,7 +59,7 @@ fn colon_in_password_roundtrips() -> gix_error::TestResult { } #[test] -fn username_and_password_and_port() -> gix_error::TestResult { +fn username_and_password_and_port() -> gix_testtools::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com:8080/~byron/hello"), @@ -68,7 +68,7 @@ fn username_and_password_and_port() -> gix_error::TestResult { } #[test] -fn username_and_password_with_spaces_and_port() -> gix_error::TestResult { +fn username_and_password_with_spaces_and_port() -> gix_testtools::TestResult { let expected = gix_url::Url::from_parts( Scheme::Http, Some("user name".into()), @@ -88,7 +88,7 @@ fn username_and_password_with_spaces_and_port() -> gix_error::TestResult { } #[test] -fn only_password() -> gix_error::TestResult { +fn only_password() -> gix_testtools::TestResult { let password = test_password(false); Ok(assert_url_roundtrip( &format!("http://:{password}@example.com/~byron/hello"), @@ -97,7 +97,7 @@ fn only_password() -> gix_error::TestResult { } #[test] -fn username_and_empty_password() -> gix_error::TestResult { +fn username_and_empty_password() -> gix_testtools::TestResult { let actual = gix_url::parse("http://user:@example.com/~byron/hello")?; let expected = url(Scheme::Http, "user", "example.com", None, b"/~byron/hello"); assert_eq!(actual, expected); @@ -110,7 +110,7 @@ fn username_and_empty_password() -> gix_error::TestResult { } #[test] -fn secure() -> gix_error::TestResult { +fn secure() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "https://github.com/byron/gitoxide", url(Scheme::Https, None, "github.com", None, b"/byron/gitoxide"), @@ -118,14 +118,14 @@ fn secure() -> gix_error::TestResult { } #[test] -fn http_missing_path() -> gix_error::TestResult { +fn http_missing_path() -> gix_testtools::TestResult { assert_url_roundtrip("http://host.xz/", url(Scheme::Http, None, "host.xz", None, b"/"))?; assert_url("http://host.xz", url(Scheme::Http, None, "host.xz", None, b"/"))?; Ok(()) } #[test] -fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { +fn username_with_dot_is_not_percent_encoded() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "http://user.name@example.com/repo", url(Scheme::Http, "user.name", "example.com", None, b"/repo"), @@ -133,7 +133,7 @@ fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { } #[test] -fn password_with_dot_is_not_percent_encoded() -> gix_error::TestResult { +fn password_with_dot_is_not_percent_encoded() -> gix_testtools::TestResult { let password = test_password(true); Ok(assert_url_roundtrip( &format!("http://user:{password}@example.com/repo"), @@ -142,7 +142,7 @@ fn password_with_dot_is_not_percent_encoded() -> gix_error::TestResult { } #[test] -fn username_and_password_with_dots_are_not_percent_encoded() -> gix_error::TestResult { +fn username_and_password_with_dots_are_not_percent_encoded() -> gix_testtools::TestResult { let password = test_password(true); Ok(assert_url_roundtrip( &format!("http://user.name:{password}@example.com/repo"), @@ -151,7 +151,7 @@ fn username_and_password_with_dots_are_not_percent_encoded() -> gix_error::TestR } #[test] -fn http_with_ipv6() -> gix_error::TestResult { +fn http_with_ipv6() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "http://[::1]/repo", url(Scheme::Http, None, "[::1]", None, b"/repo"), @@ -159,7 +159,7 @@ fn http_with_ipv6() -> gix_error::TestResult { } #[test] -fn http_with_ipv6_and_port() -> gix_error::TestResult { +fn http_with_ipv6_and_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "http://[::1]:8080/repo", url(Scheme::Http, None, "[::1]", 8080, b"/repo"), @@ -167,7 +167,7 @@ fn http_with_ipv6_and_port() -> gix_error::TestResult { } #[test] -fn https_with_ipv6_user_and_port() -> gix_error::TestResult { +fn https_with_ipv6_user_and_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "https://user@[2001:db8::1]:8443/repo", url(Scheme::Https, "user", "[2001:db8::1]", 8443, b"/repo"), @@ -175,7 +175,7 @@ fn https_with_ipv6_user_and_port() -> gix_error::TestResult { } #[test] -fn percent_encoded_path() -> gix_error::TestResult { +fn percent_encoded_path() -> gix_testtools::TestResult { let url = gix_url::parse("https://example.com/path/with%20spaces/file")?; assert_eq!(url.path, "/path/with spaces/file", "paths are now decoded"); assert_eq!( @@ -187,14 +187,14 @@ fn percent_encoded_path() -> gix_error::TestResult { } #[test] -fn percent_encoded_international_path() -> gix_error::TestResult { +fn percent_encoded_international_path() -> gix_testtools::TestResult { let url = gix_url::parse("https://example.com/caf%C3%A9")?; assert_eq!(url.path, "/cafĂŠ", "international characters are decoded in path"); Ok(()) } #[test] -fn original_path_preserves_exact_spelling() -> gix_error::TestResult { +fn original_path_preserves_exact_spelling() -> gix_testtools::TestResult { for (input, decoded_path, original_path, message) in [ ( "https://example.com/plain", @@ -230,7 +230,7 @@ fn original_path_preserves_exact_spelling() -> gix_error::TestResult { } #[test] -fn reserved_percent_encoded_path_octets_remain_lossless() -> gix_error::TestResult { +fn reserved_percent_encoded_path_octets_remain_lossless() -> gix_testtools::TestResult { for (input, expected_path, message) in [ ( "https://example.com/a%2Fb", @@ -261,7 +261,7 @@ fn reserved_percent_encoded_path_octets_remain_lossless() -> gix_error::TestResu } #[test] -fn literal_percent_escape_text_from_parts_is_encoded() -> gix_error::TestResult { +fn literal_percent_escape_text_from_parts_is_encoded() -> gix_testtools::TestResult { let url = gix_url::Url::from_parts( Scheme::Https, None, @@ -305,7 +305,7 @@ fn literal_percent_escape_text_from_parts_is_encoded() -> gix_error::TestResult } #[test] -fn percent_encoded_path_roundtrips_in_lossless_serialization() -> gix_error::TestResult { +fn percent_encoded_path_roundtrips_in_lossless_serialization() -> gix_testtools::TestResult { for (input, message, expected_host, expected_path) in [ ( "https://%20@%40.example.org/%20%25", @@ -331,7 +331,7 @@ fn percent_encoded_path_roundtrips_in_lossless_serialization() -> gix_error::Tes } #[test] -fn query_and_fragment_delimiters_in_path_roundtrip() -> gix_error::TestResult { +fn query_and_fragment_delimiters_in_path_roundtrip() -> gix_testtools::TestResult { assert_url_roundtrip( "https://host/repo.git?token=abc", url(Scheme::Https, None, "host", None, b"/repo.git?token=abc"), @@ -344,7 +344,7 @@ fn query_and_fragment_delimiters_in_path_roundtrip() -> gix_error::TestResult { } #[test] -fn query_and_fragment_delimiters_end_the_authority() -> gix_error::TestResult { +fn query_and_fragment_delimiters_end_the_authority() -> gix_testtools::TestResult { for input in ["https://host?@redirected/repo", "https://host#@redirected/repo"] { let url = gix_url::parse(input)?; assert_eq!(url.host(), Some("host"), "the authority ends at the delimiter"); @@ -366,7 +366,7 @@ fn query_and_fragment_delimiters_end_the_authority() -> gix_error::TestResult { } #[test] -fn authority_length_limit_excludes_the_scheme_separator() -> gix_error::TestResult { +fn authority_length_limit_excludes_the_scheme_separator() -> gix_testtools::TestResult { let at_limit = format!("https://{}", "a".repeat(1024)); assert_eq!( gix_url::parse(&at_limit)?.host().map(str::len), diff --git a/gix-url/tests/url/parse/mod.rs b/gix-url/tests/url/parse/mod.rs index fff0e5265c0..51e50da9009 100644 --- a/gix-url/tests/url/parse/mod.rs +++ b/gix-url/tests/url/parse/mod.rs @@ -90,7 +90,7 @@ mod radicle { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn basic() -> gix_error::TestResult { + fn basic() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "rad://hynkuwzskprmswzeo4qdtku7grdrs4ffj3g9tjdxomgmjzhtzpqf81@hwd1yregyf1dudqwkx85x5ps3qsrqw3ihxpx3ieopq6ukuuq597p6m8161c.git", url( @@ -111,7 +111,7 @@ mod ports { use gix_url::Scheme; #[test] - fn max_valid_port() -> gix_error::TestResult { + fn max_valid_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:65535/repo", url(Scheme::Ssh, None, "host.xz", 65535, b"/repo"), @@ -119,7 +119,7 @@ mod ports { } #[test] - fn port_one() -> gix_error::TestResult { + fn port_one() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:1/repo", url(Scheme::Ssh, None, "host.xz", 1, b"/repo"), @@ -133,7 +133,7 @@ mod git { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn username_expansion_with_username() -> gix_error::TestResult { + fn username_expansion_with_username() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "git://example.com/~byron/hello", url(Scheme::Git, None, "example.com", None, b"~byron/hello"), @@ -141,14 +141,14 @@ mod git { } #[test] - fn default_port_is_9418() -> gix_error::TestResult { + fn default_port_is_9418() -> gix_testtools::TestResult { let url = url(Scheme::Git, None, "example.com", None, b"/repo"); assert_eq!(url.port_or_default(), Some(9418)); Ok(()) } #[test] - fn git_with_explicit_port() -> gix_error::TestResult { + fn git_with_explicit_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "git://example.com:1234/repo", url(Scheme::Git, None, "example.com", 1234, b"/repo"), @@ -162,7 +162,7 @@ mod unknown { use crate::parse::{assert_url_roundtrip, url}; #[test] - fn any_protocol_is_supported_via_a_remote_helper_url() -> gix_error::TestResult { + fn any_protocol_is_supported_via_a_remote_helper_url() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "abc://example.com/~byron/hello", url( diff --git a/gix-url/tests/url/parse/remote_helper.rs b/gix-url/tests/url/parse/remote_helper.rs index 72dbb0b63e1..faa8d363af3 100644 --- a/gix-url/tests/url/parse/remote_helper.rs +++ b/gix-url/tests/url/parse/remote_helper.rs @@ -3,7 +3,7 @@ use gix_url::Scheme; use crate::parse::{assert_url, assert_url_roundtrip, url_alternate}; #[test] -fn address_may_contain_a_url() -> gix_error::TestResult { +fn address_may_contain_a_url() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "codecommit::eu-central-1://myaccount@my-repo", helper("codecommit", b"eu-central-1://myaccount@my-repo"), @@ -11,7 +11,7 @@ fn address_may_contain_a_url() -> gix_error::TestResult { } #[test] -fn address_is_not_interpreted() -> gix_error::TestResult { +fn address_is_not_interpreted() -> gix_testtools::TestResult { for (input, name, address) in [ ("transport::address", "transport", &b"address"[..]), ("myhelper::/abs/path", "myhelper", b"/abs/path"), @@ -29,7 +29,7 @@ fn address_is_not_interpreted() -> gix_error::TestResult { } #[test] -fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> gix_error::TestResult { +fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> gix_testtools::TestResult { assert_eq!( Scheme::from("ext"), Scheme::Ext, @@ -62,14 +62,14 @@ fn ext_commands_are_normalized_and_distinguishable_from_other_helpers() -> gix_e } #[test] -fn address_may_be_empty_or_contain_more_separators() -> gix_error::TestResult { +fn address_may_be_empty_or_contain_more_separators() -> gix_testtools::TestResult { assert_url_roundtrip("foo::", helper("foo", b""))?; assert_url_roundtrip("foo::a::b", helper("foo", b"a::b"))?; Ok(assert_url_roundtrip("a:::b", helper("a", b":b"))?) } #[test] -fn address_may_be_an_arbitrary_command_line() -> gix_error::TestResult { +fn address_may_be_an_arbitrary_command_line() -> gix_testtools::TestResult { // Helpers can be as flexible as `git-remote-bash` running `eval "$2"`, so nothing about the // address may be assumed. Ok(assert_url_roundtrip( @@ -82,7 +82,7 @@ fn address_may_be_an_arbitrary_command_line() -> gix_error::TestResult { } #[test] -fn a_single_letter_name_is_not_a_dos_drive_letter() -> gix_error::TestResult { +fn a_single_letter_name_is_not_a_dos_drive_letter() -> gix_testtools::TestResult { // A DOS drive letter is followed by a single `:`, so these remain remote helpers on all platforms, // just like in Git. assert_url_roundtrip("c::foo", helper("c", b"foo"))?; @@ -90,7 +90,7 @@ fn a_single_letter_name_is_not_a_dos_drive_letter() -> gix_error::TestResult { } #[test] -fn helper_names_shadow_built_in_schemes() -> gix_error::TestResult { +fn helper_names_shadow_built_in_schemes() -> gix_testtools::TestResult { for name in ["ssh", "file", "git", "http", "https"] { assert_url_roundtrip(&format!("{name}::address"), helper(name, b"address"))?; } @@ -98,7 +98,7 @@ fn helper_names_shadow_built_in_schemes() -> gix_error::TestResult { } #[test] -fn helper_names_are_case_sensitive() -> gix_error::TestResult { +fn helper_names_are_case_sensitive() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "CodeCommit::address", helper("CodeCommit", b"address"), @@ -106,7 +106,7 @@ fn helper_names_are_case_sensitive() -> gix_error::TestResult { } #[test] -fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> gix_error::TestResult { +fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> gix_testtools::TestResult { for name in ["a1.2+3-4", "9foo", "foo.bar", "foo-bar", "foo+bar"] { assert_url_roundtrip(&format!("{name}::address"), helper(name, b"address"))?; } @@ -114,7 +114,7 @@ fn helper_names_may_contain_special_characters_and_start_with_a_digit() -> gix_e } #[test] -fn addresses_may_contain_arbitrary_bytes() -> gix_error::TestResult { +fn addresses_may_contain_arbitrary_bytes() -> gix_testtools::TestResult { let url = gix_url::parse(bstr::BStr::new(b"foo::\xff\xfe"))?; assert_eq!(url.scheme, Scheme::Helper("foo".into()), "the name is always ASCII"); assert_eq!( @@ -131,7 +131,7 @@ fn addresses_may_contain_arbitrary_bytes() -> gix_error::TestResult { } #[test] -fn transport_form_and_url_form_are_distinguishable() -> gix_error::TestResult { +fn transport_form_and_url_form_are_distinguishable() -> gix_testtools::TestResult { let helper_form = assert_url("codecommit::my-repo", helper("codecommit", b"my-repo"))?; let url_form = assert_url( "codecommit://my-repo", @@ -162,7 +162,7 @@ fn from_parts_rejects_invalid_helper_names() { } #[test] -fn helper_form_is_always_retained() -> gix_error::TestResult { +fn helper_form_is_always_retained() -> gix_testtools::TestResult { for alt in [false, true] { for input in ["9foo::address", "ssh::address", "foo::address", "ext::address"] { let url = gix_url::parse(input)?.with_request_alternate_form(alt); @@ -183,7 +183,7 @@ mod not_a_remote_helper { use crate::parse::{assert_url, url_alternate}; #[test] - fn names_with_characters_git_does_not_accept() -> gix_error::TestResult { + fn names_with_characters_git_does_not_accept() -> gix_testtools::TestResult { // `_` and `%` are not part of `[A-Za-z0-9][A-Za-z0-9+.-]*`, so Git falls back to SCP-like syntax. for (input, host, path) in [("foo_bar::baz", "foo_bar", &b":baz"[..]), ("f%o::bar", "f%o", b":bar")] { assert_url(input, url_alternate(Scheme::Ssh, None, host, None, path))?; @@ -192,7 +192,7 @@ mod not_a_remote_helper { } #[test] - fn names_that_do_not_start_with_an_alphanumeric_character() -> gix_error::TestResult { + fn names_that_do_not_start_with_an_alphanumeric_character() -> gix_testtools::TestResult { assert_url(".foo::bar", url_alternate(Scheme::Ssh, None, ".foo", None, b":bar"))?; assert!( gix_url::parse("::bar").is_err(), @@ -202,7 +202,7 @@ mod not_a_remote_helper { } #[test] - fn a_single_colon_does_not_start_an_address() -> gix_error::TestResult { + fn a_single_colon_does_not_start_an_address() -> gix_testtools::TestResult { // Note that the name is longer than one character on purpose, as a single one would be a DOS // drive letter on Windows, which is decided after this and is unrelated to remote helpers. assert_url("ab:b::c", url_alternate(Scheme::Ssh, None, "ab", None, b"b::c"))?; diff --git a/gix-url/tests/url/parse/ssh.rs b/gix-url/tests/url/parse/ssh.rs index bc035342dc9..c47f7329e77 100644 --- a/gix-url/tests/url/parse/ssh.rs +++ b/gix-url/tests/url/parse/ssh.rs @@ -3,7 +3,7 @@ use gix_url::Scheme; use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate, url_with_pass}; #[test] -fn without_user_and_without_port() -> gix_error::TestResult { +fn without_user_and_without_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz/path/to/repo.git/", url(Scheme::Ssh, None, "host.xz", None, b"/path/to/repo.git/"), @@ -11,7 +11,7 @@ fn without_user_and_without_port() -> gix_error::TestResult { } #[test] -fn without_user_and_with_port() -> gix_error::TestResult { +fn without_user_and_with_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://host.xz:21/", url(Scheme::Ssh, None, "host.xz", 21, b"/"), @@ -19,7 +19,7 @@ fn without_user_and_with_port() -> gix_error::TestResult { } #[test] -fn host_is_ipv4() -> gix_error::TestResult { +fn host_is_ipv4() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://127.69.0.1/hello", url(Scheme::Ssh, None, "127.69.0.1", None, b"/hello"), @@ -27,7 +27,7 @@ fn host_is_ipv4() -> gix_error::TestResult { } #[test] -fn username_expansion_with_username() -> gix_error::TestResult { +fn username_expansion_with_username() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://example.com/~byron/hello/git", url(Scheme::Ssh, None, "example.com", None, b"~byron/hello/git"), @@ -35,7 +35,7 @@ fn username_expansion_with_username() -> gix_error::TestResult { } #[test] -fn username_expansion_without_username() -> gix_error::TestResult { +fn username_expansion_without_username() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://example.com/~/hello/git", url(Scheme::Ssh, None, "example.com", None, b"~/hello/git"), @@ -43,7 +43,7 @@ fn username_expansion_without_username() -> gix_error::TestResult { } #[test] -fn scp_like_with_ssh_host_alias() -> gix_error::TestResult { +fn scp_like_with_ssh_host_alias() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "user@alias:username/repo.git", url_alternate(Scheme::Ssh, "user", "alias", None, b"username/repo.git"), @@ -51,7 +51,7 @@ fn scp_like_with_ssh_host_alias() -> gix_error::TestResult { } #[test] -fn with_user_and_without_port() -> gix_error::TestResult { +fn with_user_and_without_port() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://user@host.xz/.git", url(Scheme::Ssh, "user", "host.xz", None, b"/.git"), @@ -59,7 +59,7 @@ fn with_user_and_without_port() -> gix_error::TestResult { } #[test] -fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { +fn username_with_dot_is_not_percent_encoded() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://user.name@host.xz/.git", url(Scheme::Ssh, "user.name", "host.xz", None, b"/.git"), @@ -67,7 +67,7 @@ fn username_with_dot_is_not_percent_encoded() -> gix_error::TestResult { } #[test] -fn with_user_and_port_and_absolute_path() -> gix_error::TestResult { +fn with_user_and_port_and_absolute_path() -> gix_testtools::TestResult { Ok(assert_url_roundtrip( "ssh://user@host.xz:42/.git", url(Scheme::Ssh, "user", "host.xz", 42, b"/.git"), @@ -75,7 +75,7 @@ fn with_user_and_port_and_absolute_path() -> gix_error::TestResult { } #[test] -fn ssh_alias_without_username() -> gix_error::TestResult { +fn ssh_alias_without_username() -> gix_testtools::TestResult { let url = assert_url( "host:/path/to/git", url_alternate(Scheme::Ssh, None, "host", None, b"/path/to/git"), @@ -86,14 +86,14 @@ fn ssh_alias_without_username() -> gix_error::TestResult { } #[test] -fn default_port_is_22() -> gix_error::TestResult { +fn default_port_is_22() -> gix_testtools::TestResult { let url = url_alternate(Scheme::Ssh, None, "host.xz", None, b"path/to/git"); assert_eq!(url.port_or_default(), Some(22)); Ok(()) } #[test] -fn scp_like_without_user() -> gix_error::TestResult { +fn scp_like_without_user() -> gix_testtools::TestResult { let url = assert_url( "host.xz:path/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"path/to/git"), @@ -104,7 +104,7 @@ fn scp_like_without_user() -> gix_error::TestResult { } #[test] -fn scp_like_with_absolute_path() -> gix_error::TestResult { +fn scp_like_with_absolute_path() -> gix_testtools::TestResult { let url = assert_url( "host.xz:/path/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"/path/to/git"), @@ -115,7 +115,7 @@ fn scp_like_with_absolute_path() -> gix_error::TestResult { } #[test] -fn scp_like_with_absolute_path_with_whitespace() -> gix_error::TestResult { +fn scp_like_with_absolute_path_with_whitespace() -> gix_testtools::TestResult { let url = assert_url( "host.xz:/path/to/git with space", url_alternate(Scheme::Ssh, None, "host.xz", None, b"/path/to/git with space"), @@ -126,7 +126,7 @@ fn scp_like_with_absolute_path_with_whitespace() -> gix_error::TestResult { } #[test] -fn scp_like_without_user_and_username_expansion_without_username() -> gix_error::TestResult { +fn scp_like_without_user_and_username_expansion_without_username() -> gix_testtools::TestResult { let url = assert_url( "host.xz:~/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"~/to/git"), @@ -137,7 +137,7 @@ fn scp_like_without_user_and_username_expansion_without_username() -> gix_error: } #[test] -fn scp_like_without_user_and_username_expansion_with_username() -> gix_error::TestResult { +fn scp_like_without_user_and_username_expansion_with_username() -> gix_testtools::TestResult { let url = assert_url( "host.xz:~byron/to/git", url_alternate(Scheme::Ssh, None, "host.xz", None, b"~byron/to/git"), @@ -148,7 +148,7 @@ fn scp_like_without_user_and_username_expansion_with_username() -> gix_error::Te } #[test] -fn scp_like_with_user_and_relative_path_keep_relative_path() -> gix_error::TestResult { +fn scp_like_with_user_and_relative_path_keep_relative_path() -> gix_testtools::TestResult { let url = assert_url( "user@host.xz:relative", url_alternate(Scheme::Ssh, "user", "host.xz", None, b"relative"), @@ -187,7 +187,7 @@ fn scp_like_with_user_and_relative_path_keep_relative_path() -> gix_error::TestR } #[test] -fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> gix_error::TestResult { +fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> gix_testtools::TestResult { assert_eq!( gix_url::parse("user@host.xz:relative")? .with_request_alternate_form(false) @@ -222,7 +222,7 @@ fn canonical_form_is_used_only_if_it_preserves_scp_path_semantics() -> gix_error } #[test] -fn scp_like_with_windows_path() -> gix_error::TestResult { +fn scp_like_with_windows_path() -> gix_testtools::TestResult { let url = assert_url( "user@host.xz:C:/strange/absolute/path", url_alternate(Scheme::Ssh, "user", "host.xz", None, b"C:/strange/absolute/path"), @@ -233,7 +233,7 @@ fn scp_like_with_windows_path() -> gix_error::TestResult { } #[test] -fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> gix_error::TestResult { +fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> gix_testtools::TestResult { let url = gix_url::parse("user@host.xz:42:C:/strange/absolute/path")?; assert_eq!( url.to_bstring(), @@ -249,7 +249,7 @@ fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> gix_erro } #[test] -fn scp_like_with_non_alphanumeric_username() -> gix_error::TestResult { +fn scp_like_with_non_alphanumeric_username() -> gix_testtools::TestResult { let url = assert_url( "_user.name@host.xz:C:/path", url_alternate(Scheme::Ssh, "_user.name", "host.xz", None, b"C:/path"), @@ -262,7 +262,7 @@ fn scp_like_with_non_alphanumeric_username() -> gix_error::TestResult { // Git passes the non-path part "user@name@host.xz" to OpenSSH, and the ssh // command interprets it as user = "user@name", host = "host.xz". #[test] -fn scp_like_with_username_including_at() -> gix_error::TestResult { +fn scp_like_with_username_including_at() -> gix_testtools::TestResult { let url = assert_url( "user@name@host.xz:path", url_alternate(Scheme::Ssh, "user@name", "host.xz", None, b"path"), @@ -273,7 +273,7 @@ fn scp_like_with_username_including_at() -> gix_error::TestResult { } #[test] -fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> gix_error::TestResult { +fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> gix_testtools::TestResult { let input = "[:[@]:\x1a"; let url = gix_url::parse(input)?; assert_eq!(url.user(), Some("[:["), "OpenSSH splits user and host at the last @"); @@ -287,14 +287,14 @@ fn scp_like_username_with_colon_is_not_mistaken_for_a_password() -> gix_error::T // I btw tested this, yes you can really clone a repository from there, just `git init` // in the directory above your home directory on the remote machine. #[test] -fn strange_scp_like_with_host_named_file() -> gix_error::TestResult { +fn strange_scp_like_with_host_named_file() -> gix_testtools::TestResult { let url = assert_url("file:..", url_alternate(Scheme::Ssh, None, "file", None, b".."))?; assert_eq!(url.to_bstring(), "file:.."); Ok(()) } #[test] -fn bad_alternative_form_with_password() -> gix_error::TestResult { +fn bad_alternative_form_with_password() -> gix_testtools::TestResult { let password = std::process::id().to_string(); let url = url_with_pass(Scheme::Ssh, "user", password.as_str(), "host.xz", None, b"/") .with_request_alternate_form(true) @@ -304,21 +304,21 @@ fn bad_alternative_form_with_password() -> gix_error::TestResult { } #[test] -fn bad_alternative_form_with_port() -> gix_error::TestResult { +fn bad_alternative_form_with_port() -> gix_testtools::TestResult { let url = url_alternate(Scheme::Ssh, None, "host.xz", 21, b"/").to_bstring(); assert_eq!(url, "ssh://host.xz:21/"); Ok(()) } #[test] -fn ipv6_address_without_port() -> gix_error::TestResult { +fn ipv6_address_without_port() -> gix_testtools::TestResult { let url = assert_url("ssh://[::1]/repo", url(Scheme::Ssh, None, "::1", None, b"/repo"))?; assert_eq!(url.host(), Some("::1"), "brackets are stripped for SSH"); Ok(()) } #[test] -fn ipv6_address_with_port() -> gix_error::TestResult { +fn ipv6_address_with_port() -> gix_testtools::TestResult { let url = assert_url("ssh://[::1]:22/repo", url(Scheme::Ssh, None, "::1", 22, b"/repo"))?; assert_eq!(url.host(), Some("::1")); assert_eq!(url.port, Some(22)); @@ -326,7 +326,7 @@ fn ipv6_address_with_port() -> gix_error::TestResult { } #[test] -fn ipv6_address_with_user() -> gix_error::TestResult { +fn ipv6_address_with_user() -> gix_testtools::TestResult { let url = assert_url("ssh://user@[::1]/repo", url(Scheme::Ssh, "user", "::1", None, b"/repo"))?; assert_eq!(url.host(), Some("::1")); assert_eq!(url.user(), Some("user")); @@ -334,7 +334,7 @@ fn ipv6_address_with_user() -> gix_error::TestResult { } #[test] -fn ipv6_address_with_user_and_port() -> gix_error::TestResult { +fn ipv6_address_with_user_and_port() -> gix_testtools::TestResult { let url = assert_url( "ssh://user@[::1]:22/repo", url(Scheme::Ssh, "user", "::1", 22, b"/repo"), @@ -346,7 +346,7 @@ fn ipv6_address_with_user_and_port() -> gix_error::TestResult { } #[test] -fn ipv6_full_address() -> gix_error::TestResult { +fn ipv6_full_address() -> gix_testtools::TestResult { let url = assert_url( "ssh://[2001:db8::1]/repo", url(Scheme::Ssh, None, "2001:db8::1", None, b"/repo"), @@ -356,7 +356,7 @@ fn ipv6_full_address() -> gix_error::TestResult { } #[test] -fn scoped_ipv6_address() -> gix_error::TestResult { +fn scoped_ipv6_address() -> gix_testtools::TestResult { let input = "ssh://[fe80::1%25Eth0]/repo"; let url = gix_url::parse(input)?; assert_eq!( @@ -370,7 +370,7 @@ fn scoped_ipv6_address() -> gix_error::TestResult { } #[test] -fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> gix_error::TestResult { +fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> gix_testtools::TestResult { let input = "[fe80::1%Eth0]:repo"; let url = gix_url::parse(input)?; assert_eq!(url.host(), Some("fe80::1%Eth0"), "the raw percent sign is host data"); @@ -384,7 +384,7 @@ fn scp_like_scoped_ipv6_address_uses_a_raw_zone_separator() -> gix_error::TestRe } #[test] -fn scoped_ipv6_address_with_empty_port() -> gix_error::TestResult { +fn scoped_ipv6_address_with_empty_port() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://[fe80::1%25Eth0]:/repo")?; assert_eq!( url.host(), @@ -401,7 +401,7 @@ fn scoped_ipv6_address_with_empty_port() -> gix_error::TestResult { } #[test] -fn bracketed_host_is_percent_decoded_once() -> gix_error::TestResult { +fn bracketed_host_is_percent_decoded_once() -> gix_testtools::TestResult { let url = gix_url::parse("ssh://[::81ssssssssssssssssssssssssssssssssssssss%2585]:/00%2585]://")?; assert_eq!( url.host(), @@ -413,7 +413,7 @@ fn bracketed_host_is_percent_decoded_once() -> gix_error::TestResult { } #[test] -fn escaped_authority_delimiters_remain_host_data() -> gix_error::TestResult { +fn escaped_authority_delimiters_remain_host_data() -> gix_testtools::TestResult { for (input, expected_host) in [ ("ssh://host%3A123/repo", "host:123"), ("ssh://host%2Fname/repo", "host/name"), @@ -439,7 +439,7 @@ fn escaped_authority_delimiters_remain_host_data() -> gix_error::TestResult { } #[test] -fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> gix_error::TestResult { +fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> gix_testtools::TestResult { for (input, decoded_path, original_path, message) in [ ( "ssh://example.com/a%2Fb", @@ -463,14 +463,14 @@ fn percent_encoded_paths_are_decoded_and_the_original_is_retained() -> gix_error } #[test] -fn ipv6_address_scp_like() -> gix_error::TestResult { +fn ipv6_address_scp_like() -> gix_testtools::TestResult { let url = assert_url("[::1]:repo", url_alternate(Scheme::Ssh, None, "::1", None, b"repo"))?; assert_eq!(url.host(), Some("::1"), "SCP-like format with IPv6"); Ok(()) } #[test] -fn ipv6_address_scp_like_with_user() -> gix_error::TestResult { +fn ipv6_address_scp_like_with_user() -> gix_testtools::TestResult { let result = gix_url::parse("user@[::1]:repo"); assert!( result.is_err(), diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index e4302a613c6..fe432e91c5b 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, io::Write, path::{Path, PathBuf}, }; @@ -83,7 +82,7 @@ pub fn checkout<'entry, Find>( where Find: gix_object::Find, { - let dest_relative = gix_path::try_from_bstr(entry_path) + let dest_relative = gix_path::from_bstr(entry_path) .or_raise(|| validation("Could not convert path to UTF8").with_input(entry_path))?; let path_cache = path_cache .at_path(dest_relative.as_ref(), Some(entry.mode), &*objects) @@ -158,15 +157,8 @@ where ) })?; if symlink { - #[cfg_attr(not(windows), allow(unused_mut))] - let mut symlink_destination = Cow::Borrowed( - gix_path::try_from_byte_slice(obj.data) - .or_raise(|| validation("Could not convert path to UTF8").with_input(obj.data))?, - ); - #[cfg(windows)] - { - symlink_destination = gix_path::to_native_path_on_windows(gix_path::into_bstr(symlink_destination)) - } + let symlink_destination = gix_path::to_native_path_on_windows(BStr::new(obj.data)) + .or_raise(|| validation("Could not convert path to UTF8").with_input(obj.data))?; try_op_or_unlink(dest, overwrite_existing, |p| { gix_fs::symlink::create(symlink_destination.as_ref(), p) @@ -386,7 +378,7 @@ pub(crate) enum ExecutableBitChange { mod tests { #[test] #[cfg(windows)] - fn forced_operations_never_receive_terminal_symlinks() -> gix_testtools::Result { + fn forced_operations_never_receive_terminal_symlinks() -> gix_testtools::TestResult { let dir = gix_testtools::tempfile::tempdir()?; let target = dir.path().join("target"); let link = dir.path().join("link"); @@ -417,7 +409,7 @@ mod tests { #[test] #[cfg(any(windows, unix))] - fn nonexclusive_open_defers_truncation_until_handle_validation() -> gix_testtools::Result { + fn nonexclusive_open_defers_truncation_until_handle_validation() -> gix_testtools::TestResult { use std::io::Write; let dir = gix_testtools::tempfile::tempdir()?; @@ -455,7 +447,7 @@ mod tests { #[test] #[cfg(any(windows, unix))] - fn terminal_symlinks_created_after_precheck_are_rejected_or_replaced() -> gix_testtools::Result { + fn terminal_symlinks_created_after_precheck_are_rejected_or_replaced() -> gix_testtools::TestResult { use std::{ io::Write, sync::atomic::{AtomicUsize, Ordering}, diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index a88d11f7fa9..3c26a50ec95 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -1,4 +1,5 @@ use crate::Result; +use gix_testtools::TestResult; #[cfg(unix)] use std::os::unix::prelude::{MetadataExt, PermissionsExt}; use std::{ @@ -33,7 +34,7 @@ fn assure_is_empty(dir: impl AsRef<Path>) -> std::io::Result<()> { } #[test] -fn submodules_are_instantiated_as_directories() -> Result { +fn submodules_are_instantiated_as_directories() -> TestResult { let mut opts = opts_from_probe(); opts.overwrite_existing = false; let (_source_tree, destination, _index, _outcome) = checkout_index_in_tmp_dir(opts.clone(), "make_mixed", None)?; @@ -123,7 +124,7 @@ fn writes_through_symlinks_are_prevented_even_if_overwriting_is_allowed() { } #[test] -fn checkout_does_not_follow_terminal_symlinks() -> Result { +fn checkout_does_not_follow_terminal_symlinks() -> TestResult { use gix_index::entry::Mode; use gix_object::Write; @@ -248,7 +249,7 @@ fn checkout_does_not_follow_terminal_symlinks() -> Result { } #[test] -fn delayed_symlinks_do_not_reuse_replaced_directory_prefixes() -> gix_testtools::Result { +fn delayed_symlinks_do_not_reuse_replaced_directory_prefixes() -> gix_testtools::TestResult { use gix_index::entry::Mode; use gix_object::Write; @@ -272,8 +273,12 @@ fn delayed_symlinks_do_not_reuse_replaced_directory_prefixes() -> gix_testtools: let object_hash = gix_testtools::object_hash(); let objects = gix_odb::memory::Proxy::new(gix_object::find::Never, object_hash); - let directory_blob_id = - objects.write_buf(gix_object::Kind::Blob, gix_path::into_bstr(outside.path()).as_ref())?; + let directory_blob_id = objects.write_buf( + gix_object::Kind::Blob, + gix_path::into_bstr(outside.path()) + .expect("fixture path is representable") + .as_ref(), + )?; let link_blob_id = objects.write_buf(gix_object::Kind::Blob, b"target")?; let seed_blob_id = objects.write_buf(gix_object::Kind::Blob, b"seed")?; let child_path = format!("{prefix}/canary"); @@ -353,7 +358,7 @@ fn delayed_symlinks_do_not_reuse_replaced_directory_prefixes() -> gix_testtools: } #[test] -fn filter_spawn_errors_are_not_collisions() -> Result { +fn filter_spawn_errors_are_not_collisions() -> TestResult { let mut error_snapshots = Vec::new(); // A relative path with forward slashes makes spawning fail directly, without a shell. let dir = gix_testtools::tempfile::tempdir_in(".")?; @@ -370,7 +375,12 @@ fn filter_spawn_errors_are_not_collisions() -> Result { opts.filters.options_mut().drivers = vec![gix_filter::Driver { name: "arrow".into(), clean: None, - smudge: Some(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&program)).into_owned()), + smudge: Some( + gix_path::to_unix_separators_on_windows( + gix_path::into_bstr(&program).expect("fixture path is representable"), + ) + .into_owned(), + ), process: None, required: true, }]; @@ -430,7 +440,7 @@ fn filter_spawn_errors_are_not_collisions() -> Result { } #[test] -fn delayed_driver_process() -> Result { +fn delayed_driver_process() -> TestResult { let mut opts = opts_from_probe(); opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; setup_filter_pipeline(opts.filters.options_mut()); @@ -477,7 +487,7 @@ fn delayed_driver_process() -> Result { } #[test] -fn filter_process_failure_during_shutdown_is_ignored() -> Result { +fn filter_process_failure_during_shutdown_is_ignored() -> TestResult { let mut opts = opts_from_probe(); setup_filter_pipeline(opts.filters.options_mut()); opts.filters @@ -517,7 +527,7 @@ fn forgotten_delayed_path_is_a_corruption_error() { #[cfg(unix)] #[test] -fn delayed_driver_process_removes_obsolete_executable_bits() -> Result { +fn delayed_driver_process_removes_obsolete_executable_bits() -> TestResult { let mut opts = opts_from_probe(); opts.destination_is_initially_empty = false; opts.filter_process_delay = gix_filter::driver::apply::Delay::Allow; @@ -547,7 +557,7 @@ fn delayed_driver_process_removes_obsolete_executable_bits() -> Result { #[cfg(unix)] #[test] -fn nonexclusive_checkout_adjusts_executable_bits_in_both_directions() -> Result { +fn nonexclusive_checkout_adjusts_executable_bits_in_both_directions() -> TestResult { for overwrite_existing in [false, true] { let mut opts = opts_from_probe(); opts.destination_is_initially_empty = false; @@ -590,7 +600,7 @@ fn nonexclusive_checkout_adjusts_executable_bits_in_both_directions() -> Result } #[test] -fn overwriting_files_and_lone_directories_works() -> Result { +fn overwriting_files_and_lone_directories_works() -> TestResult { for delay in [ gix_filter::driver::apply::Delay::Allow, gix_filter::driver::apply::Delay::Forbid, @@ -688,7 +698,7 @@ fn overwriting_files_and_lone_directories_works() -> Result { } #[test] -fn symlinks_become_files_if_disabled() -> Result { +fn symlinks_become_files_if_disabled() -> TestResult { let mut opts = opts_from_probe(); opts.fs.symlink = false; let (source_tree, destination, _index, outcome) = @@ -700,7 +710,7 @@ fn symlinks_become_files_if_disabled() -> Result { } #[test] -fn symlinks_to_directories_are_usable() -> Result { +fn symlinks_to_directories_are_usable() -> TestResult { let opts = opts_from_probe(); assert!(opts.fs.symlink, "The probe must detect to be able to generate symlinks"); @@ -711,25 +721,15 @@ fn symlinks_to_directories_are_usable() -> Result { assert_eq!(worktree_files_stripped, paths(["symlink"])); let symlink_path = &worktree_files[0]; - assert!( - symlink_path - .symlink_metadata() - .expect("symlink is on disk") - .is_symlink() - ); - assert!( - symlink_path - .metadata() - .expect("metadata accessible through symlink") - .is_dir() - ); + assert!(symlink_path.symlink_metadata()?.is_symlink()); + assert!(symlink_path.metadata()?.is_dir()); assert_eq!(std::fs::read_link(symlink_path)?, Path::new(".")); assert!(outcome.collisions.is_empty()); Ok(()) } #[test] -fn dangling_symlinks_can_be_created() -> Result { +fn dangling_symlinks_can_be_created() -> TestResult { let opts = opts_from_probe(); assert!(opts.fs.symlink, "The probe must detect to be able to generate symlinks"); @@ -752,12 +752,7 @@ fn dangling_symlinks_can_be_created() -> Result { assert_eq!(worktree_files_stripped, paths([symlink_name])); let symlink_path = &worktree_files[0]; - assert!( - symlink_path - .symlink_metadata() - .expect("dangling symlink is on disk") - .is_symlink() - ); + assert!(symlink_path.symlink_metadata()?.is_symlink()); assert_eq!(std::fs::read_link(symlink_path)?, Path::new(target_name)); assert!(outcome.collisions.is_empty()); } @@ -766,7 +761,7 @@ fn dangling_symlinks_can_be_created() -> Result { } #[test] -fn allow_or_disallow_symlinks() -> Result { +fn allow_or_disallow_symlinks() -> TestResult { let mut opts = opts_from_probe(); for allowed in &[false, true] { opts.fs.symlink = *allowed; @@ -867,7 +862,7 @@ fn no_case_related_collisions_on_case_sensitive_filesystem() { } #[test] -fn safety_checks_dotdot_trees() { +fn safety_checks_dotdot_trees() -> TestResult { let mut opts = opts_from_probe(); let err = checkout_index_in_tmp_dir(opts.clone(), "make_traverse_trees", Some("traverse_dotdot_trees")).unwrap_err(); @@ -880,8 +875,7 @@ fn safety_checks_dotdot_trees() { opts.keep_going = true; let (_source_tree, _destination, _index, outcome) = - checkout_index_in_tmp_dir(opts, "make_traverse_trees", Some("traverse_dotdot_trees")) - .expect("keep-going checks out as much as possible"); + checkout_index_in_tmp_dir(opts, "make_traverse_trees", Some("traverse_dotdot_trees"))?; assert_eq!(outcome.errors.len(), 1, "one path could not be checked out"); insta::assert_debug_snapshot!(outcome.errors[0].error, "safety checks dotdot trees", @r#" I/O error (Other) @@ -889,6 +883,7 @@ fn safety_checks_dotdot_trees() { Caused by: 0: Input path "../outside" contains relative or absolute components "#); + Ok(()) } #[test] @@ -1147,7 +1142,7 @@ fn setup_filter_pipeline(opts: &mut gix_filter::pipeline::Options) { } #[test] -fn checkout_truncates_existing_longer_files() -> Result { +fn checkout_truncates_existing_longer_files() -> TestResult { let mut opts = opts_from_probe(); opts.overwrite_existing = false; opts.destination_is_initially_empty = false; diff --git a/gix-worktree-state/tests/worktree-state/main.rs b/gix-worktree-state/tests/worktree-state/main.rs index 248aa2cd440..723f5956471 100644 --- a/gix-worktree-state/tests/worktree-state/main.rs +++ b/gix-worktree-state/tests/worktree-state/main.rs @@ -2,7 +2,7 @@ mod checkout; use std::path::{Path, PathBuf}; -pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>; +pub use gix_testtools::Result; pub use gix_testtools::scripted_fixture_read_only; diff --git a/gix-worktree-stream/src/lib.rs b/gix-worktree-stream/src/lib.rs index a4351e24447..5a8cacd60e4 100644 --- a/gix-worktree-stream/src/lib.rs +++ b/gix-worktree-stream/src/lib.rs @@ -133,12 +133,17 @@ impl Stream { ) -> std::io::Result<&mut Self> { let rela_path = path.strip_prefix(root).map_err(std::io::Error::other)?; let meta = path.symlink_metadata()?; - let relative_path = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).into_owned(); + let relative_path = gix_path::to_unix_separators_on_windows( + gix_path::into_bstr(rela_path).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?, + ) + .into_owned(); let id = object_hash.null(); let entry = if meta.is_symlink() { let content = std::fs::read_link(path)?; - let content = gix_path::into_bstr(content).into_owned(); + let content = gix_path::into_bstr(content) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))? + .into_owned(); AdditionalEntry { id, mode: gix_object::tree::EntryKind::Link.into(), diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index bc4642c0a78..a567dd9a13b 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -95,7 +95,7 @@ mod from_tree { } #[test] - fn can_receive_err_if_attribute_not_found() -> gix_testtools::Result { + fn can_receive_err_if_attribute_not_found() -> gix_testtools::TestResult { let (_dir, head_tree, odb, _cache) = basic()?; let mut stream = gix_worktree_stream::from_tree(head_tree, odb, mutating_pipeline(false), |_, _, _| { Err(Error::other("attribute retrieval failed")) @@ -112,7 +112,7 @@ mod from_tree { } #[test] - fn filter_process_failure_during_shutdown_is_ignored() -> gix_testtools::Result { + fn filter_process_failure_during_shutdown_is_ignored() -> gix_testtools::TestResult { let (_dir, head_tree, odb, mut cache) = basic()?; let mut pipeline = mutating_pipeline(true); *pipeline @@ -128,17 +128,14 @@ mod from_tree { .map(|_| ()) }); - while let Some(mut entry) = stream - .next_entry() - .expect("a filter failure after successful conversion is ignored") - { + while let Some(mut entry) = stream.next_entry()? { std::io::copy(&mut entry, &mut std::io::sink())?; } Ok(()) } #[test] - fn will_provide_all_information_and_respect_export_ignore() -> gix_testtools::Result { + fn will_provide_all_information_and_respect_export_ignore() -> gix_testtools::TestResult { let (dir, head_tree, odb, mut cache) = basic()?; let mut stream = gix_worktree_stream::from_tree( head_tree, @@ -167,10 +164,10 @@ mod from_tree { let mut paths_and_modes = Vec::new(); let mut stream = gix_worktree_stream::Stream::from_read(tee_read); - while let Some(mut entry) = stream.next_entry().expect("entry retrieval does not fail") { + while let Some(mut entry) = stream.next_entry()? { paths_and_modes.push((entry.relative_path().to_owned(), entry.mode.kind(), entry.id)); let mut buf = Vec::new(); - entry.read_to_end(&mut buf).expect("stream can always be read"); + entry.read_to_end(&mut buf)?; if !buf.is_empty() && entry.mode.is_blob() { if entry.relative_path().contains_str("extra") { assert!( @@ -296,10 +293,10 @@ mod from_tree { gix_worktree_stream::Stream::from_read(std::io::Cursor::new(copy.lock().as_bytes().to_owned())); let mut copied_paths_and_modes = Vec::new(); let mut buf = Vec::new(); - while let Some(mut entry) = copied_stream.next_entry().expect("entry retrieval does not fail") { + while let Some(mut entry) = copied_stream.next_entry()? { copied_paths_and_modes.push((entry.relative_path().to_owned(), entry.mode.kind(), entry.id)); buf.clear(); - entry.read_to_end(&mut buf).expect("stream can always be read"); + entry.read_to_end(&mut buf)?; } assert_eq!( copied_paths_and_modes, paths_and_modes, @@ -309,7 +306,7 @@ mod from_tree { } #[test] - fn can_drop_entry_without_reading_it() -> gix_testtools::Result { + fn can_drop_entry_without_reading_it() -> gix_testtools::TestResult { let (_dir, head_tree, odb, mut cache) = basic()?; let mut stream = gix_worktree_stream::from_tree( head_tree, @@ -323,7 +320,7 @@ mod from_tree { }, ); - drop(stream.next_entry().expect("entry retrieval does not fail")); + drop(stream.next_entry()?); Ok(()) } diff --git a/gix-worktree/src/add.rs b/gix-worktree/src/add.rs index 3f6cb12f412..920212fc612 100644 --- a/gix-worktree/src/add.rs +++ b/gix-worktree/src/add.rs @@ -137,7 +137,7 @@ pub fn prepare( })?; let basename = gix_path::os_str_into_bstr(basename) .map(gix_validate::reference::name_partial_or_sanitize) - .and_then(gix_path::try_from_bstring) + .and_then(gix_path::from_bstring) .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let git_dir = reserve_git_dir(&worktrees_dir, basename.as_os_str())?; @@ -258,7 +258,7 @@ fn write_path(path: PathBuf, prefix: &[u8], value: &Path) -> io::Result<()> { } fn write_path_to(mut out: impl Write, prefix: &[u8], value: &Path) -> io::Result<()> { - let value = gix_path::try_into_bstr(value).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + let value = gix_path::into_bstr(value).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; let value = gix_path::to_unix_separators_on_windows(value); out.write_all(prefix)?; out.write_all(&value)?; diff --git a/gix-worktree/src/remove.rs b/gix-worktree/src/remove.rs index 37e19fcca80..31dc7c83321 100644 --- a/gix-worktree/src/remove.rs +++ b/gix-worktree/src/remove.rs @@ -500,7 +500,7 @@ mod tests { #[cfg(unix)] #[test] - fn normalized_roots_keep_their_containing_device() -> gix_testtools::Result { + fn normalized_roots_keep_their_containing_device() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let root = super::normalize_root(&tmp.path().join("one-component"))?; assert!(root.is_absolute(), "root normalization preserves absolute paths"); @@ -552,7 +552,7 @@ mod tests { #[cfg(unix)] #[test] - fn filesystem_root_aliases_are_not_traversed() -> gix_testtools::Result { + fn filesystem_root_aliases_are_not_traversed() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let root_link = tmp.path().join("root-link"); gix_fs::symlink::create(std::path::Path::new("/"), &root_link)?; @@ -576,7 +576,7 @@ mod tests { } #[test] - fn retries_when_an_entry_appears_after_scanning() -> gix_testtools::Result { + fn retries_when_an_entry_appears_after_scanning() -> gix_testtools::TestResult { for options in [ Options::default(), Options { @@ -649,7 +649,7 @@ mod tests { } #[test] - fn stops_after_the_configured_number_of_improving_deletion_passes() -> gix_testtools::Result { + fn stops_after_the_configured_number_of_improving_deletion_passes() -> gix_testtools::TestResult { for max_retries in [0, 1, 2, 4] { let tmp = gix_testtools::tempfile::tempdir()?; let root = tmp.path().join("worktree"); diff --git a/gix-worktree/src/stack/delegate.rs b/gix-worktree/src/stack/delegate.rs index edf856c4870..8848710c52f 100644 --- a/gix-worktree/src/stack/delegate.rs +++ b/gix-worktree/src/stack/delegate.rs @@ -31,7 +31,7 @@ pub(crate) struct StackDelegate<'a, 'find> { impl gix_fs::stack::Delegate for StackDelegate<'_, '_> { fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { self.statistics.delegate.push_directory += 1; - let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()); + let rela_dir_bstr = gix_path::into_bstr(stack.current_relative()).map_err(std::io::Error::other)?; let rela_dir = gix_path::to_unix_separators_on_windows(rela_dir_bstr); match &mut self.state { #[cfg(feature = "attributes")] @@ -139,7 +139,7 @@ fn validate_last_component( let Some(last_component) = stack.current_relative().components().next_back() else { return Ok(()); }; - let last_component = gix_path::try_into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref())) + let last_component = gix_path::into_bstr(std::borrow::Cow::Borrowed(last_component.as_os_str().as_ref())) .or_raise(|| { gix_error::message!( "Path component {last_component:?} of path \"{}\" contained invalid UTF-8 and could not be validated", diff --git a/gix-worktree/src/stack/mod.rs b/gix-worktree/src/stack/mod.rs index e8fc6a03908..1d46bf94738 100644 --- a/gix-worktree/src/stack/mod.rs +++ b/gix-worktree/src/stack/mod.rs @@ -1,5 +1,8 @@ #![allow(missing_docs)] -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; use bstr::{BStr, ByteSlice}; @@ -51,6 +54,7 @@ pub enum State { #[must_use] pub struct Platform<'a> { parent: &'a Stack, + relative_path: Cow<'a, BStr>, is_dir: Option<bool>, } @@ -137,8 +141,12 @@ impl Stack { statistics: &mut self.statistics, }; self.stack.make_relative_path_current(relative, &mut delegate)?; + let relative_path = gix_path::to_unix_separators_on_windows( + gix_path::into_bstr(self.stack.current_relative()).map_err(std::io::Error::other)?, + ); Ok(Platform { parent: self, + relative_path, is_dir: mode_is_dir(mode), }) } diff --git a/gix-worktree/src/stack/platform.rs b/gix-worktree/src/stack/platform.rs index 2e3e62ccfe6..fc55ef4c162 100644 --- a/gix-worktree/src/stack/platform.rs +++ b/gix-worktree/src/stack/platform.rs @@ -45,9 +45,7 @@ impl<'a> Platform<'a> { /// If the cache was configured without exclude patterns. pub fn matching_exclude_pattern(&self) -> Option<gix_ignore::search::Match<'_>> { let ignore = self.parent.state.ignore_or_panic(); - let relative_path = - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(self.parent.stack.current_relative())); - ignore.matching_exclude_pattern(relative_path.as_bstr(), self.is_dir, self.parent.case) + ignore.matching_exclude_pattern(self.relative_path.as_bstr(), self.is_dir, self.parent.case) } /// Match all attributes at the current path and store the result in `out`, returning `true` if at least one attribute was found. @@ -58,9 +56,7 @@ impl<'a> Platform<'a> { #[cfg(feature = "attributes")] pub fn matching_attributes(&self, out: &mut gix_attributes::search::Outcome) -> bool { let attrs = self.parent.state.attributes_or_panic(); - let relative_path = - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(self.parent.stack.current_relative())); - attrs.matching_attributes(relative_path.as_bstr(), self.parent.case, self.is_dir, out) + attrs.matching_attributes(self.relative_path.as_bstr(), self.parent.case, self.is_dir, out) } } diff --git a/gix-worktree/src/stack/state/attributes.rs b/gix-worktree/src/stack/state/attributes.rs index 7eda457639c..cae3497c8d5 100644 --- a/gix-worktree/src/stack/state/attributes.rs +++ b/gix-worktree/src/stack/state/attributes.rs @@ -107,14 +107,15 @@ impl Attributes { let blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(std::io::Error::other)?; - let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); + let attr_path = + gix_path::from_bstring(attr_path_relative.into_owned()).map_err(std::io::Error::other)?; self.stack.add_patterns_buffer( blob.data, attr_path, Some(Path::new("")), &mut self.collection, read_macros_as_dir_is_root, - ); + )?; added = true; stats.patterns_buffers += 1; } @@ -146,14 +147,15 @@ impl Attributes { let blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(std::io::Error::other)?; - let attr_path = gix_path::from_bstring(attr_path_relative.into_owned()); + let attr_path = + gix_path::from_bstring(attr_path_relative.into_owned()).map_err(std::io::Error::other)?; self.stack.add_patterns_buffer( blob.data, attr_path, Some(Path::new("")), &mut self.collection, read_macros_as_dir_is_root, - ); + )?; added = true; stats.patterns_buffers += 1; } @@ -163,7 +165,7 @@ impl Attributes { // Need one stack level per component so push and pop matches, but only if this isn't the root level which is never popped. if !added && self.info_attributes.is_none() { self.stack - .add_patterns_buffer(&[], "<empty dummy>".into(), None, &mut self.collection, true); + .add_patterns_buffer(&[], "<empty dummy>".into(), None, &mut self.collection, true)?; } // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is diff --git a/gix-worktree/src/stack/state/ignore.rs b/gix-worktree/src/stack/state/ignore.rs index edddb6cdd4b..2c1293c0da1 100644 --- a/gix-worktree/src/stack/state/ignore.rs +++ b/gix-worktree/src/stack/state/ignore.rs @@ -182,9 +182,14 @@ impl Ignore { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(std::io::Error::other)?; - let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); - self.stack - .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new("")), self.parse); + let ignore_path = + gix_path::from_bstring(ignore_path_relative.into_owned()).map_err(std::io::Error::other)?; + self.stack.add_patterns_buffer( + ignore_blob.data, + ignore_path, + Some(Path::new("")), + self.parse, + )?; stats.patterns_buffers += 1; } Err(_) => { @@ -210,13 +215,14 @@ impl Ignore { let ignore_blob = objects .find_blob(&id_mappings[idx].1, buf) .map_err(std::io::Error::other)?; - let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); + let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()) + .map_err(std::io::Error::other)?; self.stack.add_patterns_buffer( ignore_blob.data, ignore_path, Some(Path::new("")), self.parse, - ); + )?; stats.patterns_buffers += 1; } Err(_) => { diff --git a/gix-worktree/tests/worktree/add.rs b/gix-worktree/tests/worktree/add.rs index 5305ac1488c..a4b81f0d9b0 100644 --- a/gix-worktree/tests/worktree/add.rs +++ b/gix-worktree/tests/worktree/add.rs @@ -4,7 +4,7 @@ use gix_path::{into_bstr, to_unix_separators_on_windows}; use gix_testtools::tempfile::tempdir; #[test] -fn relative_input_paths_are_made_absolute_with_default_options() -> crate::Result { +fn relative_input_paths_are_made_absolute_with_default_options() -> gix_testtools::TestResult { let cwd = std::env::current_dir()?; let tmp = gix_testtools::tempfile::tempdir_in(&cwd)?; let relative = tmp.path().strip_prefix(&cwd)?; @@ -28,7 +28,7 @@ fn relative_input_paths_are_made_absolute_with_default_options() -> crate::Resul } #[test] -fn prepares_git_compatible_links_and_unique_sanitized_names() -> crate::Result { +fn prepares_git_compatible_links_and_unique_sanitized_names() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); fs::create_dir(&common_dir)?; @@ -53,14 +53,14 @@ fn prepares_git_compatible_links_and_unique_sanitized_names() -> crate::Result { fs::read_to_string(first.git_dir().join("gitdir"))?, format!( "{}\n", - to_unix_separators_on_windows(into_bstr(first.work_dir().join(".git"))) + to_unix_separators_on_windows(into_bstr(first.work_dir().join(".git"))?) ) ); assert_eq!( fs::read_to_string(first.work_dir().join(".git"))?, format!( "gitdir: {}\n", - to_unix_separators_on_windows(into_bstr(first.git_dir())) + to_unix_separators_on_windows(into_bstr(first.git_dir())?) ) ); @@ -96,7 +96,7 @@ fn prepares_git_compatible_links_and_unique_sanitized_names() -> crate::Result { } #[test] -fn rollback_preserves_a_caller_owned_empty_directory() -> crate::Result { +fn rollback_preserves_a_caller_owned_empty_directory() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); let work_dir = tmp.path().join("existing"); @@ -121,7 +121,7 @@ fn rollback_preserves_a_caller_owned_empty_directory() -> crate::Result { } #[test] -fn explicit_rollback_removes_new_directories_but_preserves_their_parents() -> crate::Result { +fn explicit_rollback_removes_new_directories_but_preserves_their_parents() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); let work_dir = tmp.path().join("new-parent/worktree"); @@ -146,7 +146,7 @@ fn explicit_rollback_removes_new_directories_but_preserves_their_parents() -> cr } #[test] -fn explicit_rollback_attempts_both_directories_even_if_one_fails() -> crate::Result { +fn explicit_rollback_attempts_both_directories_even_if_one_fails() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); let work_dir = tmp.path().join("worktree"); @@ -171,7 +171,7 @@ fn explicit_rollback_attempts_both_directories_even_if_one_fails() -> crate::Res } #[test] -fn rollback_removes_directory_symlinks_without_touching_their_target() -> crate::Result { +fn rollback_removes_directory_symlinks_without_touching_their_target() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); let work_dir = tmp.path().join("existing"); @@ -203,7 +203,7 @@ fn rollback_removes_directory_symlinks_without_touching_their_target() -> crate: #[test] #[cfg(unix)] -fn linking_paths_preserve_backslashes_in_unix_filenames() -> crate::Result { +fn linking_paths_preserve_backslashes_in_unix_filenames() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join(r"repo\backslash.git"); let work_dir = tmp.path().join(r"with\backslash"); @@ -232,7 +232,7 @@ fn linking_paths_preserve_backslashes_in_unix_filenames() -> crate::Result { #[test] #[cfg(unix)] -fn linking_paths_resolve_symlinked_parent_directories() -> crate::Result { +fn linking_paths_resolve_symlinked_parent_directories() -> gix_testtools::TestResult { let tmp = tempdir()?; let actual_parent = tmp.path().join("actual"); let linked_parent = tmp.path().join("linked"); @@ -268,7 +268,7 @@ fn linking_paths_resolve_symlinked_parent_directories() -> crate::Result { } #[test] -fn rejects_occupied_destinations() -> crate::Result { +fn rejects_occupied_destinations() -> gix_testtools::TestResult { let tmp = tempdir()?; let common_dir = tmp.path().join("repo.git"); fs::create_dir(&common_dir)?; diff --git a/gix-worktree/tests/worktree/remove.rs b/gix-worktree/tests/worktree/remove.rs index 864b4298a8b..533d2999a3e 100644 --- a/gix-worktree/tests/worktree/remove.rs +++ b/gix-worktree/tests/worktree/remove.rs @@ -3,7 +3,7 @@ use std::fs; use gix_worktree::remove::Options; #[test] -fn relative_roots_are_rejected_before_either_root_is_removed() -> crate::Result { +fn relative_roots_are_rejected_before_either_root_is_removed() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -58,7 +58,7 @@ fn relative_roots_are_rejected_before_either_root_is_removed() -> crate::Result } #[test] -fn removes_both_roots_with_configured_thread_limits_without_retries() -> crate::Result { +fn removes_both_roots_with_configured_thread_limits_without_retries() -> gix_testtools::TestResult { for thread_limit in [None, Some(0), Some(1), Some(2)] { let tmp = gix_testtools::tempfile::tempdir()?; let worktrees_dir = tmp.path().join("repo.git/worktrees"); @@ -89,7 +89,7 @@ fn removes_both_roots_with_configured_thread_limits_without_retries() -> crate:: } #[test] -fn removes_both_roots_and_does_not_follow_symlinks() -> crate::Result { +fn removes_both_roots_and_does_not_follow_symlinks() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let worktrees_dir = tmp.path().join("repo.git/worktrees"); let git_dir = worktrees_dir.join("linked"); @@ -126,7 +126,7 @@ fn removes_both_roots_and_does_not_follow_symlinks() -> crate::Result { #[test] #[cfg(unix)] -fn symlink_roots_with_or_without_a_trailing_separator_are_unlinked() -> crate::Result { +fn symlink_roots_with_or_without_a_trailing_separator_are_unlinked() -> gix_testtools::TestResult { for trailing_separator in [false, true] { let tmp = gix_testtools::tempfile::tempdir()?; let work_dir = tmp.path().join("linked"); @@ -156,7 +156,7 @@ fn symlink_roots_with_or_without_a_trailing_separator_are_unlinked() -> crate::R } #[test] -fn missing_roots_are_already_removed() -> crate::Result { +fn missing_roots_are_already_removed() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; gix_worktree::remove( tmp.path().join("missing-worktree"), @@ -176,7 +176,7 @@ fn missing_roots_are_already_removed() -> crate::Result { } #[test] -fn only_the_conventional_empty_worktrees_parent_is_removed() -> crate::Result { +fn only_the_conventional_empty_worktrees_parent_is_removed() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let parent = tmp.path().join("custom-parent"); let git_dir = parent.join("linked"); @@ -195,7 +195,7 @@ fn only_the_conventional_empty_worktrees_parent_is_removed() -> crate::Result { #[test] #[cfg(unix)] -fn unreadable_directories_match_git() -> crate::Result { +fn unreadable_directories_match_git() -> gix_testtools::TestResult { use std::os::unix::fs::PermissionsExt; for empty in [true, false] { @@ -267,7 +267,7 @@ fn unreadable_directories_match_git() -> crate::Result { #[test] #[cfg(unix)] -fn administrative_data_is_removed_after_checkout_removal_fails() -> crate::Result { +fn administrative_data_is_removed_after_checkout_removal_fails() -> gix_testtools::TestResult { use std::os::unix::fs::PermissionsExt; let tmp = gix_testtools::tempfile::tempdir()?; @@ -310,7 +310,7 @@ fn administrative_data_is_removed_after_checkout_removal_fails() -> crate::Resul #[test] #[cfg(windows)] -fn readonly_files_do_not_prevent_removal() -> crate::Result { +fn readonly_files_do_not_prevent_removal() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let work_dir = tmp.path().join("linked"); let git_dir = tmp.path().join("repo.git/worktrees/linked"); diff --git a/gix-worktree/tests/worktree/stack/attributes.rs b/gix-worktree/tests/worktree/stack/attributes.rs index cab580c84d6..89ae7947f5d 100644 --- a/gix-worktree/tests/worktree/stack/attributes.rs +++ b/gix-worktree/tests/worktree/stack/attributes.rs @@ -1,12 +1,12 @@ -use crate::Result; use bstr::ByteSlice; use gix_attributes::search::Outcome; +use gix_testtools::TestResult; use gix_worktree::stack::state; use crate::stack::probe_case; #[test] -fn baseline() -> Result { +fn baseline() -> TestResult { // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this. let case = probe_case()?; diff --git a/gix-worktree/tests/worktree/stack/create_directory.rs b/gix-worktree/tests/worktree/stack/create_directory.rs index 0564db93eed..e4a31af4e69 100644 --- a/gix-worktree/tests/worktree/stack/create_directory.rs +++ b/gix-worktree/tests/worktree/stack/create_directory.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use gix_testtools::tempfile::{TempDir, tempdir}; @@ -9,7 +9,7 @@ const IS_DIR: Option<gix_index::entry::Mode> = Some(gix_index::entry::Mode::DIR) const IS_SYMLINK: Option<gix_index::entry::Mode> = Some(gix_index::entry::Mode::SYMLINK); #[test] -fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> Result { +fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> TestResult { let dir = tempdir()?; let mut cache = Stack::new( dir.path().join("non-existing-root"), @@ -48,7 +48,7 @@ fn directory_paths_are_created_in_full() { } #[test] -fn existing_directories_are_fine() -> Result { +fn existing_directories_are_fine() -> TestResult { let (mut cache, tmp) = new_cache(); std::fs::create_dir(tmp.path().join("dir"))?; @@ -60,7 +60,7 @@ fn existing_directories_are_fine() -> Result { } #[test] -fn validation_to_each_component() -> Result { +fn validation_to_each_component() -> TestResult { let (mut cache, tmp) = new_cache(); let err = cache @@ -82,7 +82,7 @@ fn validation_to_each_component() -> Result { } #[test] -fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> Result { +fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> TestResult { let mut inline_error_diagnostics = Vec::new(); let (mut cache, tmp) = new_cache(); let forbidden = tmp.path().join("forbidden"); @@ -140,7 +140,7 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> Result { #[test] #[cfg(windows)] -fn terminal_symlinks_are_forbidden_without_force() -> Result { +fn terminal_symlinks_are_forbidden_without_force() -> TestResult { let (mut cache, tmp) = new_cache(); cache.enable_terminal_symlink_check(); @@ -175,7 +175,7 @@ fn terminal_symlinks_are_forbidden_without_force() -> Result { } #[test] -fn symlink_cached_as_file_is_revalidated_before_use_as_directory() -> Result { +fn symlink_cached_as_file_is_revalidated_before_use_as_directory() -> TestResult { let (mut cache, tmp) = new_cache(); let forbidden = tmp.path().join("forbidden"); std::fs::create_dir(&forbidden)?; @@ -199,7 +199,7 @@ fn symlink_cached_as_file_is_revalidated_before_use_as_directory() -> Result { } #[test] -fn symlink_cached_as_file_is_unlinked_before_use_as_directory_when_forced() -> Result { +fn symlink_cached_as_file_is_unlinked_before_use_as_directory_when_forced() -> TestResult { let (mut cache, tmp) = new_cache(); let forbidden = tmp.path().join("forbidden"); std::fs::create_dir(&forbidden)?; @@ -226,7 +226,7 @@ fn symlink_cached_as_file_is_unlinked_before_use_as_directory_when_forced() -> R } #[test] -fn cached_directory_returned_as_terminal_is_revalidated_before_descending() -> Result { +fn cached_directory_returned_as_terminal_is_revalidated_before_descending() -> TestResult { let mut inline_error_diagnostics = Vec::new(); for relative in ["link", "parent/link", "parent/deeper/link"] { for force in [false, true] { @@ -300,7 +300,7 @@ fn cached_directory_returned_as_terminal_is_revalidated_before_descending() -> R } #[test] -fn cached_terminal_is_revalidated_when_mode_changes() -> Result { +fn cached_terminal_is_revalidated_when_mode_changes() -> TestResult { let mut error_snapshots = Vec::new(); let (mut cache, _tmp) = new_cache(); for relative in [".gitmodules", "parent/.gitmodules"] { diff --git a/gix-worktree/tests/worktree/stack/ignore.rs b/gix-worktree/tests/worktree/stack/ignore.rs index c85b8aa2311..8c283341a3c 100644 --- a/gix-worktree/tests/worktree/stack/ignore.rs +++ b/gix-worktree/tests/worktree/stack/ignore.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::fs::Metadata; use bstr::{BStr, ByteSlice}; @@ -10,7 +10,7 @@ use crate::{hex_to_id, stack::probe_case}; #[test] #[cfg(any(unix, windows))] -fn symlinked_ignore_files_are_skipped_with_index_fallback() -> Result { +fn symlinked_ignore_files_are_skipped_with_index_fallback() -> TestResult { for use_index in [false, true] { let dir = gix_testtools::scripted_fixture_read_only_with_args( "make_symlinked_ignore_repo.sh", @@ -101,7 +101,7 @@ impl<'a> Iterator for IgnoreExpectations<'a> { } #[test] -fn exclude_by_dir_is_handled_just_like_git() { +fn exclude_by_dir_is_handled_just_like_git() -> TestResult { let dir = crate::scripted_fixture_read_only("make_special_exclude_case.sh").unwrap(); let git_dir = dir.join(".git"); @@ -134,7 +134,7 @@ fn exclude_by_dir_is_handled_just_like_git() { } for (relative_entry, source_and_line) in expectations { let (source, line, expected_pattern) = source_and_line.expect("every value is matched"); - let relative_path = gix_path::from_byte_slice(relative_entry); + let relative_path = gix_path::from_byte_slice(relative_entry)?; let is_dir = dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &FindError).unwrap(); @@ -158,6 +158,7 @@ fn exclude_by_dir_is_handled_just_like_git() { assert_eq!(line, 2); assert_eq!(source, ".gitignore"); } + Ok(()) } fn metadata_to_mode(meta: Metadata) -> Mode { @@ -169,7 +170,7 @@ fn metadata_to_mode(meta: Metadata) -> Mode { } #[test] -fn check_against_baseline() -> Result { +fn check_against_baseline() -> TestResult { let dir = crate::scripted_fixture_read_only("make_ignore_and_attributes_setup.sh")?; let worktree_dir = dir.join("repo"); let git_dir = worktree_dir.join(".git"); @@ -214,7 +215,7 @@ fn check_against_baseline() -> Result { lines: baseline.lines(), }; for (relative_entry, source_and_line) in expectations { - let relative_path = gix_path::from_byte_slice(relative_entry); + let relative_path = gix_path::from_byte_slice(relative_entry)?; let is_dir = worktree_dir.join(relative_path).metadata().ok().map(metadata_to_mode); let platform = cache.at_entry(relative_entry, is_dir, &odb)?; diff --git a/gix-zlib/tests/zlib/decompress.rs b/gix-zlib/tests/zlib/decompress.rs index e2f99fd39ea..c341d59b275 100644 --- a/gix-zlib/tests/zlib/decompress.rs +++ b/gix-zlib/tests/zlib/decompress.rs @@ -3,7 +3,7 @@ use gix_zlib::{Decompress, FlushDecompress, Status}; use crate::stream::deflate::compressed; #[test] -fn lifecycle_counters_and_flush_modes() { +fn lifecycle_counters_and_flush_modes() -> gix_error::TestResult { let expected = b"hello decompressor"; let input = compressed(expected); let mut state = Decompress::default(); @@ -12,9 +12,7 @@ fn lifecycle_counters_and_flush_modes() { assert_eq!(state.error_message(), None); let mut output = [0; 64]; - let status = state - .decompress(&input, &mut output, FlushDecompress::Finish) - .expect("valid input can be decompressed"); + let status = state.decompress(&input, &mut output, FlushDecompress::Finish)?; assert_eq!(status, Status::StreamEnd); assert_eq!(state.total_in(), input.len() as u64); assert_eq!(state.total_out(), expected.len() as u64); @@ -24,9 +22,8 @@ fn lifecycle_counters_and_flush_modes() { assert_eq!(state.total_in(), 0); assert_eq!(state.total_out(), 0); assert_eq!( - state - .decompress(&input, &mut output, FlushDecompress::Sync) - .expect("sync flush accepts a complete stream"), + state.decompress(&input, &mut output, FlushDecompress::Sync)?, Status::StreamEnd ); + Ok(()) } diff --git a/gix/src/commit/mod.rs b/gix/src/commit/mod.rs index c333ff56cd3..13fa0563bb5 100644 --- a/gix/src/commit/mod.rs +++ b/gix/src/commit/mod.rs @@ -29,16 +29,18 @@ fn signature_program( Format::X509 => (config.trusted_path(gpg::X509::PROGRAM)?, &gpg::X509::PROGRAM), Format::Ssh => (config.trusted_path(gpg::Ssh::PROGRAM)?, &gpg::Ssh::PROGRAM), }; - Ok(program - .unwrap_or_else(|| { - let default = gix_path::from_bstr(default.default_value_or_panic()).into_owned(); + let program = match program { + Some(program) => program, + None => { + let default = gix_path::from_bstr(default.default_value_or_panic())?.into_owned(); #[cfg(windows)] if let Some(program) = default.to_str().and_then(gix_path::env::installation_program) { - return program; + return Ok(program.into_os_string()); } default - }) - .into_os_string()) + } + }; + Ok(program.into_os_string()) } /// An empty array of a type usable with the `gix::easy` API to help declaring no parents should be used diff --git a/gix/src/commit/sign.rs b/gix/src/commit/sign.rs index 5d8d54b0e90..bdd24f2326b 100644 --- a/gix/src/commit/sign.rs +++ b/gix/src/commit/sign.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsString, path::PathBuf, process::Stdio}; +use std::{ffi::OsString, process::Stdio}; use crate::error::{ResultExt, bail, message}; use crate::{ @@ -33,8 +33,9 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result<Options> { Some(key) if !key.is_empty() && format == Format::Ssh && is_literal_ssh_key(&key).is_none() => config .trusted_path(User::SIGNING_KEY) .or_raise(|| message("Could not interpolate the configured commit-signing key path"))? - .map_or_else(|| gix_path::from_bstring(key).into_os_string(), PathBuf::into_os_string), - Some(key) if !key.is_empty() => gix_path::from_bstring(key).into_os_string(), + .map_or_else(|| gix_path::from_bstring(key), Ok)? + .into_os_string(), + Some(key) if !key.is_empty() => gix_path::from_bstring(key)?.into_os_string(), _ if format == Format::Ssh => default_ssh_key(&config)?.ok_or_else(|| { Error::from_error(message( "user.signingKey or gpg.ssh.defaultKeyCommand must provide an SSH signing key", @@ -53,7 +54,7 @@ pub(crate) fn signing_options(repo: &crate::Repository) -> Result<Options> { identity.extend_from_slice(b" <"); identity.extend_from_slice(committer.email); identity.extend_from_slice(b">"); - gix_path::from_bstring(identity).into_os_string() + gix_path::from_bstring(identity)?.into_os_string() } }; Ok(Options { @@ -71,7 +72,7 @@ pub(crate) fn signing_options_if_enabled(repo: &crate::Repository) -> Result<Opt } fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result<Option<OsString>> { - let Some(program) = config.trusted_program(gpg::Ssh::DEFAULT_KEY_COMMAND) else { + let Some(program) = config.trusted_program(gpg::Ssh::DEFAULT_KEY_COMMAND)? else { return Ok(None); }; let mut command: std::process::Command = gix_command::prepare(&program) @@ -79,7 +80,7 @@ fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result<Option<OsStri .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .into(); + .try_into()?; let program_display = if program == command.get_program() { String::new() } else { @@ -101,5 +102,5 @@ fn default_ssh_key(config: &crate::config::Snapshot<'_>) -> Result<Option<OsStri if is_literal_ssh_key(key).is_none() { bail!("gpg.ssh.defaultKeyCommand returned an invalid key: {key:?}"); } - Ok(Some(gix_path::from_bstr(key.as_bstr()).into_owned().into_os_string())) + Ok(Some(gix_path::from_bstr(key.as_bstr())?.into_owned().into_os_string())) } diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 67e1d3fd968..b0b74da02c8 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -455,6 +455,7 @@ impl Cache { #[cfg(feature = "attributes")] pub(crate) fn pathspec_defaults(&self) -> Result<gix_pathspec::Defaults> { use crate::config::tree::gitoxide; + let mut conversion_error = None; let res = gix_pathspec::Defaults::from_environment(&mut |name| { let key = [ &gitoxide::Pathspec::ICASE, @@ -466,9 +467,16 @@ impl Cache { .find(|key| key.environment_override().expect("set") == name) .expect("we must know all possible input variable names"); - let val = self.resolved.string(key).map(gix_path::from_bstr)?; - Some(val.into_owned().into()) + let value = self.resolved.string(key)?; + match gix_path::from_bstr(value) { + Ok(value) => Some(value.into_owned().into()), + Err(err) => { + conversion_error.get_or_insert(err); + None + } + } }); + let res = conversion_error.map_or(res, Err); if res.is_err() && self.lenient_config { Ok(gix_pathspec::Defaults::default()) } else { diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 61789930783..2884572286d 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -66,14 +66,17 @@ impl Snapshot<'_> { } /// Return the trusted string at `key` for launching using [command::prepare()](gix_command::prepare()), - /// or `None` if there is no such value or if no value was found in a trusted file. - pub fn trusted_program(&self, key: impl gix_config::AsKey) -> Option<OsString> { + /// or `Ok(None)` if there is no such value or if no value was found in a trusted file. + /// Return an error if the selected value cannot be represented as a native program name. + pub fn trusted_program(&self, key: impl gix_config::AsKey) -> Result<Option<OsString>> { let value = self .repo .config .resolved - .string_filter(key, &mut self.repo.config.filter_config_section.clone())?; - Some(gix_path::from_bstr(value).into_owned().into_os_string()) + .string_filter(key, &mut self.repo.config.filter_config_section.clone()); + value + .map(|value| Ok(gix_path::from_bstr(value)?.into_owned().into_os_string())) + .transpose() } } diff --git a/gix/src/config/snapshot/credential_helpers.rs b/gix/src/config/snapshot/credential_helpers.rs index 9704932215a..e429480be5d 100644 --- a/gix/src/config/snapshot/credential_helpers.rs +++ b/gix/src/config/snapshot/credential_helpers.rs @@ -125,7 +125,7 @@ pub(super) mod function { if value.trim().is_empty() { programs.clear(); } else { - programs.push(gix_credentials::Program::from_custom_definition(value)); + programs.push(gix_credentials::Program::from_custom_definition(value)?); } } if let Some(Some(user)) = (!url_had_user_initially).then(|| { diff --git a/gix/src/filter.rs b/gix/src/filter.rs index 01a27967fdd..9d468138745 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -104,7 +104,7 @@ impl Pipeline<'_> { }, &mut |buf| -> Result<_> { let entry = match index - .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).as_ref()) + .entry_by_path(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)?).as_ref()) { None => return Ok(None), Some(entry) => entry, @@ -154,7 +154,7 @@ impl Pipeline<'_> { rela_path: &BStr, index: &gix_index::State, ) -> Result<Option<(gix_hash::ObjectId, gix_object::tree::EntryKind, std::fs::Metadata)>> { - let rela_path_as_path = gix_path::from_bstr(rela_path); + let rela_path_as_path = gix_path::from_bstr(rela_path)?; let repo = self.repo; let worktree_dir = repo .workdir() @@ -176,7 +176,7 @@ impl Pipeline<'_> { let (id, kind) = if md.is_symlink() { let target = std::fs::read_link(&path) .or_raise(|| message!("Failed to perform IO for object creation for \"{}\"", path.display()))?; - let id = repo.write_blob(gix_path::into_bstr(target).as_ref())?; + let id = repo.write_blob(gix_path::into_bstr(target)?.as_ref())?; (id, gix_object::tree::EntryKind::Link) } else if md.is_file() { use gix_filter::pipeline::convert::ToGitOutcome; diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index e3d1b31cbb1..93f32e0186f 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -309,7 +309,7 @@ impl ThreadSafeRepository { )); } // Git treats core.worktree as a literal path, without tilde or prefix interpolation. - let worktree = gix_path::from_bstr(worktree.as_bstr()).into_owned(); + let worktree = gix_path::from_bstr(worktree.as_bstr())?.into_owned(); let worktree = match source { gix_config::Source::Env | gix_config::Source::Cli @@ -318,7 +318,7 @@ impl ThreadSafeRepository { _ => worktree_dir_from_repository_config(&git_dir, worktree, current_dir), }; worktree_dir = if worktree_from_environment { - Some(gix_path::normalize_saturating(worktree.into(), current_dir).into_owned()) + Some(gix_path::normalize_saturating(worktree.into(), current_dir)?.into_owned()) } else { gix_path::normalize(worktree.into(), current_dir).map(Cow::into_owned) }; @@ -441,7 +441,7 @@ impl ThreadSafeRepository { { Some(value) => { gitoxide::Core::INDEX_FILE.validate(value.as_bstr())?; - gix_path::from_bstr(value).into_owned() + gix_path::from_bstr(value)?.into_owned() } None => git_dir.join("index"), }; @@ -649,7 +649,7 @@ fn check_safe_directories( let safe_dir = match gix_config::Path::from(safe_dir).interpolate(interpolate_context(git_install_dir, home)) { Ok(path) => path, - Err(_) => gix_path::from_bstr(safe_dir).into_owned(), + Err(_) => gix_path::from_bstr(safe_dir)?.into_owned(), }; if !safe_dir.is_absolute() { gix_trace::warn!( diff --git a/gix/src/reference/log.rs b/gix/src/reference/log.rs index 1cb1c7153e0..fa5cf4f4e3d 100644 --- a/gix/src/reference/log.rs +++ b/gix/src/reference/log.rs @@ -15,7 +15,7 @@ impl Reference<'_> { } /// Return true if a reflog is present for this reference. - pub fn log_exists(&self) -> bool { + pub fn log_exists(&self) -> crate::Result<bool> { self.inner.log_exists(&self.repo.refs) } } diff --git a/gix/src/remote/connect.rs b/gix/src/remote/connect.rs index 6e42cc4b6ed..ec0a90157b7 100644 --- a/gix/src/remote/connect.rs +++ b/gix/src/remote/connect.rs @@ -74,7 +74,7 @@ impl<'repo> Remote<'repo> { ) -> Result<(gix_url::Url, gix_protocol::transport::Protocol)> { fn sanitize(mut url: gix_url::Url) -> Result<gix_url::Url> { if url.scheme == gix_url::Scheme::File { - let mut dir = gix_path::to_native_path_on_windows(Cow::Borrowed(url.path.as_ref())); + let mut dir = gix_path::to_native_path_on_windows(Cow::Borrowed(url.path.as_ref()))?; let kind = gix_discover::is_git(dir.as_ref()) .or_else(|_| { dir.to_mut().push(gix_discover::DOT_GIT_DIR); @@ -86,20 +86,19 @@ impl<'repo> Remote<'repo> { url.to_bstring() ) })?; - let (git_dir, _work_dir) = gix_discover::repository::Path::from_dot_git_dir( + let Some(repository_path) = gix_discover::repository::Path::from_dot_git_dir( dir.clone().into_owned(), kind, // precomposed unicode doesn't matter here as long as the produced path is accessible, // which is a given either way. &gix_fs::current_dir(false).or_raise(|| message("Could not obtain the current directory"))?, - ) - .ok_or_else(|| { - message("Could not access remote repository") - .with_input(gix_path::into_bstr(dir.clone().into_owned()).into_owned()) - .validation_error() - })? - .into_repository_and_work_tree_directories(); - url.path = gix_path::into_bstr(git_dir).into_owned(); + ) else { + return Err(message("Could not access remote repository") + .with_input(gix_path::into_bstr(dir.into_owned())?.into_owned()) + .validation_error()); + }; + let (git_dir, _work_dir) = repository_path.into_repository_and_work_tree_directories(); + url.path = gix_path::into_bstr(git_dir)?.into_owned(); } Ok(url) } diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index bc6ec9d9fea..9d446fbcdfb 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -124,7 +124,7 @@ where } let fetch_options = gix_protocol::fetch::Options { - shallow_file: repo.shallow_file(), + shallow_file: repo.shallow_file()?, shallow: &self.shallow, tags: con.remote.fetch_tags, reject_shallow_remote: Clone::REJECT_SHALLOW diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index bd27a918113..5f658eace6d 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -8,7 +8,7 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId { } mod update { - use gix_testtools::Result; + use gix_testtools::TestResult; use super::hex_to_id; use crate as gix; @@ -218,7 +218,7 @@ mod update { } #[test] - fn checked_out_branches_in_worktrees_are_rejected_with_additional_information() -> Result { + fn checked_out_branches_in_worktrees_are_rejected_with_additional_information() -> TestResult { let root = gix_path::realpath(gix_testtools::scripted_fixture_read_only_with_args_single_archive( "make_fetch_repos.sh", [base_repo_path()], @@ -263,7 +263,7 @@ mod update { } #[test] - fn incomplete_linked_worktrees_without_a_head_are_ignored() -> Result { + fn incomplete_linked_worktrees_without_a_head_are_ignored() -> TestResult { let (repo, _tmp) = repo_rw("two-origins"); let git_dir = repo.common_dir().join("worktrees/incomplete"); std::fs::create_dir_all(&git_dir)?; @@ -312,7 +312,7 @@ mod update { } #[test] - fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> Result { + fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> TestResult { let repo = named_repo("unborn"); let (mappings, specs) = mapping_from_spec("HEAD:refs/remotes/origin/HEAD", &repo); assert_eq!(mappings.len(), 1); @@ -339,7 +339,7 @@ mod update { } #[test] - fn unborn_remote_branches_can_update_local_unborn_branches() -> Result { + fn unborn_remote_branches_can_update_local_unborn_branches() -> TestResult { let repo = named_repo("unborn"); let peel_err = repo .find_reference("refs/heads/existing-unborn-symbolic")? @@ -374,9 +374,9 @@ mod update { assert_eq!( out.edits[0], RefEdit::update( - "refs/heads/existing-unborn-symbolic".try_into().expect("valid"), - Target::Symbolic("refs/heads/main".try_into().expect("valid")), - PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into().expect("valid"),)), + "refs/heads/existing-unborn-symbolic".try_into()?, + Target::Symbolic("refs/heads/main".try_into()?), + PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/main".try_into()?,)), "action: change unborn ref", ) ); @@ -409,9 +409,9 @@ mod update { assert_eq!( out.edits[0], RefEdit::update( - "refs/heads/existing-unborn-symbolic-other".try_into().expect("valid"), - Target::Symbolic("refs/heads/main".try_into().expect("valid")), - PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/other".try_into().expect("valid"),)), + "refs/heads/existing-unborn-symbolic-other".try_into()?, + Target::Symbolic("refs/heads/main".try_into()?), + PreviousValue::MustExistAndMatch(Target::Symbolic("refs/heads/other".try_into()?,)), "action: change unborn ref", ) ); @@ -419,7 +419,7 @@ mod update { } #[test] - fn remote_symbolic_refs_with_locally_unavailable_target_result_in_valid_peeled_branches() -> Result { + fn remote_symbolic_refs_with_locally_unavailable_target_result_in_valid_peeled_branches() -> TestResult { let remote_repo = named_repo("one-commit-with-symref"); let local_repo = named_repo("unborn"); let (mappings, specs) = mapping_from_spec("refs/heads/symbolic:refs/heads/new", &remote_repo); @@ -448,7 +448,7 @@ mod update { assert_eq!( out.edits[0], RefEdit::update( - "refs/heads/new".try_into().expect("valid"), + "refs/heads/new".try_into()?, target.clone(), PreviousValue::ExistingMustMatch(target), "action: storing head", @@ -460,7 +460,7 @@ mod update { } #[test] - fn remote_symbolic_refs_with_locally_unavailable_target_dont_overwrite_valid_local_branches() -> Result { + fn remote_symbolic_refs_with_locally_unavailable_target_dont_overwrite_valid_local_branches() -> TestResult { let remote_repo = named_repo("one-commit-with-symref"); let local_repo = named_repo("one-commit-with-symref-missing-branch"); let (mappings, specs) = mapping_from_spec("refs/heads/unborn:refs/heads/valid-locally", &remote_repo); @@ -489,7 +489,7 @@ mod update { } #[test] - fn unborn_remote_refs_dont_overwrite_valid_local_refs() -> Result { + fn unborn_remote_refs_dont_overwrite_valid_local_refs() -> TestResult { let remote_repo = named_repo("unborn"); let local_repo = named_repo("one-commit-with-symref"); let (mappings, specs) = @@ -520,7 +520,7 @@ mod update { } #[test] - fn symbolic_tags_with_malformed_referents_are_not_unborn() -> Result { + fn symbolic_tags_with_malformed_referents_are_not_unborn() -> TestResult { let mut diagnostics = Vec::new(); let (repo, _tmp) = repo_rw("two-origins"); std::fs::write(repo.git_dir().join("refs/tags/broken"), b"ref: refs/tags/malformed\n")?; @@ -565,7 +565,7 @@ mod update { } #[test] - fn symbolic_tags_with_missing_objects_are_not_unborn() -> Result { + fn symbolic_tags_with_missing_objects_are_not_unborn() -> TestResult { let mut diagnostics = Vec::new(); let (repo, _tmp) = repo_rw("two-origins"); let worktree = repo.workdir().expect("fixture has a worktree"); @@ -1017,7 +1017,7 @@ mod update { } #[test] - fn malformed_commits_cannot_force_reference_updates() -> Result { + fn malformed_commits_cannot_force_reference_updates() -> TestResult { let mut diagnostics = Vec::new(); use gix_object::Write; @@ -1063,7 +1063,7 @@ mod update { } #[test] - fn non_commit_targets_can_still_be_updated() -> Result { + fn non_commit_targets_can_still_be_updated() -> TestResult { let (repo, _tmp) = repo_rw("two-origins"); let blob_id = repo.write_blob(b"valid blob")?; let commit_id = repo.head_id()?; diff --git a/gix/src/remote/url/scheme_permission.rs b/gix/src/remote/url/scheme_permission.rs index 011781163d2..bc75efc006a 100644 --- a/gix/src/remote/url/scheme_permission.rs +++ b/gix/src/remote/url/scheme_permission.rs @@ -192,7 +192,7 @@ mod tests { } #[test] - fn allow_protocol_overrides_all_other_policy() { + fn allow_protocol_overrides_all_other_policy() -> gix_testtools::TestResult { let config = gix_config::File::from_bytes_no_includes( br#"[gitoxide "allow"] protocol = foo @@ -203,9 +203,8 @@ allow = always "#, gix_config::file::Metadata::default(), Default::default(), - ) - .expect("valid test configuration"); - let permissions = SchemePermission::from_config(&config, |_| true).expect("valid permissions"); + )?; + let permissions = SchemePermission::from_config(&config, |_| true)?; assert!( permissions.allow(&gix_url::Scheme::Helper("foo".into())), @@ -223,5 +222,6 @@ allow = always !permissions.allow(&gix_url::Scheme::Https), "the allowlist overrides known-safe defaults" ); + Ok(()) } } diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index 0bf764ce80f..f8063432421 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -90,21 +90,22 @@ impl crate::Repository { /// `GIT_EDITOR` takes precedence over `core.editor`. If the terminal isn't dumb, `VISUAL` is considered next, /// followed by `EDITOR`. If none are set, a bundled `vi` (or its `vim` implementation) is returned when available /// unless `TERM` is unset or `dumb`, in which case there is no usable editor. + /// Return an error if the selected editor cannot be represented as a native program name. /// /// Use [`editor_command()`](Self::editor_command) to obtain a command prepared for execution. - pub fn editor(&self) -> Option<OsString> { + pub fn editor(&self) -> Result<Option<OsString>> { use crate::config::tree::{Core, Gitoxide}; let config = self.config_snapshot(); let terminal_is_dumb = config.string(Gitoxide::TERM).is_none_or(|terminal| terminal == "dumb"); - config - .trusted_program(Core::EDITOR) - .or_else(|| { - (!terminal_is_dumb) - .then(|| config.trusted_program(Gitoxide::VISUAL)) - .flatten() - }) - .or_else(|| config.trusted_program(Gitoxide::EDITOR)) + let mut editor = config.trusted_program(Core::EDITOR)?; + if editor.is_none() && !terminal_is_dumb { + editor = config.trusted_program(Gitoxide::VISUAL)?; + } + if editor.is_none() { + editor = config.trusted_program(Gitoxide::EDITOR)?; + } + Ok(editor .or_else(|| { (!terminal_is_dumb).then(|| { gix_path::env::installation_program("vi") @@ -119,7 +120,7 @@ impl crate::Repository { .into_os_string() }) }) - .filter(|editor| !editor.is_empty()) + .filter(|editor| !editor.is_empty())) } /// Return the prepared [`editor`](Self::editor) command. @@ -130,7 +131,7 @@ impl crate::Repository { pub fn editor_command(&self) -> Result<Option<gix_command::Prepare>> { use std::{path::Path, process::Stdio}; - let Some(editor) = self.editor() else { + let Some(editor) = self.editor()? else { return Ok(None); }; @@ -237,7 +238,8 @@ impl crate::Repository { fallback_active = true; config.string_filter(gitoxide::Ssh::COMMAND_WITHOUT_SHELL_FALLBACK, &mut trusted) }) - .map(|cmd| gix_path::from_bstr(cmd).into_owned().into()); + .map(|cmd| gix_path::from_bstr(cmd).map(|cmd| cmd.into_owned().into())) + .transpose()?; let opts = gix_protocol::transport::client::blocking_io::ssh::connect::Options { disallow_shell: fallback_active, command: ssh_command, diff --git a/gix/src/repository/dirwalk.rs b/gix/src/repository/dirwalk.rs index a27d7ffa38e..b5e869b6bac 100644 --- a/gix/src/repository/dirwalk.rs +++ b/gix/src/repository/dirwalk.rs @@ -78,9 +78,9 @@ impl Repository { .filter_map(|proxy| proxy.base().ok()) .filter_map(|base| base.strip_prefix(&real_workdir).map(ToOwned::to_owned).ok()) .map(|rela_path| { - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)).into_owned() + Ok(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(rela_path)?).into_owned()) }) - .collect(); + .collect::<Result<_>>()?; opts.worktree_relative_worktree_dirs = Some(&worktree_relative_worktree_dirs_storage); } } diff --git a/gix/src/repository/location.rs b/gix/src/repository/location.rs index ae450cc5f9a..5770cbbd3a3 100644 --- a/gix/src/repository/location.rs +++ b/gix/src/repository/location.rs @@ -107,11 +107,8 @@ impl crate::Repository { /// first for paths supplied relative to the current working directory, absolute paths, or paths with `.` or `..` /// components. pub fn workdir_path(&self, rela_path: impl AsRef<BStr>) -> Option<PathBuf> { - self.workdir().and_then(|wd| { - gix_path::try_from_bstr(rela_path.as_ref()) - .ok() - .map(|rela| wd.join(rela)) - }) + self.workdir() + .and_then(|wd| gix_path::from_bstr(rela_path.as_ref()).ok().map(|rela| wd.join(rela))) } /// Normalize `path` into a repository-relative Git path with slash separators. @@ -128,7 +125,7 @@ impl crate::Repository { /// Paths which traverse outside of the repository are rejected. Note that passing absolute paths is expensive /// as their realpath has to be determined. pub fn normalize_path<'a>(&self, path: &'a (impl gix_utils::AsBStr + ?Sized)) -> Result<Cow<'a, BStr>> { - let path = gix_path::from_bstr(Cow::Borrowed(path.as_bstr())); + let path = gix_path::from_bstr(Cow::Borrowed(path.as_bstr()))?; let path = if gix_path::is_absolute(path.as_ref()) { let root = gix_path::realpath_opts( self.workdir().unwrap_or_else(|| self.git_dir()), @@ -168,7 +165,7 @@ impl crate::Repository { .expect("path was just validated as normalizable") } }; - Ok(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path))) + Ok(gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path)?)) } // TODO: tests, respect precomposeUnicode diff --git a/gix/src/repository/remote.rs b/gix/src/repository/remote.rs index e62829e527d..4afe1a00fa6 100644 --- a/gix/src/repository/remote.rs +++ b/gix/src/repository/remote.rs @@ -306,7 +306,10 @@ impl crate::Repository { let name_is_url = matches!( remote::Name::try_from(std::borrow::Cow::Borrowed(name_or_url)), Ok(remote::Name::Url(_)) - ) || gix_path::is_absolute(gix_path::from_bstr(name_or_url)); + ) || match gix_path::from_bstr(name_or_url) { + Ok(path) => gix_path::is_absolute(path), + Err(err) => return Some(Err(err)), + }; match config::tree::Remote::URL.try_into_url(std::borrow::Cow::Borrowed(name_or_url)) { Ok(url) if name_is_url || url.scheme != gix_url::Scheme::File => urls.push(url), Ok(_) => {} diff --git a/gix/src/repository/shallow.rs b/gix/src/repository/shallow.rs index 57be5d732d2..78fd0254589 100644 --- a/gix/src/repository/shallow.rs +++ b/gix/src/repository/shallow.rs @@ -7,8 +7,12 @@ use crate::{ impl Repository { /// Return `true` if the repository is a shallow clone, i.e. contains history only up to a certain depth. - pub fn is_shallow(&self) -> bool { - self.shallow_file().metadata().is_ok_and(|m| m.is_file() && m.len() > 0) + /// Return an error if the configured shallow-file path cannot be represented natively. + pub fn is_shallow(&self) -> Result<bool> { + Ok(self + .shallow_file()? + .metadata() + .is_ok_and(|m| m.is_file() && m.len() > 0)) } /// Return a shared list of shallow commits which is updated automatically if the in-memory snapshot has become stale @@ -20,9 +24,10 @@ impl Repository { /// /// The shared list is shared across all clones of this repository. pub fn shallow_commits(&self) -> Result<Option<crate::shallow::Commits>> { + let shallow_file = self.shallow_file()?; self.shallow_commits.recent_snapshot( - || self.shallow_file().metadata().ok().and_then(|m| m.modified().ok()), - || gix_shallow::read(&self.shallow_file()), + || shallow_file.metadata().ok().and_then(|m| m.modified().ok()), + || gix_shallow::read(&shallow_file), ) } @@ -30,12 +35,13 @@ impl Repository { /// parents within this repository. /// /// Note that it may not exist if the repository isn't actually shallow. - pub fn shallow_file(&self) -> PathBuf { + /// Return an error if the configured path cannot be represented natively. + pub fn shallow_file(&self) -> Result<PathBuf> { let shallow_name = self .config .resolved .string_filter(gitoxide::Core::SHALLOW_FILE, &mut self.filter_config_section()) .unwrap_or_else(|| gitoxide::Core::SHALLOW_FILE.default_value_or_panic().into()); - self.common_dir().join(gix_path::from_bstr(shallow_name)) + Ok(self.common_dir().join(gix_path::from_bstr(shallow_name)?)) } } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 9eb518851b5..ab3ad68a181 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -90,12 +90,14 @@ impl crate::Repository { /// Return the worktree that [is identified](Worktree::id) by the given `id`, if it exists at /// `.git/worktrees/<id>` and its `gitdir` file exists. - /// Return `None` otherwise. - pub fn worktree_proxy_by_id<'a>(&self, id: impl Into<&'a BStr>) -> Option<worktree::Proxy<'_>> { - worktree::Proxy::new_if_gitdir_file_exists( + /// Return `Ok(None)` otherwise, or an error if `id` cannot be represented as a native path. + pub fn worktree_proxy_by_id<'a>(&self, id: impl Into<&'a BStr>) -> Result<Option<worktree::Proxy<'_>>> { + Ok(worktree::Proxy::new_if_gitdir_file_exists( self, - self.common_dir().join("worktrees").join(gix_path::from_bstr(id.into())), - ) + self.common_dir() + .join("worktrees") + .join(gix_path::from_bstr(id.into())?), + )) } /// Return the *repository* owning the main worktree, typically from a linked worktree. diff --git a/gix/src/revision/spec/parse/delegate/navigate.rs b/gix/src/revision/spec/parse/delegate/navigate.rs index 787c5cda4d4..a321c5d8f39 100644 --- a/gix/src/revision/spec/parse/delegate/navigate.rs +++ b/gix/src/revision/spec/parse/delegate/navigate.rs @@ -126,7 +126,7 @@ impl delegate::Navigate for Delegate<'_> { } let mut tree = repo.find_object(tree_id).or_erased()?.into_tree(); let entry = tree - .peel_to_entry_by_path(gix_path::from_bstr(path)) + .peel_to_entry_by_path(gix_path::from_bstr(path).or_erased()?) .or_erased()? .ok_or_raise_erased(|| { message!( @@ -334,10 +334,10 @@ impl delegate::Navigate for Delegate<'_> { .iter() .filter(|our_stage| **our_stage != stage) .find_map(|stage| index.entry_index_by_path_and_stage(path, *stage).map(|_| *stage)); - let exists = self - .repo - .workdir() - .is_some_and(|root| root.join(gix_path::from_bstr(path)).exists()); + let exists = match self.repo.workdir() { + Some(root) => root.join(gix_path::from_bstr(path)?).exists(), + None => false, + }; Err(message!( "Path {path:?} did not exist in index at stage {desired_stage}{stage_hint}{exists}", exists = if exists { diff --git a/gix/src/revision/spec/parse/delegate/revision.rs b/gix/src/revision/spec/parse/delegate/revision.rs index 5178ab607d2..8dce95427d9 100644 --- a/gix/src/revision/spec/parse/delegate/revision.rs +++ b/gix/src/revision/spec/parse/delegate/revision.rs @@ -115,7 +115,7 @@ impl delegate::Revision for Delegate<'_> { }, }; - if !r.log_exists() { + if !r.log_exists()? { r.follow_to_object() .map_err(|err| error::with_missing_reference(err.into_exn()))?; } diff --git a/gix/src/revision/spec/parse/mod.rs b/gix/src/revision/spec/parse/mod.rs index b20f54ad166..f7d7ba8d241 100644 --- a/gix/src/revision/spec/parse/mod.rs +++ b/gix/src/revision/spec/parse/mod.rs @@ -27,7 +27,7 @@ pub enum Error { MissingReference { /// The missing reference name, which may have been discovered while following symbolic references. /// It is not necessarily the original revision specification. - name: std::path::PathBuf, + name: BString, }, /// More than one object matches the prefix and the specification did not disambiguate them. /// Intrinsically classified as [`gix_error::Class::Validation`]. @@ -52,9 +52,7 @@ pub enum Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let candidates = match self { - Self::MissingReference { name } => { - return write!(f, "Reference {name} could not be found", name = name.display()); - } + Self::MissingReference { name } => return write!(f, "Reference {name:?} could not be found"), Self::AmbiguousPrefix { prefix, candidates } => { write!(f, "Short id {prefix} is ambiguous. Candidates are:")?; candidates diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 2c61f33d36d..5ead9b33b0a 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -193,7 +193,7 @@ impl Submodule<'_> { .repo .head_commit()? .tree()? - .peel_to_entry_by_path(gix_path::from_bstring(path))? + .peel_to_entry_by_path(gix_path::from_bstring(path)?)? .and_then(|entry| (entry.mode().is_commit()).then_some(entry.inner.oid))) } @@ -201,7 +201,7 @@ impl Submodule<'_> { /// /// The retunred directory might not exist yet. pub fn git_dir(&self) -> Result<PathBuf> { - Ok(git_dir_from_name(self.state.repo.common_dir(), self.validated_name()?)) + git_dir_from_name(self.state.repo.common_dir(), self.validated_name()?) } /// Return the path to the location at which the workdir would be checked out. @@ -209,7 +209,7 @@ impl Submodule<'_> { /// Note that it may be a path relative to the repository if, for some reason, the parent directory /// doesn't have a working dir set. pub fn work_dir(&self) -> Result<PathBuf> { - let worktree_git = gix_path::from_bstr(self.path()?); + let worktree_git = gix_path::from_bstr(self.path()?)?; Ok(match self.state.repo.workdir() { None => worktree_git.into_owned(), Some(prefix) => prefix.join(worktree_git), @@ -347,11 +347,11 @@ impl Submodule<'_> { /// /// In particular, don't use `Path::join()` for `name`: absolute-looking names are valid in Git, /// but joining them as a path would discard the `.git/modules` prefix. -fn git_dir_from_name(common_dir: &Path, name: &BStr) -> PathBuf { +fn git_dir_from_name(common_dir: &Path, name: &BStr) -> Result<PathBuf> { let mut git_dir = common_dir.join("modules").into_os_string(); git_dir.push(std::path::MAIN_SEPARATOR_STR); - git_dir.push(gix_path::from_bstr(name).as_os_str()); - git_dir.into() + git_dir.push(gix_path::from_bstr(name)?.as_os_str()); + Ok(git_dir.into()) } #[cfg(test)] @@ -361,7 +361,7 @@ mod tests { use crate::bstr::ByteSlice; #[test] - fn git_dir_from_name_keeps_git_compatible_names_below_modules() { + fn git_dir_from_name_keeps_git_compatible_names_below_modules() -> gix_testtools::TestResult { let common_dir = Path::new("repo").join(".git"); let modules_dir = common_dir.join("modules"); @@ -374,7 +374,7 @@ mod tests { b"C:/Windows/Temp/x", b"C:x", ] { - let actual = super::git_dir_from_name(&common_dir, name.as_bstr()); + let actual = super::git_dir_from_name(&common_dir, name.as_bstr())?; assert!( actual.starts_with(&modules_dir), "Git-compatible name {name:?} must remain below {} instead of producing {}", @@ -382,6 +382,7 @@ mod tests { actual.display() ); } + Ok(()) } } diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index bcfe527b82c..cacc96e3469 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{ffi::OsStr, path::PathBuf}; #[cfg(feature = "worktree-archive")] pub use gix_archive as archive; @@ -17,7 +17,7 @@ pub mod add; pub mod remove; use crate::{ - Repository, + Repository, Result, bstr::{BStr, BString}, }; @@ -78,7 +78,7 @@ impl<'repo> crate::Worktree<'repo> { /// /// It cannot be removed. pub fn is_main(&self) -> bool { - self.id().is_none() + id(self.parent.git_dir(), self.parent.common_dir.is_some()).is_none() } /// Return true if this worktree cannot be pruned, moved or deleted, which is useful if it is located on an external storage device. @@ -96,9 +96,12 @@ impl<'repo> crate::Worktree<'repo> { Proxy::new(self.parent, self.parent.git_dir()).lock_reason() } - /// Return the ID of the repository worktree, if it is a linked worktree, or `None` if it's a linked worktree. - pub fn id(&self) -> Option<&BStr> { + /// Return the ID of the repository worktree, if it is a linked worktree, or `Ok(None)` if it's the main worktree. + /// Return an error if its directory name cannot be represented as Git bytes. + pub fn id(&self) -> Result<Option<&BStr>> { id(self.parent.git_dir(), self.parent.common_dir.is_some()) + .map(gix_path::os_str_into_bstr) + .transpose() } /// Returns true if the `.git` file or directory exists within the worktree. @@ -109,14 +112,12 @@ impl<'repo> crate::Worktree<'repo> { } } -pub(crate) fn id(git_dir: &std::path::Path, has_common_dir: bool) -> Option<&BStr> { +fn id(git_dir: &std::path::Path, has_common_dir: bool) -> Option<&OsStr> { if !has_common_dir { return None; } - let candidate = gix_path::os_str_into_bstr(git_dir.file_name().expect("at least one directory level")) - .expect("no illformed UTF-8"); - let maybe_worktrees = git_dir.parent()?; - (maybe_worktrees.file_name()?.to_str()? == "worktrees").then_some(candidate) + let candidate = git_dir.file_name().expect("at least one directory level"); + (git_dir.parent()?.file_name()? == "worktrees").then_some(candidate) } /// diff --git a/gix/src/worktree/proxy.rs b/gix/src/worktree/proxy.rs index 2093dd29267..f4914a1dff0 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -51,9 +51,9 @@ impl Proxy<'_> { } /// The name of the worktree, which is derived from its folder within the `worktrees` directory within the parent `.git` folder. - pub fn id(&self) -> &BStr { + /// Return an error if the directory name cannot be represented as Git bytes. + pub fn id(&self) -> Result<&BStr> { gix_path::os_str_into_bstr(self.git_dir.file_name().expect("worktrees/ parent dir")) - .expect("no illformed UTF-8") } /// Return true if the worktree cannot be pruned, moved or deleted, which is useful if it is located on an external storage device. diff --git a/gix/src/worktree/remove.rs b/gix/src/worktree/remove.rs index c5eb3212aa3..fb2a3ed535c 100644 --- a/gix/src/worktree/remove.rs +++ b/gix/src/worktree/remove.rs @@ -459,13 +459,20 @@ fn has_populated_submodule(repo: &crate::Repository) -> Result<bool> { let index = repo .index_or_empty() .or_raise(|| message("Could not open the linked worktree index"))?; - Ok(index.entries().iter().any(|entry| { - entry.mode == gix_index::entry::Mode::COMMIT - && repo.workdir().is_some_and(|work_dir| { - work_dir - .join(gix_path::from_bstr(entry.path(&index))) - .join(".git") - .exists() - }) - })) + if let Some(work_dir) = repo.workdir() { + for entry in index + .entries() + .iter() + .filter(|entry| entry.mode == gix_index::entry::Mode::COMMIT) + { + if work_dir + .join(gix_path::from_bstr(entry.path(&index))?) + .join(".git") + .exists() + { + return Ok(true); + } + } + } + Ok(false) } diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 04d2ed758b6..533758dc286 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -34,7 +34,7 @@ mod config { #[test] #[serial] - fn globals_from_open_options_match_repository_opening() -> gix_testtools::Result { + fn globals_from_open_options_match_repository_opening() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::TempDir::new()?; let _cwd = gix_testtools::set_current_dir(temp.path())?; @@ -125,12 +125,12 @@ mod config { mod config_mut { use gix::config::Source; use gix_sec::Permission; - use gix_testtools::{Env, Result}; + use gix_testtools::{Env, TestResult}; use serial_test::serial; #[test] #[serial] - fn path_lookup_does_not_open_configuration() -> Result { + fn path_lookup_does_not_open_configuration() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::tempdir()?; let _cwd = gix_testtools::set_current_dir(temp.path())?; @@ -175,7 +175,7 @@ mod config_mut { #[test] #[serial] - fn edits_one_physical_file_losslessly_without_a_repository() -> Result { + fn edits_one_physical_file_losslessly_without_a_repository() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::tempdir()?; let _cwd = gix_testtools::set_current_dir(temp.path())?; @@ -259,7 +259,7 @@ mod config_mut { #[test] #[serial] - fn source_paths_match_standalone_reads() -> Result { + fn source_paths_match_standalone_reads() -> TestResult { let mut diagnostics = Vec::new(); let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::tempdir()?; @@ -338,7 +338,7 @@ mod config_mut { #[test] #[serial] - fn honors_environment_and_explicit_path_overrides() -> Result { + fn honors_environment_and_explicit_path_overrides() -> TestResult { let mut diagnostics = Vec::new(); let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::tempdir()?; @@ -423,7 +423,7 @@ mod config_mut { #[test] #[serial] - fn rejects_unsupported_and_disabled_sources() -> Result { + fn rejects_unsupported_and_disabled_sources() -> TestResult { let mut diagnostics = Vec::new(); let temp = gix_testtools::tempfile::tempdir()?; let options = gix::open::Options::isolated() @@ -474,7 +474,7 @@ mod config_mut { #[test] #[serial] - fn missing_files_require_existing_parent_directories() -> Result { + fn missing_files_require_existing_parent_directories() -> TestResult { let temp = gix_testtools::tempfile::tempdir()?; let path = temp.path().join("missing/global.config"); let options = options_for(Source::System).system_config_path(&path); @@ -508,7 +508,7 @@ mod config_mut { #[test] #[serial] - fn malformed_configuration_cannot_be_overwritten() -> Result { + fn malformed_configuration_cannot_be_overwritten() -> TestResult { let temp = gix_testtools::tempfile::tempdir()?; let path = temp.path().join("global.config"); let options = options_for(Source::System).system_config_path(&path); @@ -539,7 +539,7 @@ mod config_mut { #[test] #[serial] - fn lock_timeout_uses_global_configuration_and_option_precedence() -> Result { + fn lock_timeout_uses_global_configuration_and_option_precedence() -> TestResult { let temp = gix_testtools::tempfile::tempdir()?; let global_config_path = temp.path().join("global.config"); std::fs::write( @@ -637,7 +637,7 @@ mod config_mut { #[test] #[serial] - fn relative_paths_remain_anchored_when_the_current_directory_changes() -> Result { + fn relative_paths_remain_anchored_when_the_current_directory_changes() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let temp = gix_testtools::tempfile::tempdir()?; let _cwd = gix_testtools::set_current_dir(temp.path())?; @@ -665,7 +665,7 @@ mod config_mut { #[test] #[cfg(unix)] #[serial] - fn global_permissions_honor_shared_repository_policy() -> Result { + fn global_permissions_honor_shared_repository_policy() -> TestResult { use std::os::unix::fs::PermissionsExt; let temp = gix_testtools::tempfile::tempdir()?; @@ -729,7 +729,7 @@ mod with_overrides { #[test] #[serial] - fn order_from_api_and_cli_and_environment() -> gix_testtools::Result { + fn order_from_api_and_cli_and_environment() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repo_path = gix_testtools::scripted_fixture_read_only("make_config_repos.sh")? .join("http-config") @@ -960,7 +960,7 @@ mod with_overrides { #[test] #[serial] -fn git_worktree_and_strict_config() -> gix_testtools::Result { +fn git_worktree_and_strict_config() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let worktree = gix_testtools::tempfile::tempdir()?; let _environment = _environment.set("GIT_WORK_TREE", worktree.path().to_string_lossy()); @@ -980,7 +980,7 @@ fn git_worktree_and_strict_config() -> gix_testtools::Result { #[test] #[serial] -fn git_worktree_overrides_core_worktree_and_bare() -> gix_testtools::Result { +fn git_worktree_overrides_core_worktree_and_bare() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; use std::io::Write; @@ -1040,7 +1040,7 @@ fn git_worktree_overrides_core_worktree_and_bare() -> gix_testtools::Result { #[test] #[serial] -fn git_worktree_overrides_discovered_worktree() -> gix_testtools::Result { +fn git_worktree_overrides_discovered_worktree() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repository = gix_testtools::tempfile::TempDir::new()?; gix::ThreadSafeRepository::init_opts( @@ -1067,7 +1067,7 @@ fn git_worktree_overrides_discovered_worktree() -> gix_testtools::Result { #[test] #[serial] #[cfg(unix)] -fn git_worktree_over_root_overrides_bare() -> gix_testtools::Result { +fn git_worktree_over_root_overrides_bare() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let fixture = gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?; let worktree = gix_testtools::tempfile::TempDir::new()?; @@ -1122,7 +1122,7 @@ fn git_worktree_over_root_overrides_bare() -> gix_testtools::Result { #[test] #[serial] #[cfg(unix)] -fn git_worktree_absolute_over_root_overrides_bare() -> gix_testtools::Result { +fn git_worktree_absolute_over_root_overrides_bare() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let fixture = gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?; let worktree = gix_testtools::tempfile::TempDir::new()?; @@ -1145,7 +1145,7 @@ fn git_worktree_absolute_over_root_overrides_bare() -> gix_testtools::Result { #[test] #[serial] -fn repository_transitions_do_not_inherit_repository_environment_overrides() -> gix_testtools::Result { +fn repository_transitions_do_not_inherit_repository_environment_overrides() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; fn assert_paths( repo: &Repository, @@ -1294,7 +1294,7 @@ fn repository_transitions_do_not_inherit_repository_environment_overrides() -> g #[test] #[serial] #[cfg(feature = "attributes")] -fn git_index_file_override_is_not_inherited_by_opened_submodules() -> gix_testtools::Result { +fn git_index_file_override_is_not_inherited_by_opened_submodules() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let fixture = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?; let superproject = std::fs::canonicalize(fixture.join("with-submodules"))?; @@ -1328,7 +1328,7 @@ fn git_index_file_override_is_not_inherited_by_opened_submodules() -> gix_testto #[test] #[serial] #[cfg(feature = "attributes")] -fn submodule_open_propagates_missing_environment_configuration() -> gix_testtools::Result { +fn submodule_open_propagates_missing_environment_configuration() -> gix_testtools::TestResult { let mut error_snapshots = Vec::new(); let fixture = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?; let _empty_config = gix_testtools::Env::new().set("GIT_CONFIG_COUNT", "0"); @@ -1375,7 +1375,7 @@ fn submodule_open_propagates_missing_environment_configuration() -> gix_testtool #[test] #[serial] -fn git_index_file_relative_paths_use_the_cwd_when_opening() -> gix_testtools::Result { +fn git_index_file_relative_paths_use_the_cwd_when_opening() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repository = gix_testtools::scripted_fixture_writable("make_basic_repo.sh")?; let _cwd = gix_testtools::set_current_dir(repository.path())?; diff --git a/gix/tests/gix/clone.rs b/gix/tests/gix/clone.rs index dd29c502c44..ab0c8353c10 100644 --- a/gix/tests/gix/clone.rs +++ b/gix/tests/gix/clone.rs @@ -1,9 +1,9 @@ -use crate::Result; use crate::{remote, util::restricted}; +use gix_testtools::TestResult; #[cfg(all(feature = "worktree-mutation", feature = "blocking-network-client"))] mod blocking_io { - use crate::Result; + use gix_testtools::{Result, TestResult}; use std::{borrow::Cow, path::Path, sync::atomic::AtomicBool}; use crate::{ @@ -28,7 +28,7 @@ mod blocking_io { #[test] #[serial_test::serial] - fn inherited_core_symlinks_false_is_respected() -> Result { + fn inherited_core_symlinks_false_is_respected() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; use gix_sec::Permission; @@ -126,7 +126,7 @@ mod blocking_io { } #[test] - fn fetch_shallow_no_checkout_then_unshallow() -> Result { + fn fetch_shallow_no_checkout_then_unshallow() -> TestResult { // Local Git transport children read ambient config independently of repository options. // Process isolation keeps the shallow fetch and unshallow I/O off the shared serial lock. if gix_testtools::run_in_isolated_process()? { @@ -152,7 +152,7 @@ mod blocking_io { Ok(r) } }) - .with_shallow(Shallow::DepthAtRemote(2.try_into().expect("non-zero"))); + .with_shallow(Shallow::DepthAtRemote(2.try_into()?)); let (repo, _out) = prepare.fetch_only(gix::progress::Discard, &AtomicBool::default())?; drop(prepare); @@ -178,11 +178,11 @@ mod blocking_io { assert!(repo.shallow_commits()?.is_none(), "the repo isn't shallow anymore"); assert!( - !repo.is_shallow(), + !repo.is_shallow()?, "both methods agree - if there are no shallow commits, it shouldn't think the repo is shallow" ); assert!( - !repo.shallow_file().exists(), + !repo.shallow_file()?.exists(), "when the repo is not shallow anymore, there is no need for a shallow file" ); assert!( @@ -194,7 +194,7 @@ mod blocking_io { } #[test] - fn shallow_clone_uses_single_branch_refspec() -> Result { + fn shallow_clone_uses_single_branch_refspec() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -203,7 +203,7 @@ mod blocking_io { .with_shallow(Shallow::DepthAtRemote(1.try_into()?)) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; - assert!(repo.is_shallow(), "repository should be shallow"); + assert!(repo.is_shallow()?, "repository should be shallow"); // Verify that only a single-branch refspec was configured let remote = repo.find_remote("origin")?; @@ -216,7 +216,7 @@ mod blocking_io { assert_eq!(refspecs.len(), 1, "shallow clone should have only one fetch refspec"); // The refspec should be for a single branch (main), not a wildcard - let refspec_str = refspecs[0].to_str().expect("valid utf8"); + let refspec_str = refspecs[0].to_str()?; assert_eq!( refspec_str, "+refs/heads/main:refs/remotes/origin/main", "shallow clone refspec should not use wildcard and should be the main branch: {refspec_str}" @@ -226,7 +226,7 @@ mod blocking_io { } #[test] - fn shallow_clone_with_ambiguous_branch_and_tag_name_prefers_branch() -> Result { + fn shallow_clone_with_ambiguous_branch_and_tag_name_prefers_branch() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -283,7 +283,7 @@ mod blocking_io { } #[test] - fn from_shallow_prohibited_with_option() -> Result { + fn from_shallow_prohibited_with_option() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -309,7 +309,7 @@ mod blocking_io { } #[test] - fn from_shallow_allowed_by_default() -> Result { + fn from_shallow_allowed_by_default() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -341,7 +341,7 @@ mod blocking_io { } #[test] - fn from_non_shallow_then_deepen_then_deepen_since_to_unshallow() -> Result { + fn from_non_shallow_then_deepen_then_deepen_since_to_unshallow() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -354,7 +354,7 @@ mod blocking_io { }) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; - assert!(repo.is_shallow()); + assert!(repo.is_shallow()?); assert_eq!( shallow_ids(&repo, "present")?, sorted([ @@ -396,7 +396,7 @@ mod blocking_io { .receive(gix::progress::Discard, &AtomicBool::default())?; assert!( - !repo.is_shallow(), + !repo.is_shallow()?, "the cutoff date is before the first commit, effectively unshallowing" ); assert!( @@ -407,7 +407,7 @@ mod blocking_io { } #[test] - fn from_non_shallow_by_deepen_exclude_then_deepen_to_unshallow() -> Result { + fn from_non_shallow_by_deepen_exclude_then_deepen_to_unshallow() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -430,7 +430,7 @@ mod blocking_io { }) .fetch_only(gix::progress::Discard, &AtomicBool::default())?; - assert!(repo.is_shallow()); + assert!(repo.is_shallow()?); assert_eq!( shallow_ids(&repo, "present")?, sorted([ @@ -446,12 +446,12 @@ mod blocking_io { .with_shallow(Shallow::Deepen(2)) .receive(gix::progress::Discard, &AtomicBool::default())?; - assert!(!repo.is_shallow(), "one is just enough to unshallow it"); + assert!(!repo.is_shallow()?, "one is just enough to unshallow it"); Ok(()) } #[test] - fn fetch_only_with_configuration() -> Result { + fn fetch_only_with_configuration() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -507,7 +507,7 @@ mod blocking_io { .expect("present") .path .as_ref() - )) + ))? .is_absolute(), "file urls can't be relative paths" ); @@ -586,14 +586,14 @@ mod blocking_io { match r.target() { TargetRef::Object(_) => { let mut logs = r.log_iter(); - assert_reflog(logs.all()); + assert_reflog(logs.all())?; } TargetRef::Symbolic(_) => { // TODO: it *should* be possible to set the reflog here based on the referent if deref = true // when setting up the edits. But it doesn't seem to work. Also, some tests are // missing for `leaf_referent_previous_oid`. assert!( - !r.log_exists(), + !r.log_exists()?, "symbolic refs don't have object ids, so they can't get \ into the reflog as these need previous and new oid" ); @@ -614,14 +614,7 @@ mod blocking_io { }) .map(|(idx, _)| &out.ref_map.mappings[idx]) { - out_of_graph_tags.push( - mapping - .remote - .as_name() - .expect("tag always has a path") - .to_str() - .expect("valid UTF8"), - ); + out_of_graph_tags.push(mapping.remote.as_name().expect("tag always has a path").to_str()?); } assert_eq!( out_of_graph_tags, @@ -637,9 +630,7 @@ mod blocking_io { } let remote_repo = remote::repo("base"); - let remote_head = repo - .find_reference(&format!("refs/remotes/{remote_name}/HEAD")) - .expect("remote HEAD present"); + let remote_head = repo.find_reference(&format!("refs/remotes/{remote_name}/HEAD"))?; let remote_head_id = remote_repo.head_id()?; assert_eq!( remote_head.target().try_id(), @@ -650,7 +641,7 @@ mod blocking_io { let head = repo.head()?; { let mut logs = head.log_iter(); - assert_reflog(logs.all()); + assert_reflog(logs.all())?; } let referent = head.try_into_referent().expect("symbolic ref is present"); @@ -681,12 +672,12 @@ mod blocking_io { { let mut logs = referent.log_iter(); - assert_reflog(logs.all()); + assert_reflog(logs.all())?; } Ok(()) } - fn assert_reflog(log: std::io::Result<Option<gix_ref::file::log::iter::Forward<'_>>>) { + fn assert_reflog(log: std::io::Result<Option<gix_ref::file::log::iter::Forward<'_>>>) -> Result { let lines = log .unwrap() .expect("log present") @@ -699,12 +690,13 @@ mod blocking_io { "{:?} unexpected", line.message ); - let path = gix_path::from_bstr(line.message.rsplit(|b| *b == b' ').next().expect("path").as_bstr()); + let path = gix_path::from_bstr(line.message.rsplit(|b| *b == b' ').next().expect("path").as_bstr())?; assert!(path.is_absolute(), "\"{}\" must be absolute", path.display()); + Ok(()) } #[test] - fn fetch_and_checkout() -> Result { + fn fetch_and_checkout() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -722,13 +714,13 @@ mod blocking_io { let index = repo.index()?; assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree"); - assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare")); + assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare"))?; Ok(()) } #[test] #[cfg(unix)] - fn fetch_and_checkout_does_not_follow_delayed_symlink_prefixes() -> Result { + fn fetch_and_checkout_does_not_follow_delayed_symlink_prefixes() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -766,7 +758,7 @@ mod blocking_io { } #[test] - fn fetch_and_checkout_into_non_empty_directory() -> Result { + fn fetch_and_checkout_into_non_empty_directory() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -790,14 +782,14 @@ mod blocking_io { let index = repo.index()?; assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree"); - assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare")); + assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare"))?; assert_eq!(std::fs::read(&existing_path)?, EXISTING_CONTENT); Ok(()) } #[test] - fn fetch_and_checkout_into_non_empty_directory_does_not_overwrite_pre_existing_tracked_file() -> Result { + fn fetch_and_checkout_into_non_empty_directory_does_not_overwrite_pre_existing_tracked_file() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -841,7 +833,7 @@ mod blocking_io { } #[test] - fn fetch_and_checkout_into_non_empty_directory_with_existing_dot_git_is_rejected() -> Result { + fn fetch_and_checkout_into_non_empty_directory_with_existing_dot_git_is_rejected() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_clone_destinations.sh")?; let destination = fixture.path().join("non-empty-with-dot-git"); let existing_path = destination.join("existing.txt"); @@ -874,7 +866,7 @@ mod blocking_io { } #[test] - fn drop_after_failed_fetch_into_non_empty_directory_preserves_destination() -> Result { + fn drop_after_failed_fetch_into_non_empty_directory_preserves_destination() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -912,7 +904,7 @@ mod blocking_io { } #[test] - fn fetch_and_checkout_specific_ref() -> Result { + fn fetch_and_checkout_specific_ref() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -955,12 +947,12 @@ mod blocking_io { let index = repo.index()?; assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree"); - assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare")); + assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare"))?; Ok(()) } #[test] - fn fetch_and_checkout_specific_revision() -> Result { + fn fetch_and_checkout_specific_revision() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1012,7 +1004,7 @@ mod blocking_io { } #[test] - fn fetch_specific_revision_bare_and_shallow() -> Result { + fn fetch_specific_revision_bare_and_shallow() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1048,7 +1040,7 @@ mod blocking_io { .with_shallow(Shallow::DepthAtRemote(1.try_into()?)); let (mut checkout, _) = shallow.fetch_then_checkout(gix::progress::Discard, &AtomicBool::default())?; let (repo, _) = checkout.main_worktree(gix::progress::Discard, &AtomicBool::default())?; - assert!(repo.is_shallow(), "depth applies to a single-revision clone"); + assert!(repo.is_shallow()?, "depth applies to a single-revision clone"); assert_eq!(repo.head_id()?, expected, "the requested revision is checked out"); assert_eq!( repo.references()?.all()?.count(), @@ -1059,7 +1051,7 @@ mod blocking_io { } #[test] - fn invalid_specific_revisions_are_rejected() -> Result { + fn invalid_specific_revisions_are_rejected() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1119,7 +1111,7 @@ mod blocking_io { } #[test] - fn fetch_and_checkout_specific_non_existing() -> Result { + fn fetch_and_checkout_specific_non_existing() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1145,7 +1137,7 @@ mod blocking_io { } #[test] - fn fetch_retries_without_the_implicit_head_refspec_on_conflict() -> Result { + fn fetch_retries_without_the_implicit_head_refspec_on_conflict() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1173,7 +1165,7 @@ mod blocking_io { } #[test] - fn fetch_and_checkout_specific_annotated_tag() -> Result { + fn fetch_and_checkout_specific_annotated_tag() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1197,7 +1189,7 @@ mod blocking_io { let (repo, _) = checkout.main_worktree(gix::progress::Discard, &AtomicBool::default())?; - assert_eq!(repo.is_shallow(), shallow); + assert_eq!(repo.is_shallow()?, shallow); let remote_ref_name = format!("refs/tags/{ref_to_checkout}"); if shallow { let remote = repo.find_remote("origin")?; @@ -1233,15 +1225,16 @@ mod blocking_io { Ok(()) } - fn assure_index_entries_on_disk(index: &gix::worktree::Index, work_dir: &Path) { + fn assure_index_entries_on_disk(index: &gix::worktree::Index, work_dir: &Path) -> Result { for entry in index.entries() { - let entry_path = work_dir.join(gix_path::from_bstr(entry.path(index))); + let entry_path = work_dir.join(gix_path::from_bstr(entry.path(index))?); assert!(entry_path.is_file(), "\"{}\" not found on disk", entry_path.display()); } + Ok(()) } #[test] - fn fetch_and_checkout_empty_remote_repo() -> Result { + fn fetch_and_checkout_empty_remote_repo() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1293,7 +1286,7 @@ mod blocking_io { } #[test] - fn fetch_only_without_configuration() -> Result { + fn fetch_only_without_configuration() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1321,7 +1314,7 @@ mod blocking_io { #[test] #[cfg(feature = "sha256")] - fn fetch_only_adopts_remote_sha256_object_format() -> Result { + fn fetch_only_adopts_remote_sha256_object_format() -> TestResult { let remote = gix_testtools::scripted_fixture_read_only("make_sha256_remote.sh")?.join("remote"); assert_eq!( gix::open_opts(&remote, gix::open::Options::isolated())?.object_hash(), @@ -1369,7 +1362,7 @@ mod blocking_io { } #[test] -fn clone_ignores_repository_paths_and_keeps_preferences() -> Result { +fn clone_ignores_repository_paths_and_keeps_preferences() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1400,7 +1393,7 @@ fn clone_ignores_repository_paths_and_keeps_preferences() -> Result { "the clone keeps its index in its own git directory" ); assert_eq!( - repo.editor(), + repo.editor()?, Some("git-editor".into()), "the clone honors the caller's editor preference" ); @@ -1413,7 +1406,7 @@ fn clone_ignores_repository_paths_and_keeps_preferences() -> Result { } #[test] -fn clone_and_early_persist_without_receive() -> Result { +fn clone_and_early_persist_without_receive() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let repo = gix::clone::PrepareFetch::new( remote::repo("base").path(), @@ -1429,7 +1422,7 @@ fn clone_and_early_persist_without_receive() -> Result { } #[test] -fn clone_and_destination_must_be_empty() -> Result { +fn clone_and_destination_must_be_empty() -> TestResult { let mut error_snapshots = Vec::new(); let tmp = gix_testtools::tempfile::TempDir::new()?; std::fs::write(tmp.path().join("file"), b"hello")?; @@ -1467,7 +1460,7 @@ fn clone_and_destination_must_be_empty() -> Result { } #[test] -fn clone_with_worktree_and_destination_must_be_empty() -> Result { +fn clone_with_worktree_and_destination_must_be_empty() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_clone_destinations.sh")?; let destination = fixture.path().join("non-empty"); let err = gix::clone::PrepareFetch::new( @@ -1494,7 +1487,7 @@ fn clone_with_worktree_and_destination_must_be_empty() -> Result { } #[test] -fn clone_bare_into_empty_directory_and_early_drop() -> Result { +fn clone_bare_into_empty_directory_and_early_drop() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; // this breaks isolation, but shouldn't be affecting the test. If so, use isolation options for opening the repo. let prep = gix::clone::PrepareFetch::new( @@ -1513,7 +1506,7 @@ fn clone_bare_into_empty_directory_and_early_drop() -> Result { } #[test] -fn clone_into_empty_directory_and_early_drop() -> Result { +fn clone_into_empty_directory_and_early_drop() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let prep = gix::clone::PrepareFetch::new( remote::repo("base").path(), diff --git a/gix/tests/gix/commit.rs b/gix/tests/gix/commit.rs index 00c0df623d1..fa892478814 100644 --- a/gix/tests/gix/commit.rs +++ b/gix/tests/gix/commit.rs @@ -1,19 +1,19 @@ #[cfg(feature = "revision")] mod describe { - use crate::Result; use gix::commit::describe::SelectRef::{AllRefs, AllTags, AnnotatedTags}; + use gix_testtools::TestResult; use crate::named_repo; #[cfg(feature = "status")] mod with_dirty_suffix { - use crate::Result; use gix::commit::describe::SelectRef; + use gix_testtools::TestResult; use crate::util::named_subrepo_opts; #[test] - fn dirty_suffix_applies_automatically_if_dirty() -> Result { + fn dirty_suffix_applies_automatically_if_dirty() -> TestResult { let repo = named_subrepo_opts( "make_submodules.sh", "submodule-head-changed", @@ -33,7 +33,7 @@ mod describe { } #[test] - fn dirty_suffix_does_not_apply_if_not_dirty() -> Result { + fn dirty_suffix_does_not_apply_if_not_dirty() -> TestResult { let repo = named_subrepo_opts("make_submodules.sh", "module1", gix::open::Options::isolated())?; let actual = repo @@ -50,7 +50,7 @@ mod describe { } #[test] - fn tags_are_sorted_by_date_and_lexicographically() -> Result { + fn tags_are_sorted_by_date_and_lexicographically() -> TestResult { let repo = named_repo("make_commit_describe_multiple_tags.sh")?; let mut describe = repo.head_commit()?.describe(); for filter in &[AnnotatedTags, AllTags, AllRefs] { @@ -61,7 +61,7 @@ mod describe { } #[test] - fn tags_are_sorted_by_priority() -> Result { + fn tags_are_sorted_by_priority() -> TestResult { let repo = named_repo("make_commit_describe_multiple_tags.sh")?; let commit = repo.find_reference("refs/tags/v0")?.id().object()?.into_commit(); let mut describe = commit.describe(); @@ -73,7 +73,7 @@ mod describe { } #[test] - fn lightweight_tags_are_sorted_lexicographically() -> Result { + fn lightweight_tags_are_sorted_lexicographically() -> TestResult { let repo = named_repo("make_commit_describe_multiple_tags.sh")?; let commit = repo.find_reference("refs/tags/l0")?.id().object()?.into_commit(); let mut describe = commit.describe(); @@ -92,8 +92,8 @@ mod describe { #[cfg(feature = "command")] mod signature { - use crate::Result; use gix::config::tree::{Gpg, Key, User, gpg}; + use gix_testtools::TestResult; use gix_testtools::signature; use serial_test::serial; @@ -102,7 +102,7 @@ mod signature { // `fatal: invalid date format: 42 +0030` while creating the reference signature. #[serial] #[cfg_attr(windows, ignore = "TODO: requires direct investigation on Windows")] - fn verifies_a_commit_signed_by_git_with_ssh() -> Result { + fn verifies_a_commit_signed_by_git_with_ssh() -> TestResult { if !signature::program_available("ssh-keygen") { return Ok(()); } @@ -112,13 +112,13 @@ mod signature { let email = User::EMAIL.validated_assignment_fmt(&signature::IDENTITY)?; // Git for Windows passes this native path to `ssh-keygen`, where forward slashes avoid backslash // interpretation while retaining the drive prefix. An MSYS path like `/c/...` isn't accepted here. - let key_for_git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&key)); + let key_for_git = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&key)?); let signing_key = User::SIGNING_KEY.validated_assignment(key_for_git.as_ref())?; let output = gix_testtools::git_command(fixture.path()) .args(["-c", "user.name=Gitoxide Signing Fixture", "-c"]) - .arg(gix_path::from_bstring(email).into_os_string()) + .arg(gix_path::from_bstring(email)?.into_os_string()) .args(["-c", "gpg.format=ssh", "-c"]) - .arg(gix_path::from_bstring(signing_key).into_os_string()) + .arg(gix_path::from_bstring(signing_key)?.into_os_string()) .args(["commit", "--allow-empty", "-S", "-m", "signed by Git"]) .output()?; assert!( @@ -147,7 +147,7 @@ mod signature { } #[test] - fn sign_write_and_verify_an_ssh_commit() -> Result { + fn sign_write_and_verify_an_ssh_commit() -> TestResult { let (_key_home, key) = signature::ssh_private_key()?; let options = gix::open::Options::isolated().config_overrides([ User::NAME.validated_assignment_fmt(&"Gitoxide Signing Fixture")?, @@ -192,7 +192,7 @@ mod signature { } #[test] - fn resolves_format_defaults_and_program_paths() -> Result { + fn resolves_format_defaults_and_program_paths() -> TestResult { let home = gix::path::env::home_dir().expect("the test environment has a home directory"); let mut permissions = gix::open::Permissions::isolated(); permissions.env.home = gix::sec::Permission::Allow; @@ -215,7 +215,7 @@ mod signature { } #[test] - fn expands_verification_program_paths() -> Result { + fn expands_verification_program_paths() -> TestResult { let home = gix::path::env::home_dir().expect("the test environment has a home directory"); let mut permissions = gix::open::Permissions::isolated(); permissions.env.home = gix::sec::Permission::Allow; @@ -246,7 +246,7 @@ mod signature { } #[test] - fn resolves_signing_options_only_when_enabled() -> Result { + fn resolves_signing_options_only_when_enabled() -> TestResult { let disabled = gix::open_opts( gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?, gix::open::Options::isolated().config_overrides(["gpg.format=invalid"]), @@ -273,7 +273,7 @@ mod signature { #[cfg(unix)] #[test] - fn resolves_the_default_ssh_key_command() -> Result { + fn resolves_the_default_ssh_key_command() -> TestResult { let options = gix::open::Options::isolated().config_overrides([ "user.name=Gitoxide Signing Fixture", "user.email=signing@example.com", diff --git a/gix/tests/gix/config/file_mut.rs b/gix/tests/gix/config/file_mut.rs index 117235b09b3..b709073bcd8 100644 --- a/gix/tests/gix/config/file_mut.rs +++ b/gix/tests/gix/config/file_mut.rs @@ -1,10 +1,10 @@ -use crate::Result; use crate::repository::config::config_snapshot::options_with_includes; use crate::{named_repo, repo_rw_opts}; +use gix_testtools::TestResult; use std::io::Write; #[test] -fn locks_and_edits_one_physical_file_until_explicit_reload() -> Result { +fn locks_and_edits_one_physical_file_until_explicit_reload() -> TestResult { let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options_with_includes())?; let git_config_path = repo.git_dir().join("config"); let target_path = repo.workdir().expect("worktree repository").join("a.config"); @@ -102,7 +102,7 @@ leading = value } #[test] -fn honors_core_config_lock_timeout() -> Result { +fn honors_core_config_lock_timeout() -> TestResult { let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", gix::open::Options::isolated())?; let config_path = repo.git_dir().join("config"); let mut lock_path = config_path.as_os_str().to_owned(); @@ -143,7 +143,7 @@ fn honors_core_config_lock_timeout() -> Result { } #[test] -fn preserves_permissions() -> Result { +fn preserves_permissions() -> TestResult { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -184,7 +184,7 @@ fn preserves_permissions() -> Result { #[test] #[cfg(unix)] -fn follows_symlinked_configuration_files() -> Result { +fn follows_symlinked_configuration_files() -> TestResult { use std::os::unix::fs::symlink; let repo = named_repo("make_basic_repo.sh")?; @@ -232,7 +232,7 @@ fn follows_symlinked_configuration_files() -> Result { } #[test] -fn supports_empty_and_missing_files() -> Result { +fn supports_empty_and_missing_files() -> TestResult { let repo = named_repo("make_basic_repo.sh")?; let dir = gix_testtools::tempfile::tempdir()?; let empty = dir.path().join("empty.config"); @@ -258,7 +258,7 @@ fn supports_empty_and_missing_files() -> Result { #[test] #[cfg(unix)] -fn applies_shared_repository_permissions_to_new_files() -> Result { +fn applies_shared_repository_permissions_to_new_files() -> TestResult { use std::os::unix::fs::PermissionsExt; let mut repo = named_repo("make_basic_repo.sh")?; @@ -279,7 +279,7 @@ fn applies_shared_repository_permissions_to_new_files() -> Result { } #[test] -fn semantic_validation_happens_on_reload() -> Result { +fn semantic_validation_happens_on_reload() -> TestResult { let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options_with_includes().strict_config(true))?; let original_format_version = repo.config_snapshot().integer("core.repositoryFormatVersion"); let config_path = repo.git_dir().join("config"); @@ -313,7 +313,7 @@ fn semantic_validation_happens_on_reload() -> Result { } #[test] -fn include_changes_take_effect_only_after_reload() -> Result { +fn include_changes_take_effect_only_after_reload() -> TestResult { let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options_with_includes())?; let replacement_path = repo.workdir().expect("worktree repository").join("replacement.config"); std::fs::write( @@ -343,7 +343,7 @@ value = visible #[test] #[cfg(unix)] -fn paths_with_parent_components_retain_symlink_semantics() -> Result { +fn paths_with_parent_components_retain_symlink_semantics() -> TestResult { use std::os::unix::fs::symlink; let repo = named_repo("make_basic_repo.sh")?; diff --git a/gix/tests/gix/config/tree.rs b/gix/tests/gix/config/tree.rs index 8495661772b..d095a2da8a8 100644 --- a/gix/tests/gix/config/tree.rs +++ b/gix/tests/gix/config/tree.rs @@ -4,12 +4,12 @@ fn config_value_error(message: &'static str, input: &'static str) -> gix::Error } mod keys { - use crate::Result; use gix::config::tree::{Key, Section}; use gix_object::bstr::{BStr, ByteSlice}; + use gix_testtools::TestResult; #[test] - fn string() -> Result { + fn string() -> TestResult { assert_eq!(gix::config::tree::Http::USER_AGENT.try_into_string("agent")?, "agent"); assert!(gix::config::tree::Http::USER_AGENT.validate("agent".into()).is_ok()); @@ -275,7 +275,7 @@ mod compression { use gix::config::tree::Key; #[test] - fn validate_and_convert() { + fn validate_and_convert() -> gix_testtools::TestResult { for key in [ &gix::config::tree::Core::COMPRESSION, &gix::config::tree::Core::LOOSE_COMPRESSION, @@ -290,9 +290,7 @@ mod compression { } assert_eq!( - gix::config::tree::Core::COMPRESSION - .try_into_compression(Ok(Some(-1))) - .expect("git maps -1 to the zlib default"), + gix::config::tree::Core::COMPRESSION.try_into_compression(Ok(Some(-1)))?, Some(gix::zlib::Compression::DEFAULT) ); assert_eq!( @@ -312,6 +310,7 @@ mod compression { .try_into_compression(Ok(Some(10))) .is_err() ); + Ok(()) } } @@ -319,7 +318,7 @@ mod branch { use gix::config::tree::{Branch, Key, branch}; #[test] - fn merge() { + fn merge() -> gix_testtools::TestResult { assert!(branch::Merge::try_into_fullrefname("refs/heads/main").is_ok()); assert!(branch::Merge::try_into_fullrefname("main").is_err()); assert!( @@ -336,19 +335,17 @@ mod branch { ); assert!(Branch::MERGE.full_name(None).is_err()); - assert_eq!( - Branch::MERGE.full_name(Some("name".into())).expect("valid"), - "branch.name.merge" - ); + assert_eq!(Branch::MERGE.full_name(Some("name".into()))?, "branch.name.merge"); + Ok(()) } } mod ssh { #[cfg(feature = "blocking-network-client")] - use crate::Result; + use gix_testtools::TestResult; #[test] #[cfg(feature = "blocking-network-client")] - fn variant() -> Result { + fn variant() -> TestResult { use gix::config::tree::Ssh; use gix_protocol::transport::client::blocking_io::ssh::ProgramKind; @@ -375,11 +372,11 @@ mod ssh { #[cfg(feature = "status")] mod status { - use crate::Result; use gix::{config::tree::Status, status::UntrackedFiles}; + use gix_testtools::TestResult; #[test] - fn default() -> Result { + fn default() -> TestResult { for (actual, expected) in [ ("no", UntrackedFiles::None), ("normal", UntrackedFiles::Collapsed), @@ -404,11 +401,11 @@ mod status { } mod push { - use crate::Result; use gix::{config::tree::Push, push}; + use gix_testtools::TestResult; #[test] - fn default() -> Result { + fn default() -> TestResult { for (actual, expected) in [ ("nothing", push::Default::Nothing), ("current", push::Default::Current), @@ -434,10 +431,10 @@ mod push { mod fetch { - use crate::Result; + use gix_testtools::TestResult; #[test] #[cfg(feature = "credentials")] - fn algorithm() -> Result { + fn algorithm() -> TestResult { use gix::{ config::tree::{Fetch, Key}, remote::fetch::negotiate::Algorithm, @@ -468,7 +465,7 @@ mod fetch { #[test] #[cfg(feature = "attributes")] - fn recurse_submodule() -> Result { + fn recurse_submodule() -> TestResult { use gix::{ bstr::ByteSlice, config::tree::{Fetch, Key}, @@ -506,15 +503,15 @@ mod fetch { #[cfg(feature = "blob-diff")] mod diff { - use crate::Result; use gix::{ config::tree::{Diff, Key}, diff::rename::Tracking, }; use gix_diff::blob::Algorithm; + use gix_testtools::TestResult; #[test] - fn renames() -> Result { + fn renames() -> TestResult { assert_eq!(Diff::RENAMES.try_into_renames(Ok(Some(true)))?, Some(Tracking::Renames)); assert!(Diff::RENAMES.validate("1".into()).is_ok()); assert_eq!( @@ -567,7 +564,7 @@ mod diff { } #[test] - fn driver_binary() -> Result { + fn driver_binary() -> TestResult { assert_eq!( Diff::DRIVER_BINARY.try_into_binary(Some("auto"))?, None, @@ -599,7 +596,7 @@ mod diff { } #[test] - fn algorithm() -> Result { + fn algorithm() -> TestResult { for (actual, expected) in [ ("myers", Algorithm::Myers), ("Myers", Algorithm::Myers), @@ -619,12 +616,12 @@ mod diff { #[cfg(feature = "merge")] mod merge { - use crate::Result; use gix::config::tree::{Key, Merge}; use gix_merge::blob::builtin_driver::text::ConflictStyle; + use gix_testtools::TestResult; #[test] - fn conflict_style() -> Result { + fn conflict_style() -> TestResult { for (actual, expected) in [ ("merge", ConflictStyle::Merge), ("diff3", ConflictStyle::Diff3), @@ -646,7 +643,7 @@ mod merge { } mod core { - use crate::Result; + use gix_testtools::TestResult; use std::time::Duration; use gix::config::tree::{Core, Key}; @@ -657,7 +654,7 @@ mod core { } #[test] - fn repository_format_version() -> Result { + fn repository_format_version() -> TestResult { use gix::repository::FormatVersion; let key = &Core::REPOSITORY_FORMAT_VERSION; @@ -766,7 +763,7 @@ mod core { } #[test] - fn shared_repository() -> Result { + fn shared_repository() -> TestResult { for (value, expected) in [ (None, 0o660), (Some("umask"), 0), @@ -806,7 +803,7 @@ mod core { } #[test] - fn timeouts() -> Result { + fn timeouts() -> TestResult { assert_eq!( Core::FILES_REF_LOCK_TIMEOUT.try_into_lock_timeout(Ok(Some(0)))?, Some(Fail::Immediately) @@ -836,7 +833,7 @@ mod core { #[test] #[cfg(feature = "revision")] - fn disambiguate() -> Result { + fn disambiguate() -> TestResult { use gix::revision::spec::parse::ObjectKindHint; for (value, expected) in [ ("none", None), @@ -861,7 +858,7 @@ mod core { } #[test] - fn log_all_ref_updates() -> Result { + fn log_all_ref_updates() -> TestResult { assert_eq!( Core::LOG_ALL_REF_UPDATES.try_into_ref_updates(Ok(Some(true)))?, Some(gix_ref::store::WriteReflog::Normal) @@ -923,7 +920,7 @@ mod core { } #[test] - fn abbrev() -> Result { + fn abbrev() -> TestResult { let object_hash = gix_hash::Kind::Sha1; assert_eq!(Core::ABBREV.try_into_abbreviation("4", object_hash)?, Some(4)); for (input, expected) in [("0x4", 4), ("010", 8), ("0b100", 4), ("40", 40)] { @@ -967,7 +964,7 @@ mod core { } #[test] - fn delta_base_cache_limit() -> Result { + fn delta_base_cache_limit() -> TestResult { assert_eq!(Core::DELTA_BASE_CACHE_LIMIT.try_into_usize(signed(1))?, Some(1)); assert_eq!(Core::DELTA_BASE_CACHE_LIMIT.try_into_usize(signed(0))?, Some(0)); assert!(Core::DELTA_BASE_CACHE_LIMIT.validate("0".into()).is_ok()); @@ -985,7 +982,7 @@ mod core { } #[test] - fn check_stat() -> Result { + fn check_stat() -> TestResult { assert!(Core::CHECK_STAT.try_into_checkstat("default")?); assert!(!Core::CHECK_STAT.try_into_checkstat("minimal")?); crate::config::key::assert_config_error( @@ -1005,7 +1002,7 @@ mod core { #[test] #[cfg(feature = "attributes")] - fn safecrlf() -> Result { + fn safecrlf() -> TestResult { for (value, expected) in [ ("false", gix_filter::pipeline::CrlfRoundTripCheck::Skip), ("true", gix_filter::pipeline::CrlfRoundTripCheck::Fail), @@ -1027,7 +1024,7 @@ mod core { #[test] #[cfg(feature = "attributes")] - fn autocrlf() -> Result { + fn autocrlf() -> TestResult { for (value, expected) in [ ("false", gix_filter::eol::AutoCrlf::Disabled), ("true", gix_filter::eol::AutoCrlf::Enabled), @@ -1049,7 +1046,7 @@ mod core { #[test] #[cfg(feature = "attributes")] - fn eol() -> Result { + fn eol() -> TestResult { for (value, expected) in [ ("lf", gix_filter::eol::Mode::Lf), ("crlf", gix_filter::eol::Mode::CrLf), @@ -1069,7 +1066,7 @@ mod core { #[test] #[cfg(feature = "attributes")] - fn check_round_trip_encoding() -> Result { + fn check_round_trip_encoding() -> TestResult { for (value, expected) in [ ( Some("UTF-8 utf-16BE"), @@ -1101,11 +1098,11 @@ mod core { } mod index { - use crate::Result; use gix::config::tree::{Index, Key}; + use gix_testtools::TestResult; #[test] - fn threads() -> Result { + fn threads() -> TestResult { for (value, expected) in [ ("false", 1), ("true", 0), @@ -1144,11 +1141,11 @@ mod index { } mod extensions { - use crate::Result; use gix::config::tree::{Extensions, Key}; + use gix_testtools::TestResult; #[test] - fn object_format() -> Result { + fn object_format() -> TestResult { #[cfg(feature = "sha1")] { assert_eq!( @@ -1189,15 +1186,15 @@ mod extensions { } mod checkout { - use crate::Result; use gix::config::tree::{Checkout, Key}; + use gix_testtools::TestResult; fn int(value: i64) -> gix::Result<Option<i64>> { Ok(Some(value)) } #[test] - fn workers() -> Result { + fn workers() -> TestResult { assert!(Checkout::WORKERS.validate("0".into()).is_ok()); assert_eq!(Checkout::WORKERS.try_from_workers(int(0))?, Some(0)); assert!(Checkout::WORKERS.validate("-1".into()).is_ok()); @@ -1210,11 +1207,11 @@ mod checkout { } mod pack { - use crate::Result; use gix::config::tree::{Key, Pack}; + use gix_testtools::TestResult; #[test] - fn index_version() -> Result { + fn index_version() -> TestResult { assert_eq!( Pack::INDEX_VERSION.try_into_index_version(Ok(Some(1)))?, Some(gix_pack::index::Version::V1) @@ -1245,13 +1242,13 @@ mod pack { } mod protocol { - #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] - use crate::Result; use gix::config::tree::{Key, Protocol}; + #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] + use gix_testtools::TestResult; #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))] #[test] - fn allow() -> Result { + fn allow() -> TestResult { let mut error_snapshots = Vec::new(); use gix::{config::tree::protocol, remote::url::scheme_permission::Allow}; @@ -1298,7 +1295,7 @@ mod protocol { } #[test] - fn version() { + fn version() -> gix_testtools::TestResult { for valid in [0, 1, 2] { assert!(Protocol::VERSION.validate(valid.to_string().as_str().into()).is_ok()); } @@ -1322,12 +1319,7 @@ mod protocol { (Some(1), gix_protocol::transport::Protocol::V1), (Some(2), gix_protocol::transport::Protocol::V2), ] { - assert_eq!( - Protocol::VERSION - .try_into_protocol_version(Ok(valid)) - .expect("valid version"), - expected - ); + assert_eq!(Protocol::VERSION.try_into_protocol_version(Ok(valid))?, expected); } crate::config::key::assert_config_error( @@ -1339,18 +1331,19 @@ mod protocol { None, ); } + Ok(()) } } mod gpg { - use crate::Result; use gix::{ bstr::BStr, config::tree::{Gpg, Key, Section, gpg}, }; + use gix_testtools::TestResult; #[test] - fn format() -> Result { + fn format() -> TestResult { use gix_object::signature::Format; assert_eq!( @@ -1438,9 +1431,9 @@ mod gpg { } mod notes { - use crate::Result; use gix::config::tree::{Key, Notes}; use gix_object::bstr::BString; + use gix_testtools::TestResult; #[test] fn display_ref_metadata() { @@ -1453,7 +1446,7 @@ mod notes { } #[test] - fn display_ref_parsing() -> Result { + fn display_ref_parsing() -> TestResult { assert_eq!( Notes::DISPLAY_REF.try_into_display_refs(":refs/notes/review::refs/notes/*:")?, vec![BString::from("refs/notes/review"), BString::from("refs/notes/*")], @@ -1488,13 +1481,13 @@ mod notes { mod gitoxide { mod http { - use crate::Result; + use gix_testtools::TestResult; use std::time::Duration; use gix::config::tree::{Key, gitoxide}; #[test] - fn connect_timeout() -> Result { + fn connect_timeout() -> TestResult { assert_eq!( gitoxide::Http::CONNECT_TIMEOUT.validated_assignment_fmt(&Duration::from_secs(1).as_millis())?, "gitoxide.http.connectTimeout=1000" @@ -1520,65 +1513,56 @@ mod gitoxide { use gix::config::tree::{Key, gitoxide}; #[test] - fn author_and_committer_date() { + fn author_and_committer_date() -> gix_testtools::TestResult { assert_eq!( - gitoxide::Commit::AUTHOR_DATE - .validated_assignment("Thu, 1 Aug 2022 12:45:06 +0800".into()) - .expect("valid"), + gitoxide::Commit::AUTHOR_DATE.validated_assignment("Thu, 1 Aug 2022 12:45:06 +0800".into())?, "gitoxide.commit.authorDate=Thu, 1 Aug 2022 12:45:06 +0800" ); assert_eq!( - gitoxide::Commit::COMMITTER_DATE - .validated_assignment("Thu, 1 Aug 2022 12:45:06 +0800".into()) - .expect("valid"), + gitoxide::Commit::COMMITTER_DATE.validated_assignment("Thu, 1 Aug 2022 12:45:06 +0800".into())?, "gitoxide.commit.committerDate=Thu, 1 Aug 2022 12:45:06 +0800" ); + Ok(()) } } mod author { use gix::config::tree::{Key, gitoxide}; #[test] - fn name_and_email_fallback() { + fn name_and_email_fallback() -> gix_testtools::TestResult { assert_eq!( - gitoxide::Author::NAME_FALLBACK - .validated_assignment("name".into()) - .expect("valid"), + gitoxide::Author::NAME_FALLBACK.validated_assignment("name".into())?, "gitoxide.author.nameFallback=name" ); assert_eq!( - gitoxide::Author::EMAIL_FALLBACK - .validated_assignment("email".into()) - .expect("valid"), + gitoxide::Author::EMAIL_FALLBACK.validated_assignment("email".into())?, "gitoxide.author.emailFallback=email" ); + Ok(()) } } mod committer { use gix::config::tree::{Key, gitoxide}; #[test] - fn name_and_email_fallback() { + fn name_and_email_fallback() -> gix_testtools::TestResult { assert_eq!( - gitoxide::Committer::NAME_FALLBACK - .validated_assignment("name".into()) - .expect("valid"), + gitoxide::Committer::NAME_FALLBACK.validated_assignment("name".into())?, "gitoxide.committer.nameFallback=name" ); assert_eq!( - gitoxide::Committer::EMAIL_FALLBACK - .validated_assignment("email".into()) - .expect("valid"), + gitoxide::Committer::EMAIL_FALLBACK.validated_assignment("email".into())?, "gitoxide.committer.emailFallback=email" ); + Ok(()) } } mod objects { - use crate::Result; use gix::config::tree::{Key, gitoxide}; + use gix_testtools::TestResult; #[test] - fn alloc_limit() -> Result { + fn alloc_limit() -> TestResult { assert_eq!( gitoxide::Objects::ALLOC_LIMIT.validated_assignment("16m".into())?, "gitoxide.objects.allocLimit=16m" @@ -1587,7 +1571,7 @@ mod gitoxide { } #[test] - fn alloc_limit_if_reduced_trust() -> Result { + fn alloc_limit_if_reduced_trust() -> TestResult { assert_eq!( gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST.validated_assignment("16m".into())?, "gitoxide.objects.allocLimitIfReducedTrust=16m" @@ -1602,12 +1586,12 @@ mod gitoxide { feature = "blocking-http-transport-curl" ))] mod http { - use crate::Result; use gix::config::tree::{Http, Key}; use gix_object::bstr::ByteSlice; + use gix_testtools::TestResult; #[test] - fn follow_redirects() -> Result { + fn follow_redirects() -> TestResult { use gix_transport::client::blocking_io::http::options::FollowRedirects; assert_eq!( Http::FOLLOW_REDIRECTS.try_into_follow_redirects("initial", || unreachable!("no call"))?, @@ -1648,7 +1632,7 @@ mod http { } #[test] - fn extra_header() -> Result { + fn extra_header() -> TestResult { assert_eq!(Http::EXTRA_HEADER.try_into_extra_header(vec!["a", "b"])?, ["a", "b"]); assert_eq!( Http::EXTRA_HEADER.try_into_extra_header(vec!["a", "b", "", "c", "d"])?, @@ -1671,7 +1655,7 @@ mod http { } #[test] - fn http_version() -> Result { + fn http_version() -> TestResult { use gix_transport::client::blocking_io::http::options::HttpVersion; for (actual, expected) in [("HTTP/1.1", HttpVersion::V1_1), ("HTTP/2", HttpVersion::V2)] { @@ -1694,7 +1678,7 @@ mod http { } #[test] - fn ssl_version() -> Result { + fn ssl_version() -> TestResult { use gix_transport::client::blocking_io::http::options::SslVersion::*; for (actual, expected) in [ @@ -1725,7 +1709,7 @@ mod http { } #[test] - fn proxy_auth_method() -> Result { + fn proxy_auth_method() -> TestResult { use gix_transport::client::blocking_io::http::options::ProxyAuthMethod::*; for (actual, expected) in [ ("anyauth", AnyAuth), @@ -1752,14 +1736,14 @@ mod http { } mod remote { - use crate::Result; use gix::{ config::tree::{Key, Remote}, remote, }; + use gix_testtools::TestResult; #[test] - fn tag_opt() -> Result { + fn tag_opt() -> TestResult { assert_eq!(Remote::TAG_OPT.try_into_tag_opt("--tags")?, remote::fetch::Tags::All); assert!(Remote::TAG_OPT.validate("--tags".into()).is_ok()); assert_eq!( diff --git a/gix/tests/gix/diff.rs b/gix/tests/gix/diff.rs index b2d23537525..2d38a836599 100644 --- a/gix/tests/gix/diff.rs +++ b/gix/tests/gix/diff.rs @@ -1,10 +1,10 @@ -use crate::Result; use gix_diff::blob::{Algorithm, Driver}; +use gix_testtools::TestResult; use crate::util::named_repo; #[test] -fn resource_cache() -> Result { +fn resource_cache() -> TestResult { let repo = named_repo("make_diff_repo.sh")?; let index = repo.index()?; let cache = gix::diff::resource_cache( diff --git a/gix/tests/gix/error.rs b/gix/tests/gix/error.rs index 673f3a65a6a..cced8577a30 100644 --- a/gix/tests/gix/error.rs +++ b/gix/tests/gix/error.rs @@ -187,7 +187,7 @@ fn worktree_and_branch_rejections_have_intrinsic_recovery_classes() { } #[test] -fn opening_keeps_configuration_failure_classification_specific() -> gix_error::TestResult { +fn opening_keeps_configuration_failure_classification_specific() -> gix_testtools::TestResult { let directory = gix_testtools::tempfile::TempDir::new()?; let repo = crate::init_repo_isolated(directory.path(), gix::create::Kind::Bare)?; std::fs::write( diff --git a/gix/tests/gix/head.rs b/gix/tests/gix/head.rs index a41789ecc5d..04037a2c08b 100644 --- a/gix/tests/gix/head.rs +++ b/gix/tests/gix/head.rs @@ -1,9 +1,9 @@ mod peel { - use crate::Result; use crate::util::{hex_to_id, named_subrepo_opts}; + use gix_testtools::TestResult; #[test] - fn all_cases() -> Result { + fn all_cases() -> TestResult { let expected_commit = hex_to_id("fafd9d08a839d99db60b222cd58e2e0bfaf1f7b2"); for name in ["detached", "symbolic", "tag-detached", "tag-symbolic"] { let repo = named_subrepo_opts("make_head_repos.sh", name, gix::open::Options::isolated())?; @@ -22,7 +22,7 @@ mod peel { } #[test] - fn a_missing_detached_head_object_is_not_an_empty_tree() -> Result { + fn a_missing_detached_head_object_is_not_an_empty_tree() -> TestResult { let (repo, _keep) = crate::basic_rw_repo()?; let mut missing_id = repo.object_hash().null(); missing_id.as_mut_slice()[0] = 1; @@ -38,11 +38,11 @@ mod peel { } mod into_remote { - use crate::Result; use crate::remote; + use gix_testtools::TestResult; #[test] - fn unborn_is_none() -> Result { + fn unborn_is_none() -> TestResult { let repo = remote::repo("url-rewriting"); assert_eq!( repo.head()?.into_remote(gix::remote::Direction::Fetch).transpose()?, @@ -57,7 +57,7 @@ mod into_remote { } #[test] - fn detached_is_none() -> Result { + fn detached_is_none() -> TestResult { let repo = remote::repo("detached-head"); assert_eq!( repo.head()?.into_remote(gix::remote::Direction::Fetch).transpose()?, diff --git a/gix/tests/gix/id.rs b/gix/tests/gix/id.rs index f8c5ef3482a..d4121fa3a64 100644 --- a/gix/tests/gix/id.rs +++ b/gix/tests/gix/id.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::cmp::Ordering; use crate::util::hex_to_id; @@ -9,7 +9,7 @@ use gix::{ use gix_object::bstr::BString; #[test] -fn prefix() -> Result { +fn prefix() -> TestResult { let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local(); let work_dir = repo.workdir().expect("non-bare"); let id = hex_to_id("288e509293165cb5630d08f4185bdf2445bf6170").attach(&repo); @@ -71,7 +71,7 @@ fn prefix() -> Result { } #[test] -fn shortening_missing_object_is_not_found() -> Result { +fn shortening_missing_object_is_not_found() -> TestResult { let directory = gix_testtools::tempfile::TempDir::new()?; let repo = crate::init_repo_isolated(directory.path(), gix::create::Kind::Bare)?.to_thread_local(); let missing_blob_id = gix_hash::ObjectId::null(repo.object_hash()).attach(&repo); @@ -86,7 +86,7 @@ fn shortening_missing_object_is_not_found() -> Result { } #[test] -fn display_and_debug() -> Result { +fn display_and_debug() -> TestResult { let expected = match gix_testtools::object_hash() { gix_hash::Kind::Sha1 => { "3189cd3cb0af8586c39a838aa3e54fd72a872a41 Sha1(3189cd3cb0af8586c39a838aa3e54fd72a872a41)" @@ -104,7 +104,7 @@ fn display_and_debug() -> Result { } #[test] -fn compares_with_text() -> Result { +fn compares_with_text() -> TestResult { let repo = crate::basic_repo()?; let id = repo.head_id()?; let text = id.to_string(); @@ -119,11 +119,11 @@ fn compares_with_text() -> Result { } mod ancestors { - use crate::Result; use crate::util::hex_to_id; + use gix_testtools::TestResult; #[test] - fn all() -> Result { + fn all() -> TestResult { let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local(); let has_commit_graph = repo.commit_graph_if_enabled()?.is_some(); for use_commit_graph in [false, true] { @@ -189,7 +189,7 @@ mod ancestors { } #[test] - fn pre_epoch() -> Result { + fn pre_epoch() -> TestResult { let repo = crate::repo("make_pre_epoch_repo.sh")?.to_thread_local(); for use_commit_graph in [false, true] { let head = repo.head()?.into_peeled_id()?; @@ -209,7 +209,7 @@ mod ancestors { } #[test] - fn prune_with_auto_cutoff() -> Result { + fn prune_with_auto_cutoff() -> TestResult { let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local(); let head = repo.head()?.into_peeled_id()?; @@ -231,7 +231,7 @@ mod ancestors { } #[test] - fn filtered() -> Result { + fn filtered() -> TestResult { let repo = crate::repo("make_repo_with_fork_and_dates.sh")?.to_thread_local(); let head = repo.head()?.into_peeled_id()?; diff --git a/gix/tests/gix/init.rs b/gix/tests/gix/init.rs index 29236d06a8f..b7d136671b2 100644 --- a/gix/tests/gix/init.rs +++ b/gix/tests/gix/init.rs @@ -1,10 +1,10 @@ mod bare { - use crate::Result; + use gix_testtools::TestResult; use gix_testtools::tempfile; #[test] #[serial_test::serial] - fn init_into_non_existing_directory_creates_it() -> Result { + fn init_into_non_existing_directory_creates_it() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let tmp = tempfile::tempdir()?; let git_dir = tmp.path().join("bare.git"); @@ -25,7 +25,7 @@ mod bare { #[test] #[serial_test::serial] - fn init_into_empty_directory_uses_it_directly() -> Result { + fn init_into_empty_directory_uses_it_directly() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let tmp = tempfile::tempdir()?; let repo = gix::init_bare(tmp.path())?; @@ -44,7 +44,7 @@ mod bare { } #[test] - fn init_into_non_empty_directory_is_not_allowed() -> Result { + fn init_into_non_empty_directory_is_not_allowed() -> TestResult { let tmp = tempfile::tempdir()?; std::fs::write(tmp.path().join("existing.txt"), b"I was here before you")?; @@ -55,11 +55,11 @@ mod bare { } mod non_bare { - use crate::Result; + use gix_testtools::TestResult; use gix_testtools::tempfile; #[test] - fn init_bare_with_custom_branch_name() -> Result { + fn init_bare_with_custom_branch_name() -> TestResult { let tmp = tempfile::tempdir()?; let repo: gix::Repository = gix::ThreadSafeRepository::init_opts( tmp.path(), @@ -77,7 +77,7 @@ mod non_bare { } #[test] - fn init_bare_with_fully_qualified_custom_branch_name_is_not_prefixed_again() -> Result { + fn init_bare_with_fully_qualified_custom_branch_name_is_not_prefixed_again() -> TestResult { let tmp = tempfile::tempdir()?; let repo: gix::Repository = gix::ThreadSafeRepository::init_opts( tmp.path(), @@ -100,7 +100,7 @@ mod non_bare { } #[test] - fn init_bare_rejects_reserved_branch_name() -> Result { + fn init_bare_rejects_reserved_branch_name() -> TestResult { let tmp = tempfile::tempdir()?; let err = gix::ThreadSafeRepository::init_opts( tmp.path(), @@ -128,7 +128,7 @@ mod non_bare { } #[test] - fn init_bare_rejects_reserved_fully_qualified_branch_name() -> Result { + fn init_bare_rejects_reserved_fully_qualified_branch_name() -> TestResult { let tmp = tempfile::tempdir()?; let err = gix::ThreadSafeRepository::init_opts( tmp.path(), @@ -161,7 +161,7 @@ mod non_bare { #[test] #[serial_test::serial] - fn init_into_empty_directory_creates_a_dot_git_dir() -> Result { + fn init_into_empty_directory_creates_a_dot_git_dir() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let tmp = tempfile::tempdir()?; let repo = gix::init(tmp.path())?; @@ -181,7 +181,7 @@ mod non_bare { } #[test] - fn init_into_non_empty_directory_is_allowed_if_option_is_none_or_false() -> Result { + fn init_into_non_empty_directory_is_allowed_if_option_is_none_or_false() -> TestResult { for destination_must_be_empty in [None, Some(false)] { let tmp = tempfile::tempdir()?; std::fs::write(tmp.path().join("existing.txt"), b"I was here before you")?; @@ -206,7 +206,7 @@ mod non_bare { } #[test] - fn init_into_non_empty_directory_is_not_allowed_if_option_is_true() -> Result { + fn init_into_non_empty_directory_is_not_allowed_if_option_is_true() -> TestResult { let tmp = tempfile::tempdir()?; std::fs::write(tmp.path().join("existing.txt"), b"I was here before you")?; diff --git a/gix/tests/gix/object/commit.rs b/gix/tests/gix/object/commit.rs index 6d91cbb3f7f..aad7e16f18f 100644 --- a/gix/tests/gix/object/commit.rs +++ b/gix/tests/gix/object/commit.rs @@ -1,10 +1,10 @@ -use crate::Result; +use gix_testtools::TestResult; use std::cmp::Ordering; use crate::{basic_repo, util::hex_to_id}; #[test] -fn short_id() -> Result { +fn short_id() -> TestResult { let repo = basic_repo()?; let commit = repo.head_commit()?; assert_eq!(commit.short_id()?.cmp_oid(&commit.id), Ordering::Equal); @@ -12,7 +12,7 @@ fn short_id() -> Result { } #[test] -fn tree() -> Result { +fn tree() -> TestResult { let repo = basic_repo()?; let tree_id = repo.head_tree_id()?; assert_eq!(tree_id, hex_to_id("21d3ba9a26b790a4858d67754ae05d04dfce4d0c")); @@ -31,7 +31,7 @@ fn tree() -> Result { } #[test] -fn decode() -> Result { +fn decode() -> TestResult { let repo = basic_repo()?; let commit = repo.head_commit()?; assert_eq!(commit.decode()?.message, commit.message_raw()?); diff --git a/gix/tests/gix/object/mod.rs b/gix/tests/gix/object/mod.rs index e52141ec509..43125359559 100644 --- a/gix/tests/gix/object/mod.rs +++ b/gix/tests/gix/object/mod.rs @@ -5,7 +5,7 @@ mod tree; use gix_testtools::size_ok; #[test] -fn public_errors_use_the_crate_result_and_preserve_diagnostics() -> crate::Result { +fn public_errors_use_the_crate_result_and_preserve_diagnostics() -> gix_testtools::TestResult { let repo = crate::basic_repo()?; let mut commit = repo.head_commit()?; commit.data = b"invalid commit".to_vec(); @@ -50,7 +50,7 @@ fn public_errors_use_the_crate_result_and_preserve_diagnostics() -> crate::Resul } #[test] -fn failed_object_conversions_return_the_original_object() -> crate::Result { +fn failed_object_conversions_return_the_original_object() -> gix_testtools::TestResult { use gix::objs::Kind; let repo = crate::basic_repo()?; diff --git a/gix/tests/gix/object/tree/diff.rs b/gix/tests/gix/object/tree/diff.rs index 812b3f00e36..0b12e0ebc5e 100644 --- a/gix/tests/gix/object/tree/diff.rs +++ b/gix/tests/gix/object/tree/diff.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use gix::object::{blob::diff::lines, tree::diff::Change}; use gix_object::{bstr::ByteSlice, tree::EntryKind}; @@ -6,7 +6,7 @@ use gix_object::{bstr::ByteSlice, tree::EntryKind}; use crate::named_repo; #[test] -fn changes_against_tree_modified() -> Result { +fn changes_against_tree_modified() -> TestResult { let repo = named_repo("make_diff_repo.sh")?; let from = tree_named(&repo, "@^{/c3-modification}~1"); let to = tree_named(&repo, ":/c3-modification"); @@ -51,8 +51,8 @@ fn changes_against_tree_modified() -> Result { } } - let mut diff = change.diff(&mut cache).expect("objects available"); - let count = diff.line_counts().expect("no diff error").expect("no binary blobs"); + let mut diff = change.diff(&mut cache)?; + let count = diff.line_counts()?.expect("no binary blobs"); assert_eq!(count.insertions, 1); assert_eq!(count.removals, 0); diff.lines(|hunk| { @@ -71,8 +71,7 @@ fn changes_against_tree_modified() -> Result { lines::Change::Modification { .. } => unreachable!("there was no modification"), } Ok(()) - }) - .expect("infallible"); + })?; i += 1; Ok(std::ops::ControlFlow::Continue(())) @@ -148,7 +147,7 @@ fn changes_against_tree_modified() -> Result { } mod track_rewrites { - use crate::Result; + use gix_testtools::TestResult; use std::collections::HashMap; use gix::{ @@ -163,7 +162,7 @@ mod track_rewrites { use crate::{object::tree::diff::tree_named, util::named_subrepo_opts}; #[test] - fn jj_realistic_needs_to_be_more_clever() -> Result { + fn jj_realistic_needs_to_be_more_clever() -> TestResult { // The test case only works (and is only needed) for SHA-1. // Ideally this can be ported to SHA-256 once rename tracking is par with Git. if gix_testtools::object_hash() == gix::hash::Kind::Sha256 { @@ -443,7 +442,7 @@ mod track_rewrites { } #[test] - fn jj_realistic_directory_rename() -> Result { + fn jj_realistic_directory_rename() -> TestResult { // The test case only works (and is only needed) for SHA-1. // Ideally this can be ported to SHA-256 once rename tracking is par with Git. if gix_testtools::object_hash() == gix::hash::Kind::Sha256 { diff --git a/gix/tests/gix/object/tree/mod.rs b/gix/tests/gix/object/tree/mod.rs index 28be6034a6d..679491bb659 100644 --- a/gix/tests/gix/object/tree/mod.rs +++ b/gix/tests/gix/object/tree/mod.rs @@ -1,5 +1,5 @@ -use crate::Result; use crate::util::{named_repo, named_subrepo_opts}; +use gix_testtools::TestResult; #[cfg(all(feature = "blob-diff", feature = "revision"))] mod diff; @@ -9,7 +9,7 @@ fn worktree_repo() -> std::result::Result<gix::Repository, gix_error::Error> { } #[test] -fn find_entry() -> Result { +fn find_entry() -> TestResult { let repo = named_repo("make_basic_repo.sh")?; let tree = repo.head_commit()?.tree()?; assert_eq!(tree.find_entry("this").expect("present").filename(), "this"); @@ -19,7 +19,7 @@ fn find_entry() -> Result { } #[test] -fn lookup_entry_by_path() -> Result { +fn lookup_entry_by_path() -> TestResult { let repo = worktree_repo()?; let tree = repo.head_commit()?.tree()?; assert_eq!(tree.lookup_entry_by_path("dir/c")?.expect("present").filename(), "c"); @@ -27,7 +27,7 @@ fn lookup_entry_by_path() -> Result { } #[test] -fn decode_uses_the_tree_id_hash_kind() -> Result { +fn decode_uses_the_tree_id_hash_kind() -> TestResult { use gix::bstr::ByteSlice; let repo = named_repo("make_basic_repo.sh")?; @@ -57,10 +57,10 @@ fn decode_uses_the_tree_id_hash_kind() -> Result { } mod peel_to_entry { - use crate::Result; + use gix_testtools::TestResult; #[test] - fn top_level_file_keeps_the_current_tree() -> Result { + fn top_level_file_keeps_the_current_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let root_id = tree.id(); @@ -73,7 +73,7 @@ mod peel_to_entry { } #[test] - fn nested_file_moves_to_the_last_seen_tree() -> Result { + fn nested_file_moves_to_the_last_seen_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let dir_id = tree.lookup_entry(["dir"])?.expect("tree entry").object_id(); @@ -86,7 +86,7 @@ mod peel_to_entry { } #[test] - fn tree_leaf_moves_to_the_returned_tree() -> Result { + fn tree_leaf_moves_to_the_returned_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let dir_id = tree.lookup_entry(["dir"])?.expect("tree entry").object_id(); @@ -104,7 +104,7 @@ mod peel_to_entry { } #[test] - fn missing_top_level_entry_keeps_the_current_tree() -> Result { + fn missing_top_level_entry_keeps_the_current_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let root_id = tree.id(); @@ -117,7 +117,7 @@ mod peel_to_entry { } #[test] - fn missing_nested_entry_moves_to_the_last_seen_tree() -> Result { + fn missing_nested_entry_moves_to_the_last_seen_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let dir_id = tree.lookup_entry(["dir"])?.expect("tree entry").object_id(); @@ -130,7 +130,7 @@ mod peel_to_entry { } #[test] - fn path_continuing_past_a_top_level_file_keeps_the_current_tree() -> Result { + fn path_continuing_past_a_top_level_file_keeps_the_current_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let root_id = tree.id(); @@ -148,7 +148,7 @@ mod peel_to_entry { } #[test] - fn path_continuing_past_a_nested_file_keeps_the_last_seen_tree() -> Result { + fn path_continuing_past_a_nested_file_keeps_the_last_seen_tree() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let dir_id = tree.lookup_entry(["dir"])?.expect("tree entry").object_id(); @@ -166,7 +166,7 @@ mod peel_to_entry { } #[test] - fn by_path_has_the_same_tree_leaf_behavior() -> Result { + fn by_path_has_the_same_tree_leaf_behavior() -> TestResult { let repo = super::worktree_repo()?; let mut tree = repo.head_commit()?.tree()?; let dir_id = tree.lookup_entry(["dir"])?.expect("tree entry").object_id(); diff --git a/gix/tests/gix/reference/mod.rs b/gix/tests/gix/reference/mod.rs index 054231787c0..76de5599f5b 100644 --- a/gix/tests/gix/reference/mod.rs +++ b/gix/tests/gix/reference/mod.rs @@ -1,8 +1,8 @@ -use crate::Result; use gix::remote::Direction; +use gix_testtools::TestResult; #[test] -fn compares_with_name_representations() -> Result { +fn compares_with_name_representations() -> TestResult { use gix::{ bstr::{BString, ByteSlice}, refs::{FullName, FullNameRef, Target}, @@ -61,7 +61,7 @@ mod log { } #[test] -fn remote_name() -> Result { +fn remote_name() -> TestResult { let repo = crate::named_subrepo_opts( "make_remote_config_repos.sh", "multiple-remotes", @@ -85,8 +85,8 @@ fn remote_name() -> Result { } mod find { - use crate::Result; use gix_ref::{FullName, FullNameRef, Target, TargetRef}; + use gix_testtools::{Result, TestResult}; use crate::util::hex_to_id; @@ -95,7 +95,7 @@ mod find { } #[test] - fn missing_reference_is_classified() -> Result { + fn missing_reference_is_classified() -> TestResult { let err = repo()? .find_reference("does-not-exist") .expect_err("the reference is missing"); @@ -105,7 +105,7 @@ mod find { } #[test] - fn invalid_reference_names_are_classified() -> Result { + fn invalid_reference_names_are_classified() -> TestResult { let mut error_snapshots = Vec::new(); let repo = repo()?; for err in [ @@ -140,7 +140,7 @@ mod find { } #[test] - fn and_peel() -> Result { + fn and_peel() -> TestResult { let mut error_snapshots = Vec::new(); let repo = repo()?; let mut packed_tag_ref = repo.try_find_reference("dt1")?.expect("tag to exist"); @@ -267,13 +267,13 @@ mod find { } #[test] - fn and_follow() -> Result { + fn and_follow() -> TestResult { let repo = repo()?; let mut symbolic_ref = repo.find_reference("multi-link-target1")?; - let first_hop = Target::Symbolic(FullName::try_from("refs/tags/multi-link-target2").expect("valid")); + let first_hop = Target::Symbolic(FullName::try_from("refs/tags/multi-link-target2")?); assert_eq!(symbolic_ref.target(), first_hop.to_ref()); - let second_hop = Target::Symbolic(FullName::try_from("refs/remotes/origin/multi-link-target3").expect("valid")); + let second_hop = Target::Symbolic(FullName::try_from("refs/remotes/origin/multi-link-target3")?); symbolic_ref = symbolic_ref.follow().expect("another hop")?; assert_eq!(symbolic_ref.target(), second_hop.to_ref()); diff --git a/gix/tests/gix/reference/remote.rs b/gix/tests/gix/reference/remote.rs index 387b60850e4..94e09f8880e 100644 --- a/gix/tests/gix/reference/remote.rs +++ b/gix/tests/gix/reference/remote.rs @@ -1,8 +1,8 @@ -use crate::Result; use crate::remote; +use gix_testtools::TestResult; #[test] -fn push_defaults_to_fetch() -> Result { +fn push_defaults_to_fetch() -> TestResult { let repo = remote::repo("many-fetchspecs"); let head = repo.head()?; let branch = head.clone().try_into_referent().expect("history"); @@ -32,7 +32,7 @@ fn push_defaults_to_fetch() -> Result { } #[test] -fn separate_push_and_fetch() -> Result { +fn separate_push_and_fetch() -> TestResult { for name in ["push-default", "branch-push-remote"] { let repo = remote::repo(name); let head = repo.head()?; @@ -57,7 +57,7 @@ fn separate_push_and_fetch() -> Result { } #[test] -fn not_configured() -> Result { +fn not_configured() -> TestResult { let repo = remote::repo("base"); let head = repo.head()?; let branch = head.clone().try_into_referent().expect("history"); @@ -79,7 +79,7 @@ fn not_configured() -> Result { } #[test] -fn dot_remote_behind_symbol() -> Result { +fn dot_remote_behind_symbol() -> TestResult { let repo = remote::repo("branch-dot-remote"); let head = repo.head()?; let branch = head.clone().try_into_referent().expect("history"); @@ -104,7 +104,7 @@ fn dot_remote_behind_symbol() -> Result { } #[test] -fn url_as_remote_name() -> Result { +fn url_as_remote_name() -> TestResult { let repo = remote::repo("remote-as-url"); let branch = repo.head_ref()?.expect("history"); diff --git a/gix/tests/gix/remote/connect.rs b/gix/tests/gix/remote/connect.rs index 0f0842d0eda..f8a4bbced81 100644 --- a/gix/tests/gix/remote/connect.rs +++ b/gix/tests/gix/remote/connect.rs @@ -3,12 +3,12 @@ feature = "blocking-http-transport-reqwest" ))] mod http_authentication { - use crate::Result; use gix_error::ErrorExt; + use gix_testtools::TestResult; use std::io::{BufRead, Write}; #[test] - fn cached_credentials_are_selected_without_prompting() -> Result { + fn cached_credentials_are_selected_without_prompting() -> TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -92,8 +92,8 @@ mod http_authentication { #[cfg(feature = "blocking-network-client")] mod blocking_io { mod protocol_allow { - use crate::Result; use gix::remote::Direction::Fetch; + use gix_testtools::TestResult; use serial_test::serial; use crate::remote; @@ -136,7 +136,7 @@ mod blocking_io { #[test] #[serial] - fn user() -> Result { + fn user() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; for (env_value, should_allow) in [ (None, Some(true)), diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index 4d9bf51c74f..23df49bfffa 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -12,15 +12,14 @@ mod shallow { #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod blocking_and_async_io { - use crate::Result; + use gix_testtools::{Result, TestResult}; use std::sync::atomic::AtomicBool; use gix::{ config::tree::Protocol, remote::{Direction::Fetch, fetch, fetch::Status}, }; - #[cfg(feature = "blocking-network-client")] - use gix_error::TestResult; + use gix_features::progress; use gix_protocol::bisync; use gix_testtools::tempfile::TempDir; @@ -296,7 +295,7 @@ mod blocking_and_async_io { #[test] #[cfg(feature = "blocking-network-client")] - fn fetch_with_alternates_adds_tips_from_alternates() -> Result<()> { + fn fetch_with_alternates_adds_tips_from_alternates() -> TestResult { use gix::error::ResultExt; // Isolated repository options don't sanitize the ambient Git config inherited by local `upload-pack`. @@ -353,7 +352,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn local_transport_fetches_head_against_remote_refs() -> Result { + async fn local_transport_fetches_head_against_remote_refs() -> TestResult { // Blocking local transport spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -411,7 +410,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_ignores_dangling_symbolic_refs_during_negotiation() -> Result { + async fn fetch_ignores_dangling_symbolic_refs_during_negotiation() -> TestResult { // Blocking local transport spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -459,7 +458,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_with_multi_round_negotiation() -> Result { + async fn fetch_with_multi_round_negotiation() -> TestResult { // Blocking local clone/fetch spawns `upload-pack`, which inherits ambient Git configuration. // Use a child to keep negotiation I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -545,7 +544,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_shallow_deepen_zero_does_not_fail() -> Result { + async fn fetch_shallow_deepen_zero_does_not_fail() -> TestResult { // Even without deepening, blocking local transport spawns `upload-pack` with ambient Git config. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -589,7 +588,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_shallow_deepen_not_possible() -> Result { + async fn fetch_shallow_deepen_not_possible() -> TestResult { // Blocking local transport spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep shallow-fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -650,7 +649,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_empty_pack() -> Result { + async fn fetch_empty_pack() -> TestResult { // Even without receiving a pack, blocking local transport spawns `upload-pack` with ambient Git config. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -736,7 +735,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_pack_without_local_destination() -> Result { + async fn fetch_pack_without_local_destination() -> TestResult { // Blocking local transport spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -811,7 +810,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetching_a_missing_explicit_ref_fails_even_if_ls_refs_returns_nothing() -> Result { + async fn fetching_a_missing_explicit_ref_fails_even_if_ls_refs_returns_nothing() -> TestResult { // Blocking local ref discovery spawns `upload-pack` with ambient Git config, even for a missing ref. // Isolate it in a child to keep Git I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] @@ -847,7 +846,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn fetch_pack() -> Result { + async fn fetch_pack() -> TestResult { // Blocking local transport spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep fetch I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] diff --git a/gix/tests/gix/remote/ref_map.rs b/gix/tests/gix/remote/ref_map.rs index edc239fe847..c8951b4912a 100644 --- a/gix/tests/gix/remote/ref_map.rs +++ b/gix/tests/gix/remote/ref_map.rs @@ -1,9 +1,9 @@ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod blocking_and_async_io { - use crate::Result; use gix::{config::tree::Protocol, remote::Direction::Fetch}; use gix_features::progress; use gix_protocol::bisync; + use gix_testtools::TestResult; use crate::{ remote, @@ -13,7 +13,7 @@ mod blocking_and_async_io { #[bisync::bisync] #[cfg_attr(feature = "blocking-network-client", test)] #[cfg_attr(feature = "async-network-client-async-std", async_std::test)] - async fn all() -> Result { + async fn all() -> TestResult { // Blocking local ref discovery spawns `upload-pack`, which inherits ambient Git configuration. // Isolate it in a child to keep ref-map I/O parallel without changing the parent environment. #[cfg(feature = "blocking-network-client")] diff --git a/gix/tests/gix/remote/save.rs b/gix/tests/gix/remote/save.rs index fde8551f85b..b36e0eabf4e 100644 --- a/gix/tests/gix/remote/save.rs +++ b/gix/tests/gix/remote/save.rs @@ -1,10 +1,10 @@ mod save_to { - use crate::Result; use crate::{remote, remote::save::uniformize}; + use gix_testtools::TestResult; #[test] - fn named_remotes_save_as_is() -> Result { + fn named_remotes_save_as_is() -> TestResult { let repo = remote::repo("clone"); let remote = repo.find_remote("origin")?; @@ -43,14 +43,14 @@ mod save_to { } mod save_as_to { - use crate::Result; use crate::{ basic_repo, remote, remote::save::{remote_config, uniformize}, }; + use gix_testtools::TestResult; #[test] - fn anonymous_remotes_cannot_be_saved_lacking_a_name() -> Result { + fn anonymous_remotes_cannot_be_saved_lacking_a_name() -> TestResult { let repo = basic_repo()?; let remote = repo.remote_at("https://example.com/path")?; insta::assert_debug_snapshot!(remote @@ -64,7 +64,7 @@ mod save_as_to { } #[test] - fn new_anonymous_remote_with_name() -> Result { + fn new_anonymous_remote_with_name() -> TestResult { let repo = basic_repo()?; let mut remote = repo .remote_at("https://example.com/path")? @@ -103,14 +103,12 @@ mod save_as_to { ); { - let mut new_section = config.section_mut_or_create_new("unrelated", None).expect("works"); + let mut new_section = config.section_mut_or_create_new("unrelated", None)?; new_section.push("a", "value")?; - config - .section_mut_or_create_new("initially-empty-not-removed", "name") - .expect("works"); + config.section_mut_or_create_new("initially-empty-not-removed", "name")?; - let mut existing_section = config.section_mut_or_create_new("remote", "origin").expect("works"); + let mut existing_section = config.section_mut_or_create_new("remote", "origin")?; existing_section.push("free", "should not be removed")?; } remote.save_as_to(remote_name, &mut config)?; @@ -123,7 +121,7 @@ mod save_as_to { } #[test] - fn new_remote_in_presence_of_global_section_writes_to_local_file() -> Result { + fn new_remote_in_presence_of_global_section_writes_to_local_file() -> TestResult { use gix::bstr::ByteSlice; // A repo with no remotes, opened so that `remote.origin` exists only as a non-local (`Api`) // override, mirroring global configuration like `remote.origin.prune = true` (issue #1951). @@ -157,7 +155,7 @@ mod save_as_to { /// saving another `origin` must clear those inherited values locally, or reopening would merge them /// back into the saved URL lists. #[test] - fn inherited_urls_are_saved_with_reset_markers() -> Result { + fn inherited_urls_are_saved_with_reset_markers() -> TestResult { use gix::bstr::{BStr, BString}; let repo = gix::open_opts( @@ -217,7 +215,7 @@ mod save_as_to { /// reset-bearing local section after it so reopening clears the included URL before restoring /// the remote's effective URL list. #[test] - fn reset_markers_follow_later_foreign_url_sections() -> Result { + fn reset_markers_follow_later_foreign_url_sections() -> TestResult { use gix::bstr::{BStr, BString, ByteSlice}; let inherited_url = "https://included.example/path"; diff --git a/gix/tests/gix/repository/blame.rs b/gix/tests/gix/repository/blame.rs index c70d1d0817d..c5632adebdc 100644 --- a/gix/tests/gix/repository/blame.rs +++ b/gix/tests/gix/repository/blame.rs @@ -1,9 +1,9 @@ -use crate::Result; use gix::bstr::BString; +use gix_testtools::TestResult; use std::num::NonZero; #[test] -fn simple() -> Result { +fn simple() -> TestResult { let repo = crate::named_repo("make_blame_repo.sh")?; let suspect = repo.head_id()?; @@ -15,7 +15,7 @@ fn simple() -> Result { } #[test] -fn with_options() -> Result { +fn with_options() -> TestResult { let repo = crate::named_repo("make_blame_repo.sh")?; let options = gix::repository::blame_file::Options { diff --git a/gix/tests/gix/repository/branch.rs b/gix/tests/gix/repository/branch.rs index 43297a8c2ef..ab886b82cff 100644 --- a/gix/tests/gix/repository/branch.rs +++ b/gix/tests/gix/repository/branch.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::io::Write; use gix::refs::{ @@ -11,7 +11,7 @@ fn refname(value: &str) -> FullName { } #[test] -fn deletes_a_batch_and_all_of_its_local_config_without_inspecting_commits() -> Result { +fn deletes_a_batch_and_all_of_its_local_config_without_inspecting_commits() -> TestResult { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; let direct = refname("refs/heads/delete-direct"); let symbolic = refname("refs/heads/delete-symbolic"); @@ -37,7 +37,7 @@ fn deletes_a_batch_and_all_of_its_local_config_without_inspecting_commits() -> R let mut config = std::fs::OpenOptions::new() .append(true) .open(repo.common_dir().join("config"))?; - let included_path_for_config = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&included_path)); + let included_path_for_config = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&included_path)?); write!( config, "\n[branch \"delete-direct\"]\n\tremote = origin\n\ @@ -106,7 +106,7 @@ fn deletes_a_batch_and_all_of_its_local_config_without_inspecting_commits() -> R } #[test] -fn validation_failure_leaves_the_entire_batch_unchanged() -> Result { +fn validation_failure_leaves_the_entire_batch_unchanged() -> TestResult { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; let work_dir = repo .workdir() @@ -140,7 +140,7 @@ fn validation_failure_leaves_the_entire_batch_unchanged() -> Result { } #[test] -fn missing_branches_are_successful_and_their_config_is_removed() -> Result { +fn missing_branches_are_successful_and_their_config_is_removed() -> TestResult { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; let existing = refname("refs/heads/d1"); let missing = refname("refs/heads/does-not-exist"); @@ -214,7 +214,7 @@ fn missing_branches_are_successful_and_their_config_is_removed() -> Result { } #[test] -fn expected_targets_prevent_deleting_a_branch_that_moved() -> crate::Result { +fn expected_targets_prevent_deleting_a_branch_that_moved() -> gix_testtools::TestResult { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; let branch = refname("refs/heads/d1"); let original = repo.find_reference(branch.as_ref())?.target().into_owned(); @@ -247,7 +247,7 @@ fn expected_targets_prevent_deleting_a_branch_that_moved() -> crate::Result { } #[test] -fn expected_targets_delete_the_branch_and_its_config() -> crate::Result { +fn expected_targets_delete_the_branch_and_its_config() -> gix_testtools::TestResult { let (mut repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; let branch = refname("refs/heads/d1"); let expected = repo.find_reference(branch.as_ref())?.target().into_owned(); @@ -271,7 +271,7 @@ fn expected_targets_delete_the_branch_and_its_config() -> crate::Result { } #[test] -fn linked_worktree_branches_are_protected_and_common_config_is_updated() -> Result { +fn linked_worktree_branches_are_protected_and_common_config_is_updated() -> TestResult { // `git worktree add --relative-paths`, used by the fixture, was added in Git 2.48. let Some(fixture) = gix_testtools::scripted_fixture_writable_with_args_with_git_version( "make_worktree_relative_linking.sh", @@ -305,7 +305,7 @@ fn linked_worktree_branches_are_protected_and_common_config_is_updated() -> Resu let mut main = gix::open_opts(&main_path, crate::restricted())?; let linked_work_dir = main - .worktree_proxy_by_id("linked") + .worktree_proxy_by_id("linked")? .expect("the linked worktree is registered") .base()?; let err = main diff --git a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs index f8f4abfaf23..4a1fcd462a1 100644 --- a/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs +++ b/gix/tests/gix/repository/config/config_snapshot/credential_helpers.rs @@ -1,5 +1,5 @@ -use crate::Result; use crate::remote; +use gix_testtools::TestResult; mod baseline { use crate::Result; @@ -127,7 +127,7 @@ fn any_url_calls_global() { } #[test] -fn protect_protocol_defaults_to_true_and_can_be_overridden_per_url() -> Result { +fn protect_protocol_defaults_to_true_and_can_be_overridden_per_url() -> TestResult { let mut repo = remote::repo("credential-helpers"); let url = "https://example.com"; let (cascade, action, _) = repo.config_snapshot().credential_helpers(url.try_into()?)?; @@ -202,7 +202,7 @@ fn subdomain_globs_match_on_their_level() { #[test] #[serial_test::serial] -fn http_urls_match_the_host_without_path_as_well() -> Result { +fn http_urls_match_the_host_without_path_as_well() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("GIT_ASKPASS", "foo"); baseline::agrees_with("http://example.com:8080/other/path"); baseline::agrees_with_but_drops_default_port_in_prompt("http://example.com:80/"); @@ -213,7 +213,7 @@ fn http_urls_match_the_host_without_path_as_well() -> Result { #[test] #[serial_test::serial] -fn user_rules_only_match_urls_with_user() -> Result { +fn user_rules_only_match_urls_with_user() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?.set("SSH_ASKPASS", "foo"); baseline::agrees_with("https://user@example.com/with-user"); baseline::agrees_with("https://example.com/with-user"); @@ -241,7 +241,7 @@ fn invalid_urls_are_rejected_early() { } #[test] -fn empty_core_askpass_is_ignored() -> Result { +fn empty_core_askpass_is_ignored() -> TestResult { for strict in [false, true] { let repo = gix::open_opts( remote::repo_path("empty-core-askpass"), @@ -256,7 +256,7 @@ fn empty_core_askpass_is_ignored() -> Result { } #[test] -fn core_askpass_interpolation_errors_are_not_ignored() -> Result { +fn core_askpass_interpolation_errors_are_not_ignored() -> TestResult { let mut error_snapshots = Vec::new(); for strict in [false, true] { let repo = gix::open_opts( diff --git a/gix/tests/gix/repository/config/config_snapshot/mod.rs b/gix/tests/gix/repository/config/config_snapshot/mod.rs index 79e74764117..845d4cae367 100644 --- a/gix/tests/gix/repository/config/config_snapshot/mod.rs +++ b/gix/tests/gix/repository/config/config_snapshot/mod.rs @@ -1,5 +1,5 @@ -use crate::Result; use gix::config::tree::{Branch, Core, Key, Pack, gitoxide}; +use gix_testtools::TestResult; use crate::{named_repo, repo_rw, repo_rw_opts}; @@ -7,7 +7,7 @@ use crate::{named_repo, repo_rw, repo_rw_opts}; mod credential_helpers; #[test] -fn commit_auto_rollback() -> Result { +fn commit_auto_rollback() -> TestResult { let mut repo = named_repo("make_basic_repo.sh")?; let default_abbrev = repo.head_id()?.to_string()[..7].to_owned(); let short_abbrev = repo.head_id()?.to_string()[..4].to_owned(); @@ -37,11 +37,11 @@ fn commit_auto_rollback() -> Result { } mod trusted_path { - use crate::Result; use crate::util::named_repo; + use gix_testtools::TestResult; #[test] - fn optional_is_respected() -> Result { + fn optional_is_respected() -> TestResult { let mut repo = named_repo("make_basic_repo.sh")?; repo.config_snapshot_mut().set_raw_value("my.path", "does-not-exist")?; @@ -61,7 +61,7 @@ mod trusted_path { } #[test] -fn snapshot_mut_commit_and_forget() -> Result { +fn snapshot_mut_commit_and_forget() -> TestResult { let mut repo = named_repo("make_basic_repo.sh")?; let repo = { let mut repo = repo.config_snapshot_mut(); @@ -79,7 +79,7 @@ fn snapshot_mut_commit_and_forget() -> Result { } #[test] -fn committing_loose_compression_requires_reopening_the_object_store() -> Result { +fn committing_loose_compression_requires_reopening_the_object_store() -> TestResult { use gix::objs::Write; fn loose_object_size(repo: &gix::Repository, id: gix::ObjectId) -> std::io::Result<u64> { @@ -118,7 +118,7 @@ fn committing_loose_compression_requires_reopening_the_object_store() -> Result } #[test] -fn compression_levels() -> Result { +fn compression_levels() -> TestResult { use gix::zlib::Compression; let mut repo = named_repo("make_basic_repo.sh")?; @@ -196,7 +196,7 @@ fn set_value_in_subsection() { } #[test] -fn apply_cli_overrides() -> Result { +fn apply_cli_overrides() -> TestResult { let mut repo = named_repo("make_config_repo.sh").unwrap(); repo.config_snapshot_mut().append_config( [ @@ -236,7 +236,7 @@ fn apply_cli_overrides() -> Result { } #[test] -fn reload_reloads_on_disk_changes() -> Result { +fn reload_reloads_on_disk_changes() -> TestResult { let (mut repo, _tmp) = repo_rw("make_config_repo.sh")?; assert_eq!(repo.config_snapshot().integer("core.abbrev"), None); let original_index = repo.index_path(); @@ -245,7 +245,7 @@ fn reload_reloads_on_disk_changes() -> Result { let config_path = repo.git_dir().join("config"); let mut config = gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local)?; config.set_raw_value("core.abbrev", "4")?; - config.set_raw_value("gitoxide.core.indexFile", gix_path::into_bstr(&changed_index).as_ref())?; + config.set_raw_value("gitoxide.core.indexFile", gix_path::into_bstr(&changed_index)?.as_ref())?; std::fs::write(config_path, config.to_bstring())?; assert_eq!(repo.config_snapshot().integer("core.abbrev"), None); @@ -263,7 +263,7 @@ fn reload_reloads_on_disk_changes() -> Result { } #[test] -fn reload_discards_in_memory_only_changes() -> Result { +fn reload_discards_in_memory_only_changes() -> TestResult { let mut repo = named_repo("make_config_repo.sh")?; repo.config_snapshot_mut().set_raw_value(Core::ABBREV, "4")?; @@ -275,7 +275,7 @@ fn reload_discards_in_memory_only_changes() -> Result { } #[test] -fn reload_rebuilds_includes_even_when_the_file_was_empty_or_missing() -> Result { +fn reload_rebuilds_includes_even_when_the_file_was_empty_or_missing() -> TestResult { let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options_with_includes())?; let included_path = repo.workdir().expect("worktree repository").join("a.config"); @@ -306,7 +306,7 @@ fn reload_rebuilds_includes_even_when_the_file_was_empty_or_missing() -> Result #[test] #[cfg(feature = "index")] -fn reload_preserves_the_reduced_trust_allocation_limit() -> Result { +fn reload_preserves_the_reduced_trust_allocation_limit() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_config_repo.sh")?; let mut repo = gix::open_opts( fixture.path(), @@ -326,7 +326,7 @@ fn reload_preserves_the_reduced_trust_allocation_limit() -> Result { #[test] #[serial_test::serial] -fn reload_reapplies_per_file_safe_directory_trust() -> Result { +fn reload_reapplies_per_file_safe_directory_trust() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let fixture = gix_testtools::scripted_fixture_writable("make_config_repo.sh")?; let included_path = fixture.path().join("a.config"); @@ -338,7 +338,7 @@ fn reload_reapplies_per_file_safe_directory_trust() -> Result { let mut global = gix_config::File::new(gix_config::file::Metadata::from(gix_config::Source::User)); global.set_raw_value( "safe.directory", - gix_path::into_bstr(&std::fs::canonicalize(&included_path)?).as_ref(), + gix_path::into_bstr(&std::fs::canonicalize(&included_path)?)?.as_ref(), )?; std::fs::write(&global_path, global.to_bstring())?; let _environment = _environment.set("GIT_CONFIG_GLOBAL", global_path.display().to_string()); @@ -353,13 +353,13 @@ fn reload_reapplies_per_file_safe_directory_trust() -> Result { .with(gix_sec::Trust::Reduced), )?; assert_eq!( - repo.config_snapshot().trusted_program(Core::SSH_COMMAND), + repo.config_snapshot().trusted_program(Core::SSH_COMMAND)?, Some("trusted-ssh".into()) ); repo.reload()?; assert_eq!( - repo.config_snapshot().trusted_program(Core::SSH_COMMAND), + repo.config_snapshot().trusted_program(Core::SSH_COMMAND)?, Some("trusted-ssh".into()), "reopening repeats per-file trust promotion" ); @@ -367,7 +367,7 @@ fn reload_reapplies_per_file_safe_directory_trust() -> Result { } #[test] -fn reload_resolves_onbranch_includes_from_unnamespaced_head() -> Result { +fn reload_resolves_onbranch_includes_from_unnamespaced_head() -> TestResult { use std::io::Write; let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options_with_includes())?; @@ -412,7 +412,7 @@ fn reload_resolves_onbranch_includes_from_unnamespaced_head() -> Result { #[test] #[cfg(all(feature = "sha1", feature = "sha256"))] -fn reload_rebuilds_object_stores_for_a_new_object_format() -> Result { +fn reload_rebuilds_object_stores_for_a_new_object_format() -> TestResult { let (mut repo, _tmp) = repo_rw("make_config_repo.sh")?; let new_object_hash = match repo.object_hash() { gix::hash::Kind::Sha1 => gix::hash::Kind::Sha256, @@ -432,7 +432,7 @@ fn reload_rebuilds_object_stores_for_a_new_object_format() -> Result { } #[test] -fn reload_keeps_opening_overrides_and_discards_runtime_edits() -> Result { +fn reload_keeps_opening_overrides_and_discards_runtime_edits() -> TestResult { let options = options_with_includes().config_overrides(["refresh.open=from-options"]); let (mut repo, _tmp) = repo_rw_opts("make_config_repo.sh", options)?; repo.config_snapshot_mut() diff --git a/gix/tests/gix/repository/config/editor.rs b/gix/tests/gix/repository/config/editor.rs index c399d4115c2..90dbec452ef 100644 --- a/gix/tests/gix/repository/config/editor.rs +++ b/gix/tests/gix/repository/config/editor.rs @@ -19,14 +19,14 @@ fn repository(overrides: impl IntoIterator<Item = impl Into<BString>>) -> gix_te #[test] #[serial] -fn follows_git_editor_precedence() -> gix_testtools::Result { +fn follows_git_editor_precedence() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("TERM", "xterm") .set("GIT_EDITOR", ":") .set("VISUAL", "visual") .set("EDITOR", "editor"); assert_eq!( - repository(["core.editor=core"])?.editor().as_deref(), + repository(["core.editor=core"])?.editor()?.as_deref(), Some(OsStr::new(":")), "the selected editor is available without preparing a command" ); @@ -81,7 +81,7 @@ fn follows_git_editor_precedence() -> gix_testtools::Result { #[test] #[serial] -fn dumb_terminals_require_an_explicit_non_visual_editor() -> gix_testtools::Result { +fn dumb_terminals_require_an_explicit_non_visual_editor() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("TERM", "dumb") .unset("GIT_EDITOR") @@ -101,7 +101,7 @@ fn dumb_terminals_require_an_explicit_non_visual_editor() -> gix_testtools::Resu #[test] #[serial] -fn generic_editor_environment_is_available_as_gitoxide_configuration() -> gix_testtools::Result { +fn generic_editor_environment_is_available_as_gitoxide_configuration() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()? .set("TERM", "dumb") .set("VISUAL", "visual-from-environment") diff --git a/gix/tests/gix/repository/config/identity.rs b/gix/tests/gix/repository/config/identity.rs index 99189e38694..63cb041aa96 100644 --- a/gix/tests/gix/repository/config/identity.rs +++ b/gix/tests/gix/repository/config/identity.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::path::Path; use gix_sec::Permission; @@ -8,7 +8,7 @@ use serial_test::serial; use crate::{named_repo, util::named_subrepo_opts}; #[test] -fn custom_committer_fallback_is_only_installed_if_needed() -> Result { +fn custom_committer_fallback_is_only_installed_if_needed() -> TestResult { let tmp = tempfile::tempdir()?; let repo = crate::init_repo_isolated(tmp.path(), gix::create::Kind::Bare)?; let git_dir = repo.git_dir().to_owned(); @@ -46,7 +46,7 @@ fn custom_committer_fallback_is_only_installed_if_needed() -> Result { } #[test] -fn configured_identity_fallbacks_follow_user_identity() -> Result { +fn configured_identity_fallbacks_follow_user_identity() -> TestResult { let tmp = tempfile::tempdir()?; let repo = crate::init_repo_isolated(tmp.path(), gix::create::Kind::Bare)?; let repo = gix::open_opts( @@ -87,7 +87,7 @@ fn configured_identity_fallbacks_follow_user_identity() -> Result { #[test] #[serial] -fn author_included_by_hasconfig() -> Result { +fn author_included_by_hasconfig() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repo = named_subrepo_opts("make_config_repos.sh", "with-hasconfig", gix::open::Options::isolated())?; let _environment = _environment @@ -108,7 +108,7 @@ fn author_included_by_hasconfig() -> Result { #[test] #[serial] -fn author_and_committer_and_fallback() -> Result { +fn author_and_committer_and_fallback() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; for trust in [gix_sec::Trust::Full, gix_sec::Trust::Reduced] { let repo = named_repo("make_config_repo.sh")?; @@ -177,23 +177,17 @@ fn author_and_committer_and_fallback() -> Result { let relative_path_key = "a.relative-path"; if trust == gix_sec::Trust::Full { assert_eq!( - config - .trusted_path(relative_path_key) - .expect("no error") - .expect("exists"), + config.trusted_path(relative_path_key)?.expect("exists"), Path::new("./something") ); assert_eq!( - config - .trusted_path("a.absolute-path") - .expect("no error") - .expect("exists"), + config.trusted_path("a.absolute-path")?.expect("exists"), Path::new("/etc/man.conf") ); assert!(config.trusted_path("a.bad-user-path").is_err()); } else { assert!( - config.trusted_path(relative_path_key).expect("no error").is_none(), + config.trusted_path(relative_path_key)?.is_none(), "trusted path at {relative_path_key} need full trust: {path:?}", path = config.string(relative_path_key) ); @@ -204,7 +198,7 @@ fn author_and_committer_and_fallback() -> Result { #[test] #[serial] -fn author_from_different_config_sections() -> Result { +fn author_from_different_config_sections() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repo = named_repo("make_signatures_repo.sh")?; let work_dir = repo.workdir().unwrap().canonicalize()?; diff --git a/gix/tests/gix/repository/config/mod.rs b/gix/tests/gix/repository/config/mod.rs index a23c484a3f7..e4039369c3b 100644 --- a/gix/tests/gix/repository/config/mod.rs +++ b/gix/tests/gix/repository/config/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; pub(crate) mod config_snapshot; #[cfg(feature = "command")] mod editor; @@ -6,7 +6,7 @@ mod identity; mod remote; #[test] -fn config_path_for_repository_sources_matches_git() -> Result { +fn config_path_for_repository_sources_matches_git() -> TestResult { use gix::config::Source; let fixture = gix_testtools::scripted_fixture_read_only("make_worktree_repo.sh")?; @@ -35,7 +35,7 @@ fn config_path_for_repository_sources_matches_git() -> Result { } #[test] -fn big_file_threshold() -> Result { +fn big_file_threshold() -> TestResult { let repo = repo("with-hasconfig"); assert_eq!( repo.big_file_threshold()?, @@ -50,7 +50,7 @@ fn big_file_threshold() -> Result { #[cfg(feature = "index")] #[test] -fn invalid_stat_boolean_is_validation_error() -> Result { +fn invalid_stat_boolean_is_validation_error() -> TestResult { let mut diagnostics = Vec::new(); for key in [ "core.trustCTime", @@ -98,13 +98,13 @@ fn invalid_stat_boolean_is_validation_error() -> Result { #[cfg(feature = "blocking-network-client")] mod ssh_options { - use crate::Result; + use gix_testtools::TestResult; use std::ffi::OsStr; use crate::repository::config::repo; #[test] - fn with_command_and_variant() -> Result { + fn with_command_and_variant() -> TestResult { let repo = repo("ssh-all-options"); let opts = repo.ssh_connect_options()?; assert_eq!(opts.command.as_deref(), Some(OsStr::new("ssh -VVV"))); @@ -117,7 +117,7 @@ mod ssh_options { } #[test] - fn with_command_fallback_which_disallows_shell() -> Result { + fn with_command_fallback_which_disallows_shell() -> TestResult { let repo = repo("ssh-command-fallback"); let opts = repo.ssh_connect_options()?; assert_eq!(opts.command.as_deref(), Some(OsStr::new("ssh --fallback"))); diff --git a/gix/tests/gix/repository/config/remote.rs b/gix/tests/gix/repository/config/remote.rs index 792e1b00402..6426b95b7b4 100644 --- a/gix/tests/gix/repository/config/remote.rs +++ b/gix/tests/gix/repository/config/remote.rs @@ -52,19 +52,19 @@ fn remote_default_name() { } mod branch_remote { - use crate::Result; use gix::{config::tree::Push, remote}; + use gix_testtools::TestResult; use crate::util::named_subrepo_opts; mod name { - use crate::Result; use gix::remote; + use gix_testtools::TestResult; use crate::repository::config::remote::branch_remote::repo; #[test] - fn push() -> Result { + fn push() -> TestResult { { let repo = repo("push-remote")?; @@ -92,20 +92,18 @@ mod branch_remote { } #[test] - fn fetch() -> Result { + fn fetch() -> TestResult { let repo = repo("fetch")?; assert_eq!( repo.branch_remote_ref_name("refs/heads/main".try_into()?, remote::Direction::Fetch) - .expect("Remote Merge ref exists") - .expect("Remote Merge ref is valid") + .expect("Remote Merge ref exists")? .shorten(), "main" ); assert_eq!( repo.branch_remote_tracking_ref_name("refs/heads/main".try_into()?, remote::Direction::Fetch) - .expect("Remote Merge ref exists") - .expect("Remote Merge ref is valid"), + .expect("Remote Merge ref exists")?, "refs/remotes/remote_repo/main" ); let (upstream, remote_name) = repo @@ -134,8 +132,7 @@ mod branch_remote { assert_eq!( repo.branch_remote_ref_name("refs/heads/broken".try_into()?, remote::Direction::Fetch) - .expect("Remote Merge ref exists") - .expect("merge ref is turned into a full-name"), + .expect("Remote Merge ref exists")?, "refs/heads/not_a_valid_merge_ref", "short names are simply turned into branch names - this doesn't always work, but sometimes." ); @@ -151,8 +148,7 @@ mod branch_remote { } assert_eq!( repo.branch_remote_tracking_ref_name("refs/heads/broken".try_into()?, remote::Direction::Fetch) - .expect("no error") - .expect("valid result"), + .expect("no error")?, "refs/remotes/remote_repo/not_a_valid_merge_ref", "the merge ref is broken, but we turned it into a full ref name from which everything else was derived", ); @@ -161,7 +157,7 @@ mod branch_remote { } #[test] - fn upstream_branch_and_remote_name_for_tracking_branch() -> Result { + fn upstream_branch_and_remote_name_for_tracking_branch() -> TestResult { let mut repo = repo("multiple-remotes")?; for expected_remote_name in ["other", "with/two"] { let (upstream, remote) = repo @@ -222,7 +218,7 @@ mod branch_remote { } #[test] - fn push_default() -> Result { + fn push_default() -> TestResult { let repo = repo("fetch")?; assert_eq!( @@ -252,7 +248,7 @@ mod branch_remote { } #[test] - fn push_mapped() -> Result { + fn push_mapped() -> TestResult { let repo = repo("push-mapped")?; assert_eq!( @@ -309,7 +305,7 @@ mod branch_remote { } #[test] - fn push_missing() -> Result { + fn push_missing() -> TestResult { let repo = repo("push-missing")?; assert!( @@ -336,7 +332,7 @@ mod branch_remote { } #[test] - fn push_default_current() -> Result { + fn push_default_current() -> TestResult { let mut repo = repo("push-default-current")?; for same_name_default in ["current", "matching"] { diff --git a/gix/tests/gix/repository/excludes.rs b/gix/tests/gix/repository/excludes.rs index 39d50023524..5c7922f5c55 100644 --- a/gix/tests/gix/repository/excludes.rs +++ b/gix/tests/gix/repository/excludes.rs @@ -1,10 +1,10 @@ -use crate::Result; +use gix_testtools::TestResult; use gix_worktree::stack::state::ignore::Source; use crate::util::named_subrepo_opts; #[test] -fn empty_core_excludes() -> Result { +fn empty_core_excludes() -> TestResult { let mut error_snapshots = Vec::new(); let repo = named_subrepo_opts( "make_basic_repo.sh", @@ -22,8 +22,7 @@ fn empty_core_excludes() -> Result { } let repo = gix::open_opts(repo.git_dir(), repo.open_options().clone().strict_config(false))?; - repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped) - .expect("empty paths are now just skipped"); + repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped)?; insta::assert_debug_snapshot!(error_snapshots, "empty core excludes", @" [ The value for `core.excludesFile` could not be read from configuration @@ -36,7 +35,7 @@ fn empty_core_excludes() -> Result { } #[test] -fn missing_core_excludes_is_ignored() -> Result { +fn missing_core_excludes_is_ignored() -> TestResult { let mut repo = named_subrepo_opts( "make_basic_repo.sh", "empty-core-excludes", @@ -46,13 +45,12 @@ fn missing_core_excludes_is_ignored() -> Result { .set_value(&gix::config::tree::Core::EXCLUDES_FILE, "definitely-missing")?; let index = repo.index_or_empty()?; - repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped) - .expect("the call works as missing excludes files are ignored"); + repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped)?; Ok(()) } #[test] -fn worktree_info_exclude_from_common_dir() -> Result { +fn worktree_info_exclude_from_common_dir() -> TestResult { let repo = named_subrepo_opts( "make_worktree_repo_with_info_exclude.sh", "worktree", diff --git a/gix/tests/gix/repository/filter.rs b/gix/tests/gix/repository/filter.rs index 8d70ab9f863..543ee2d844a 100644 --- a/gix/tests/gix/repository/filter.rs +++ b/gix/tests/gix/repository/filter.rs @@ -1,10 +1,10 @@ -use crate::Result; +use gix_testtools::{Result, TestResult}; use std::path::Path; #[test] -fn pipeline_in_nonbare_repo_without_index() -> Result { +fn pipeline_in_nonbare_repo_without_index() -> TestResult { let repo = named_subrepo_opts("make_basic_repo.sh", "all-untracked", Default::default())?; - let _ = repo.filter_pipeline(None).expect("does not fail due to missing index"); + let _ = repo.filter_pipeline(None)?; Ok(()) } @@ -15,7 +15,7 @@ use super::blob_id; use crate::util::{named_repo, named_subrepo_opts}; #[test] -fn pipeline_in_repo_without_special_options() -> Result { +fn pipeline_in_repo_without_special_options() -> TestResult { let repo = named_repo("make_basic_repo.sh")?; let (mut pipe, index) = repo.filter_pipeline(None)?; @@ -34,7 +34,7 @@ fn pipeline_in_repo_without_special_options() -> Result { } #[test] -fn repo_local_filter_driver_configuration_overrides_global_configuration() -> Result { +fn repo_local_filter_driver_configuration_overrides_global_configuration() -> TestResult { let mut repo = named_repo("make_basic_repo.sh")?; repo.config_snapshot_mut() .append_config( @@ -60,7 +60,7 @@ fn repo_local_filter_driver_configuration_overrides_global_configuration() -> Re #[test] #[cfg(unix)] -fn pipeline_worktree_file_to_object() -> Result { +fn pipeline_worktree_file_to_object() -> TestResult { let repo = named_repo("repo_with_untracked_files.sh")?; let work_dir = repo.workdir().expect("non-bare"); let (mut pipe, index) = repo.filter_pipeline(None)?; @@ -121,7 +121,7 @@ fn pipeline_worktree_file_to_object() -> Result { #[test] #[cfg(unix)] -fn worktree_file_to_object_opens_submodules_after_path_options_were_consumed() -> Result { +fn worktree_file_to_object_opens_submodules_after_path_options_were_consumed() -> TestResult { let repo = named_repo("repo_with_untracked_files.sh")?; let submodule = gix::open_opts( repo.workdir().expect("non-bare").join("submodule"), @@ -163,7 +163,7 @@ fn worktree_file_to_object_opens_submodules_after_path_options_were_consumed() - } #[test] -fn pipeline_with_autocrlf() -> Result { +fn pipeline_with_autocrlf() -> TestResult { let repo = named_repo("make_config_repo.sh")?; let (mut pipe, index) = repo.filter_pipeline(None)?; diff --git a/gix/tests/gix/repository/mailmap.rs b/gix/tests/gix/repository/mailmap.rs index 697c4f1684c..29fa4a0252e 100644 --- a/gix/tests/gix/repository/mailmap.rs +++ b/gix/tests/gix/repository/mailmap.rs @@ -1,7 +1,7 @@ -use crate::{Result, named_repo}; +use crate::named_repo; #[test] -fn empty_when_no_mailmap_present() -> Result { +fn empty_when_no_mailmap_present() -> gix_testtools::TestResult { let repo = named_repo("make_basic_repo.sh")?; let snapshot = repo.open_mailmap(); assert!( @@ -20,7 +20,7 @@ fn empty_when_no_mailmap_present() -> Result { #[test] #[cfg(any(unix, windows))] -fn worktree_symlinks_are_rejected_but_configured_mailmaps_may_follow_them() -> Result { +fn worktree_symlinks_are_rejected_but_configured_mailmaps_may_follow_them() -> gix_testtools::TestResult { let dir = gix_testtools::scripted_fixture_read_only("make_symlinked_mailmap_repo.sh")?; if !gix_testtools::fixture_has_symlinks(&dir)? { return Ok(()); @@ -48,7 +48,7 @@ fn worktree_symlinks_are_rejected_but_configured_mailmaps_may_follow_them() -> R ); repo.config_snapshot_mut() - .set_raw_value("mailmap.file", gix::path::into_bstr(mailmap.as_path()))?; + .set_raw_value("mailmap.file", gix::path::into_bstr(mailmap.as_path())?)?; assert!( repo.open_mailmap_into(&mut snapshot).is_err(), "the worktree symlink error is still reported while other sources are loaded" @@ -62,7 +62,7 @@ fn worktree_symlinks_are_rejected_but_configured_mailmaps_may_follow_them() -> R } #[test] -fn reads_existing_mailmap_from_worktree_root() -> Result { +fn reads_existing_mailmap_from_worktree_root() -> gix_testtools::TestResult { let repo = named_repo("make_mailmap_repo.sh")?; let snapshot = repo.open_mailmap(); assert_eq!( diff --git a/gix/tests/gix/repository/merge.rs b/gix/tests/gix/repository/merge.rs index 8b7fd63b414..fa1674efebd 100644 --- a/gix/tests/gix/repository/merge.rs +++ b/gix/tests/gix/repository/merge.rs @@ -1,8 +1,8 @@ -use crate::Result; use crate::util::named_repo; +use gix_testtools::TestResult; #[test] -fn tree_merge_options() -> Result { +fn tree_merge_options() -> TestResult { let repo = named_repo("make_basic_repo.sh")?; let opts: gix::merge::plumbing::tree::Options = repo.tree_merge_options()?.into(); assert_eq!( diff --git a/gix/tests/gix/repository/mod.rs b/gix/tests/gix/repository/mod.rs index ce49bee605f..aa0a0a10bc4 100644 --- a/gix/tests/gix/repository/mod.rs +++ b/gix/tests/gix/repository/mod.rs @@ -1,6 +1,6 @@ -#[cfg(feature = "parallel")] -use crate::Result; use gix::Repository; +#[cfg(feature = "parallel")] +use gix_testtools::TestResult; fn blob_id(repo: &Repository, data: &[u8]) -> gix_hash::ObjectId { gix_object::compute_hash(repo.object_hash(), gix_object::Kind::Blob, data).expect("valid object hash") @@ -37,7 +37,7 @@ mod format_version { use gix_error::MetadataValue; #[test] - fn upgrade_to_v1_accepts_grandfathered_extensions() -> crate::Result { + fn upgrade_to_v1_accepts_grandfathered_extensions() -> gix_testtools::TestResult { for input in [ "", "[core]\nrepositoryFormatVersion = 0\n", @@ -51,7 +51,7 @@ mod format_version { } #[test] - fn upgrade_to_v1_rejects_unknown_and_v1_only_extensions() -> crate::Result { + fn upgrade_to_v1_rejects_unknown_and_v1_only_extensions() -> gix_testtools::TestResult { for name in ["futureExtension", "objectFormat", "relativeWorktrees", "refStorage"] { let input = format!("[extensions]\nnoop = true\n[extensions]\n{name} = false\n"); let config = gix_config::File::try_from(input.as_str())?; @@ -73,7 +73,7 @@ mod format_version { } #[test] - fn upgrade_to_v1_rejects_extension_subsections() -> crate::Result { + fn upgrade_to_v1_rejects_extension_subsections() -> gix_testtools::TestResult { for subsection in ["", "scope"] { let input = format!("[extensions \"{subsection}\"]\nnoop = true\n"); let config = gix_config::File::try_from(input.as_str())?; @@ -97,7 +97,7 @@ mod format_version { } #[test] - fn already_v1_does_not_repeat_upgrade_validation() -> crate::Result { + fn already_v1_does_not_repeat_upgrade_validation() -> gix_testtools::TestResult { let config = gix_config::File::try_from( "[extensions]\nobjectFormat = sha256\nrelativeWorktrees = true\nfutureExtension = true\n\ [extensions \"scope\"]\nnoop = true\n", @@ -112,10 +112,10 @@ mod revision; #[cfg(feature = "index")] mod index { - use crate::Result; + use gix_testtools::TestResult; #[test] - fn missing_shared_index_is_an_error() -> Result { + fn missing_shared_index_is_an_error() -> TestResult { let (repo, _tmp) = crate::basic_rw_repo()?; let workdir = repo.workdir().expect("the fixture has a worktree"); gix_testtools::git(workdir, "update-index --split-index")?; @@ -145,7 +145,7 @@ mod index { } #[test] - fn basics() -> Result { + fn basics() -> TestResult { let repo = crate::named_subrepo_opts("make_basic_repo.sh", "unborn", gix::open::Options::isolated())?; let err = repo.index().expect_err("the fixture has no index"); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&repo.git_dir().to_string_lossy(), "<git-dir>")]), "a missing index has standard not-found classification", @r#" @@ -182,13 +182,13 @@ mod index { #[cfg(feature = "dirwalk")] mod dirwalk { - use crate::Result; + use gix_testtools::TestResult; use std::sync::atomic::AtomicBool; use gix_dir::{entry::Kind::*, walk::EmissionMode}; #[test] - fn basics() -> Result { + fn basics() -> TestResult { let repo = crate::named_repo("make_basic_repo.sh")?; let untracked_only = repo.dirwalk_options()?.emit_untracked(EmissionMode::CollapseDirectory); let mut collect = gix::dir::walk::delegate::Collect::default(); @@ -260,7 +260,7 @@ fn size_in_memory() { #[test] #[cfg(feature = "parallel")] -fn thread_safe_repository_is_sync() -> Result { +fn thread_safe_repository_is_sync() -> TestResult { fn f<T: Send + Sync + Clone>(_t: T) {} f(crate::util::basic_repo()?.into_sync()); Ok(()) @@ -268,7 +268,7 @@ fn thread_safe_repository_is_sync() -> Result { #[test] #[cfg(feature = "parallel")] -fn repository_is_send() -> Result { +fn repository_is_send() -> TestResult { fn f<T: Send + Clone>(_t: T) {} f(crate::util::basic_repo()?); Ok(()) diff --git a/gix/tests/gix/repository/note.rs b/gix/tests/gix/repository/note.rs index 1d9af0081e7..6c506ad8ef4 100644 --- a/gix/tests/gix/repository/note.rs +++ b/gix/tests/gix/repository/note.rs @@ -1,6 +1,6 @@ -use crate::Result; +use gix_testtools::{Result, TestResult}; #[test] -fn query_and_mutate_a_configured_notes_ref() -> Result { +fn query_and_mutate_a_configured_notes_ref() -> TestResult { let (mut repo, _tmp) = crate::util::basic_rw_repo()?; let mut config = repo.config_snapshot_mut(); config.set_value(&gix::config::tree::Core::NOTES_REF, "refs/notes/review")?; @@ -64,7 +64,7 @@ fn query_and_mutate_a_configured_notes_ref() -> Result { } #[test] -fn mutations_follow_symbolic_references_to_their_direct_target() -> Result { +fn mutations_follow_symbolic_references_to_their_direct_target() -> TestResult { use gix::refs::{ FullName, Target, TargetRef, transaction::{PreviousValue, RefEdit}, @@ -175,7 +175,7 @@ fn mutations_follow_symbolic_references_to_their_direct_target() -> Result { } #[test] -fn mutations_reject_non_commit_notes_ref_targets() -> Result { +fn mutations_reject_non_commit_notes_ref_targets() -> TestResult { use gix::refs::transaction::PreviousValue; let (repo, _tmp) = crate::util::basic_rw_repo()?; @@ -231,7 +231,7 @@ fn mutations_reject_non_commit_notes_ref_targets() -> Result { } #[test] -fn custom_commit_message_is_used_for_mutations() -> Result { +fn custom_commit_message_is_used_for_mutations() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let annotated_blob_id = repo.write_blob(b"annotated")?; let mut notes = repo.notes()?.with_commit_message("custom notes update"); @@ -257,7 +257,7 @@ fn custom_commit_message_is_used_for_mutations() -> Result { } #[test] -fn query_and_mutate_multiple_notes_refs() -> Result { +fn query_and_mutate_multiple_notes_refs() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let target = repo.write_blob(b"annotated")?; let notes_refs = ["refs/notes/review", "refs/notes/security"]; @@ -326,7 +326,7 @@ fn query_and_mutate_multiple_notes_refs() -> Result { } #[test] -fn add_to_an_exact_fully_qualified_reference() -> Result { +fn add_to_an_exact_fully_qualified_reference() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let target = repo.write_blob(b"annotated")?.detach(); let reference: gix::refs::FullName = "refs/worktree/tix/notes".try_into()?; diff --git a/gix/tests/gix/repository/object.rs b/gix/tests/gix/repository/object.rs index b8f201c6340..aaa9c7c9264 100644 --- a/gix/tests/gix/repository/object.rs +++ b/gix/tests/gix/repository/object.rs @@ -1,17 +1,17 @@ -use crate::Result; use gix_date::parse::TimeBuf; use gix_odb::Header; use gix_pack::Find; use gix_testtools::tempfile; +use gix_testtools::{Result, TestResult}; use crate::util::named_subrepo_opts; mod object_database_impl { - use crate::Result; use gix_object::{Exists, Find, FindHeader}; + use gix_testtools::TestResult; #[test] - fn empty_tree_is_always_present() -> Result { + fn empty_tree_is_always_present() -> TestResult { let repo = crate::named_subrepo_opts("make_basic_repo.sh", "unborn", gix::open::Options::isolated())?; let empty_tree = gix::ObjectId::empty_tree(repo.object_hash()); assert!(repo.exists(&empty_tree)); @@ -34,16 +34,16 @@ mod object_database_impl { } mod edit_tree { - use crate::Result; use gix::bstr::{BStr, BString}; use gix_object::tree::EntryKind; + use gix_testtools::TestResult; use crate::util::hex_to_id; #[test] // Some part of the test validation the implementation for this exists, but it's needless nonetheless. #[expect(clippy::needless_borrows_for_generic_args)] - fn from_head_tree() -> Result { + fn from_head_tree() -> TestResult { let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; let head_tree_id = repo.head_tree_id()?; assert_eq!( @@ -153,7 +153,7 @@ mod edit_tree { } #[test] - fn submodules_are_not_checked_for_existence() -> Result { + fn submodules_are_not_checked_for_existence() -> TestResult { let repo = crate::named_subrepo_opts("make_submodules.sh", "with-submodules", gix::open::Options::isolated())? .with_object_memory(); let mut editor = repo.head_tree()?.edit()?; @@ -167,7 +167,7 @@ mod edit_tree { } #[test] - fn remove_leaf_rejects_tree_entries() -> Result { + fn remove_leaf_rejects_tree_entries() -> TestResult { let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; let head_tree_id = repo.head_tree_id()?; let this_id = hex_to_id("317e9677c3bcffd006f9fc84bbb0a54ef1676197"); @@ -190,7 +190,7 @@ mod edit_tree { } #[test] - fn missing_objects_and_illformed_path_components_trigger_error() -> Result { + fn missing_objects_and_illformed_path_components_trigger_error() -> TestResult { let (repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; let tree = repo.head_tree_id()?.object()?.into_tree(); let mut editor = tree.edit()?; @@ -208,7 +208,7 @@ mod edit_tree { .upsert( "non-existing", EntryKind::Blob, - gix_hash::ObjectId::from_hex(missing_id.as_bytes()).expect("valid object id"), + gix_hash::ObjectId::from_hex(missing_id.as_bytes())?, )? .write() .unwrap_err(); @@ -282,11 +282,11 @@ mod edit_tree { } } mod write_object { - use crate::Result; use crate::repository::object::empty_bare_in_memory_repo; + use gix_testtools::TestResult; #[test] - fn empty_tree() -> Result { + fn empty_tree() -> TestResult { let repo = empty_bare_in_memory_repo()?; let oid = repo.write_object(gix::objs::TreeRef::empty())?; assert_eq!( @@ -298,7 +298,7 @@ mod write_object { } #[test] - fn commit_with_invalid_author() -> Result { + fn commit_with_invalid_author() -> TestResult { let repo = empty_bare_in_memory_repo()?; let actor = gix::actor::Signature { name: "1 < 0".into(), @@ -324,7 +324,7 @@ mod write_object { } #[test] - fn blob_write_to_implementation() -> Result { + fn blob_write_to_implementation() -> TestResult { let repo = empty_bare_in_memory_repo()?; let blob = repo.empty_blob(); @@ -340,7 +340,7 @@ mod write_object { } mod write_blob { - use crate::Result; + use gix_testtools::TestResult; use std::io::{Seek, SeekFrom}; use crate::repository::{ @@ -349,7 +349,7 @@ mod write_blob { }; #[test] - fn from_slice() -> Result { + fn from_slice() -> TestResult { let (_tmp, repo) = empty_bare_repo()?; let expected = blob_id(&repo, b"hello world"); assert!(!repo.has_object(expected)); @@ -373,7 +373,7 @@ mod write_blob { } #[test] - fn from_stream() -> Result { + fn from_stream() -> TestResult { let repo = empty_bare_in_memory_repo()?; let mut cursor = std::io::Cursor::new(b"hello world"); let mut seek_cursor = cursor.clone(); @@ -397,7 +397,7 @@ mod write_blob { } #[test] -fn writes_avoid_io_using_duplicate_check() -> Result { +fn writes_avoid_io_using_duplicate_check() -> TestResult { let (mut repo, _tmp) = crate::repo_rw("make_packed_and_loose.sh")?; let store = gix::odb::loose::Store::at(repo.git_dir().join("objects"), repo.object_hash()); let loose_count = store.iter().count(); @@ -471,14 +471,14 @@ fn writes_avoid_io_using_duplicate_check() -> Result { } mod find { - use crate::Result; use gix_pack::Find; + use gix_testtools::TestResult; use crate::basic_repo; use crate::repository::object::empty_bare_in_memory_repo; #[test] - fn find_and_try_find_with_and_without_object_cache() -> Result { + fn find_and_try_find_with_and_without_object_cache() -> TestResult { let mut repo = basic_repo()?; assert_eq!( @@ -517,7 +517,7 @@ mod find { } #[test] - fn empty_tree_can_always_be_found() -> Result { + fn empty_tree_can_always_be_found() -> TestResult { let repo = basic_repo()?; let empty_tree = gix::hash::ObjectId::empty_tree(repo.object_hash()); assert_eq!(repo.find_object(empty_tree)?.into_tree().iter().count(), 0); @@ -556,7 +556,7 @@ mod find { } #[test] - fn empty_blob_can_be_found_if_it_exists() -> Result { + fn empty_blob_can_be_found_if_it_exists() -> TestResult { let repo = basic_repo()?; let empty_blob = gix::hash::ObjectId::empty_blob(repo.object_hash()); @@ -594,7 +594,7 @@ mod find { } #[test] - fn empty_blob() -> Result { + fn empty_blob() -> TestResult { let repo = empty_bare_in_memory_repo()?; let empty_blob = repo.empty_blob(); @@ -610,7 +610,7 @@ mod find { } #[test] -fn empty_objects_are_always_present_but_not_in_plumbing() -> Result { +fn empty_objects_are_always_present_but_not_in_plumbing() -> TestResult { let repo = empty_bare_in_memory_repo()?; let empty_blob_id = repo.object_hash().empty_blob(); @@ -640,10 +640,10 @@ fn empty_objects_are_always_present_but_not_in_plumbing() -> Result { } mod tag { - use crate::Result; + use gix_testtools::TestResult; #[test] - fn simple() -> Result { + fn simple() -> TestResult { let (repo, _keep) = crate::repo_rw("make_basic_repo.sh")?; let current_head_id = repo.head_id()?; let message = "a multi\nline message"; @@ -672,12 +672,12 @@ mod tag { } mod commit_as { - use crate::Result; use gix_date::parse::TimeBuf; + use gix_testtools::TestResult; use gix_testtools::tempfile; #[test] - fn specify_committer_and_author() -> Result { + fn specify_committer_and_author() -> TestResult { let tmp = tempfile::tempdir()?; let repo = crate::init_repo_isolated(&tmp, gix::create::Kind::WithWorktree)?.to_thread_local(); let empty_tree = repo.empty_tree(); @@ -710,7 +710,7 @@ mod commit_as { } mod commit { - use crate::Result; + use gix_testtools::TestResult; use gix_testtools::tempfile; use crate::{freeze_time, restricted_and_git, util::hex_to_id}; @@ -731,7 +731,7 @@ mod commit { } #[test] - fn parent_in_initial_commit_causes_failure() -> Result { + fn parent_in_initial_commit_causes_failure() -> TestResult { let tmp = tempfile::tempdir()?; let repo = gix::ThreadSafeRepository::init_opts( &tmp, @@ -761,7 +761,7 @@ mod commit { #[test] #[serial_test::serial] - fn single_line_initial_commit_empty_tree_ref_nonexisting() -> Result { + fn single_line_initial_commit_empty_tree_ref_nonexisting() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let _env = freeze_time(); let tmp = tempfile::tempdir()?; @@ -800,19 +800,14 @@ mod commit { #[test] #[serial_test::serial] - fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> Result { + fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let _env = freeze_time(); let (repo, _keep) = crate::repo_rw_opts("make_basic_repo.sh", restricted_and_git())?; let parent = repo.find_reference("HEAD")?.peel_to_id()?; - let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set"); + let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id()?; assert_eq!( - parent - .try_object()? - .expect("present") - .to_commit_ref_iter() - .tree_id() - .expect("tree to be set"), + parent.try_object()?.expect("present").to_commit_ref_iter().tree_id()?, empty_tree_id, "try and non-try work the same" ); @@ -873,7 +868,7 @@ mod commit { } #[test] -fn new_commit_as() -> Result { +fn new_commit_as() -> TestResult { let repo = empty_bare_in_memory_repo()?; let empty_tree = repo.empty_tree(); let committer = gix::actor::Signature { @@ -920,7 +915,7 @@ fn new_commit_as() -> Result { } #[test] -fn new_commit() -> Result { +fn new_commit() -> TestResult { let mut repo = empty_bare_in_memory_repo()?; let mut config = repo.config_snapshot_mut(); config.set_value(&gix::config::tree::User::NAME, "user")?; diff --git a/gix/tests/gix/repository/open.rs b/gix/tests/gix/repository/open.rs index e035a48f2e7..5a1c1f66647 100644 --- a/gix/tests/gix/repository/open.rs +++ b/gix/tests/gix/repository/open.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::TestResult; use std::borrow::Cow; use gix::bstr::BString; @@ -14,7 +14,7 @@ fn open_permissions_is_isolated() { #[test] #[serial_test::serial] -fn discover_with_git_dir_environment_override_uses_it_and_sets_trust() -> Result { +fn discover_with_git_dir_environment_override_uses_it_and_sets_trust() -> TestResult { let _environment = gix_testtools::isolate_git_environment()?; let fallback = gix_testtools::tempfile::TempDir::new()?; crate::init_repo_isolated(fallback.path(), gix::create::Kind::WithWorktree)?; @@ -42,7 +42,7 @@ fn discover_with_git_dir_environment_override_uses_it_and_sets_trust() -> Result } #[test] -fn core_worktree_paths_are_literal() -> Result { +fn core_worktree_paths_are_literal() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_literal_worktree_paths.sh")?; let baseline = std::fs::read_to_string(fixture.join("worktrees.baseline"))?; for line in baseline.lines() { @@ -67,7 +67,7 @@ fn core_worktree_paths_are_literal() -> Result { } #[test] -fn core_worktree_cli_override_does_not_override_bare() -> Result { +fn core_worktree_cli_override_does_not_override_bare() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?; let worktree = gix_testtools::tempfile::TempDir::new()?; let repo = gix::open_opts( @@ -85,7 +85,7 @@ fn core_worktree_cli_override_does_not_override_bare() -> Result { } #[test] -fn on_root_with_decomposed_unicode() -> Result { +fn on_root_with_decomposed_unicode() -> TestResult { let tmp = gix_testtools::tempfile::TempDir::new()?; let decomposed = "a\u{308}"; @@ -134,7 +134,7 @@ fn on_root_with_decomposed_unicode() -> Result { } #[test] -fn non_bare_reftable() -> Result { +fn non_bare_reftable() -> TestResult { let Some(root) = gix_testtools::scripted_fixture_read_only_with_git_version("make_reftable_repo.sh", |version| { version >= (2, 44, 0) })? @@ -161,7 +161,7 @@ fn non_bare_reftable() -> Result { } #[test] -fn bare_repo_with_index() -> Result { +fn bare_repo_with_index() -> TestResult { let repo = named_subrepo_opts( "make_basic_repo.sh", "bare-repo-with-index.git", @@ -177,7 +177,7 @@ fn bare_repo_with_index() -> Result { } #[test] -fn git_index_file_overrides_the_index_in_the_git_dir() -> Result { +fn git_index_file_overrides_the_index_in_the_git_dir() -> TestResult { let repository = gix_testtools::scripted_fixture_writable("make_basic_repo.sh")?; let index_file = repository.path().join(".git/temporary-index"); let repo = gix::open_opts(repository.path(), gix::open::Options::isolated())?; @@ -227,7 +227,7 @@ fn git_index_file_overrides_the_index_in_the_git_dir() -> Result { #[test] #[cfg(feature = "index")] -fn git_index_file_missing_yields_no_index() -> Result { +fn git_index_file_missing_yields_no_index() -> TestResult { let repository = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?; let index_file = repository.join(".git/missing"); let repo = gix::open_opts( @@ -248,7 +248,7 @@ fn git_index_file_missing_yields_no_index() -> Result { } #[test] -fn git_index_file_empty_is_invalid_even_with_lenient_config() -> Result { +fn git_index_file_empty_is_invalid_even_with_lenient_config() -> TestResult { assert!( gix::config::tree::gitoxide::Core::INDEX_FILE .validated_assignment("".into()) @@ -273,7 +273,7 @@ fn git_index_file_empty_is_invalid_even_with_lenient_config() -> Result { #[test] #[cfg(feature = "index")] -fn git_index_file_receives_writes_while_the_git_dir_index_is_locked() -> Result { +fn git_index_file_receives_writes_while_the_git_dir_index_is_locked() -> TestResult { let repository = gix_testtools::scripted_fixture_writable("make_basic_repo.sh")?; let index_file = repository.path().join(".git/temporary-index"); let repo = gix::open_opts( @@ -299,7 +299,7 @@ fn git_index_file_receives_writes_while_the_git_dir_index_is_locked() -> Result } #[test] -fn non_bare_repo_with_git_extension() -> Result { +fn non_bare_repo_with_git_extension() -> TestResult { let repo = named_subrepo_opts("make_basic_repo.sh", "repo.git", gix::open::Options::isolated())?; assert_eq!(repo.kind(), gix::repository::Kind::Common); assert!(!repo.is_bare()); @@ -317,7 +317,7 @@ fn non_bare_repo_with_git_extension() -> Result { } #[test] -fn non_bare_turned_bare() -> Result { +fn non_bare_turned_bare() -> TestResult { let repo = named_subrepo_opts( "make_worktree_repo.sh", "non-bare-turned-bare", @@ -333,7 +333,7 @@ fn non_bare_turned_bare() -> Result { } #[test] -fn worktree_of_bare_repo() -> Result { +fn worktree_of_bare_repo() -> TestResult { let repo = named_subrepo_opts( "make_worktree_repo.sh", "worktree-of-bare-repo", @@ -360,7 +360,7 @@ fn worktree_of_bare_repo() -> Result { } #[test] -fn worktree_of_natively_bare_repo() -> Result { +fn worktree_of_natively_bare_repo() -> TestResult { let repo = named_subrepo_opts( "make_worktree_repo.sh", "worktree-of-natively-bare-repo", @@ -387,7 +387,7 @@ fn worktree_of_natively_bare_repo() -> Result { } #[test] -fn natively_bare_repo_itself_is_common() -> Result { +fn natively_bare_repo_itself_is_common() -> TestResult { let repo = named_subrepo_opts( "make_worktree_repo.sh", "natively-bare-repo", @@ -405,7 +405,7 @@ fn natively_bare_repo_itself_is_common() -> Result { } #[test] -fn non_bare_non_git_repo_without_worktree() -> Result { +fn non_bare_non_git_repo_without_worktree() -> TestResult { let repo = named_subrepo_opts( "make_basic_repo.sh", "non-bare-without-worktree", @@ -430,7 +430,7 @@ fn non_bare_non_git_repo_without_worktree() -> Result { } #[test] -fn none_bare_repo_without_index() -> Result { +fn none_bare_repo_without_index() -> TestResult { let mut repo = named_subrepo_opts( "make_basic_repo.sh", "non-bare-repo-without-index", @@ -454,7 +454,7 @@ fn none_bare_repo_without_index() -> Result { "this is a minimal path" ); - let old = repo.set_workdir(None).expect("should never fail"); + let old = repo.set_workdir(None)?; assert_eq!( old.as_ref().and_then(|wd| wd.file_name()?.to_str()), Some("non-bare-repo-without-index") @@ -467,11 +467,7 @@ fn none_bare_repo_without_index() -> Result { ); assert_eq!(repo.kind(), gix::repository::Kind::Common); - assert_eq!( - repo.set_workdir(old.clone()).expect("does not fail as it exists"), - None, - "nothing was set before" - ); + assert_eq!(repo.set_workdir(old.clone())?, None, "nothing was set before"); assert_eq!(repo.workdir(), old.as_deref()); let worktree = repo.worktree().expect("should be present after setting"); @@ -480,7 +476,7 @@ fn none_bare_repo_without_index() -> Result { } #[test] -fn non_bare_split_worktree() -> Result { +fn non_bare_split_worktree() -> TestResult { for (name, worktree_exists) in [ ("repo-with-worktree-in-config-unborn-no-worktreedir", false), ("repo-with-worktree-in-config-unborn", true), @@ -502,7 +498,7 @@ fn non_bare_split_worktree() -> Result { } #[test] -fn non_bare_split_worktree_invalid_worktree_path_boolean() -> Result { +fn non_bare_split_worktree_invalid_worktree_path_boolean() -> TestResult { let err = named_subrepo_opts( "make_worktree_repo.sh", "repo-with-worktree-in-config-unborn-worktreedir-missing-value", @@ -515,7 +511,7 @@ fn non_bare_split_worktree_invalid_worktree_path_boolean() -> Result { } #[test] -fn non_bare_split_worktree_invalid_worktree_path_empty() -> Result { +fn non_bare_split_worktree_invalid_worktree_path_empty() -> TestResult { // "repo-with-worktree-in-config-unborn-worktreedir-missing-value", let err = named_subrepo_opts( "make_worktree_repo.sh", @@ -549,7 +545,7 @@ fn non_bare_split_worktree_invalid_worktree_path_empty() -> Result { } #[test] -fn bare_with_worktree_is_still_bare() -> Result { +fn bare_with_worktree_is_still_bare() -> TestResult { let repo = named_subrepo_opts("make_config_repos.sh", "bare-link", gix::open::Options::isolated())?; assert!( repo.is_bare(), @@ -567,11 +563,11 @@ fn bare_with_worktree_is_still_bare() -> Result { mod missing_config_file { - use crate::Result; use crate::util::named_subrepo_opts; + use gix_testtools::TestResult; #[test] - fn bare() -> Result { + fn bare() -> TestResult { let repo = named_subrepo_opts("make_config_repos.sh", "bare-no-config", gix::open::Options::isolated())?; assert!( repo.is_bare(), @@ -589,7 +585,7 @@ mod missing_config_file { } #[test] - fn non_bare() -> Result { + fn non_bare() -> TestResult { let repo = named_subrepo_opts( "make_config_repos.sh", "worktree-no-config", @@ -612,10 +608,10 @@ mod missing_config_file { } mod not_a_repository { - use crate::Result; + use gix_testtools::TestResult; #[test] - fn shows_proper_error() -> Result { + fn shows_proper_error() -> TestResult { let mut error_snapshots = Vec::new(); for name in ["empty-dir", "with-files"] { let name = format!("not-a-repo-{name}"); @@ -645,7 +641,7 @@ mod not_a_repository { mod relative_worktrees_extension { #[test] - fn requires_v1_and_a_valid_boolean() -> crate::Result { + fn requires_v1_and_a_valid_boolean() -> gix_testtools::TestResult { for name in [ "relative-worktrees-false-with-repository-format-v0", "relative-worktrees-true-with-repository-format-v0", @@ -660,11 +656,11 @@ mod relative_worktrees_extension { } mod object_format_extension { - use crate::Result; use crate::util::named_subrepo_opts; + use gix_testtools::TestResult; #[test] - fn rejects_object_format_on_v0_repo() -> Result { + fn rejects_object_format_on_v0_repo() -> TestResult { let mut error_snapshots = Vec::new(); // objectFormat is a "v1-only" extension: git refuses to operate on a version-0 repo that // sets it, even for sha1 (unlike grandfathered extensions like preciousObjects, which v0 @@ -697,7 +693,7 @@ mod object_format_extension { } #[test] - fn rejects_future_repository_format_versions() -> Result { + fn rejects_future_repository_format_versions() -> TestResult { let err = named_subrepo_opts( "make_config_repos.sh", "repository-format-v2-with-objectformat-sha1", @@ -721,22 +717,22 @@ mod object_format_extension { mod open_path_as_is { - use crate::Result; use crate::util::{named_subrepo_opts, repo_opts}; use gix::error::Message; + use gix_testtools::TestResult; fn open_path_as_is() -> gix::open::Options { gix::open::Options::isolated().open_path_as_is(true) } #[test] - fn bare_repos_open_normally() -> Result { + fn bare_repos_open_normally() -> TestResult { assert!(named_subrepo_opts("make_basic_repo.sh", "bare.git", open_path_as_is())?.is_bare()); Ok(()) } #[test] - fn worktrees_cannot_be_opened() -> Result { + fn worktrees_cannot_be_opened() -> TestResult { let err = repo_opts("make_basic_repo.sh", open_path_as_is()).unwrap_err(); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&(err), &[(&(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?).to_string_lossy(), "<worktree>")]), "worktrees cannot be opened", @r#" "<worktree>" does not appear to be a git repository @@ -754,7 +750,7 @@ mod open_path_as_is { } #[test] - fn git_dir_within_worktrees_open_normally() -> Result { + fn git_dir_within_worktrees_open_normally() -> TestResult { assert!(!named_subrepo_opts("make_basic_repo.sh", ".git", open_path_as_is())?.is_bare()); Ok(()) } @@ -808,11 +804,11 @@ mod submodules { mod object_caches { - use crate::Result; use crate::util::named_subrepo_opts; + use gix_testtools::TestResult; #[test] - fn default_git_and_custom_caches() -> Result { + fn default_git_and_custom_caches() -> TestResult { let opts = gix::open::Options::isolated(); let repo = named_subrepo_opts("make_config_repos.sh", "object-caches", opts)?; assert_eq!( @@ -827,7 +823,7 @@ mod object_caches { } #[test] - fn disabled() -> Result { + fn disabled() -> TestResult { let opts = gix::open::Options::isolated(); let repo = named_subrepo_opts("make_config_repos.sh", "disabled-object-caches", opts)?; assert!(!repo.objects.has_object_cache()); @@ -837,15 +833,15 @@ mod object_caches { } mod pack_alloc_limit_bytes { - use crate::Result; use gix_odb::HeaderExt; use gix_odb::find::Header; use gix_sec::Trust; + use gix_testtools::TestResult; use crate::util::repo_opts; #[test] - fn limits_packed_object_allocations() -> Result { + fn limits_packed_object_allocations() -> TestResult { let repo = repo_opts("make_packed_and_loose.sh", crate::util::restricted())?.to_thread_local(); let packed_only_id = repo .objects @@ -873,7 +869,7 @@ mod pack_alloc_limit_bytes { } #[test] - fn limits_loose_object_allocations() -> Result { + fn limits_loose_object_allocations() -> TestResult { let repo = repo_opts("make_packed_and_loose.sh", crate::util::restricted())?.to_thread_local(); let loose_only_blob_id = repo .objects @@ -917,7 +913,7 @@ mod pack_alloc_limit_bytes { } #[test] - fn reduced_trust_sets_a_default_limit_unless_disabled() -> Result { + fn reduced_trust_sets_a_default_limit_unless_disabled() -> TestResult { let base = repo_opts("make_packed_and_loose.sh", crate::util::restricted())?.to_thread_local(); let packed_only_id = base .objects @@ -962,7 +958,7 @@ mod worktree { use gix::open; #[test] - fn with_worktree_configs() -> gix_testtools::Result { + fn with_worktree_configs() -> gix_testtools::TestResult { let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); let fixture_dir = gix_testtools::scripted_fixture_read_only("make_worktree_repo_with_configs.sh")?; let worktree_base = manifest_dir.join(&fixture_dir).join("repo/.git/worktrees"); diff --git a/gix/tests/gix/repository/pathspec.rs b/gix/tests/gix/repository/pathspec.rs index 0b2ff1c4cdf..0438a61dc00 100644 --- a/gix/tests/gix/repository/pathspec.rs +++ b/gix/tests/gix/repository/pathspec.rs @@ -1,11 +1,11 @@ -use crate::Result; use gix::{bstr::ByteSlice, config::tree::gitoxide}; +use gix_testtools::TestResult; use gix_worktree::stack::state::attributes::Source; use crate::util::named_repo; #[test] -fn defaults_are_taken_from_repo_config() -> Result { +fn defaults_are_taken_from_repo_config() -> TestResult { let mut repo = named_repo("make_basic_repo.sh")?; repo.config_snapshot_mut() .set_value(&gitoxide::Pathspec::ICASE, "true")?; @@ -18,7 +18,7 @@ fn defaults_are_taken_from_repo_config() -> Result { ":!hip", gix::path::to_unix_separators_on_windows(gix::path::into_bstr( repo.workdir().expect("present").join("for-normalization"), - )) + )?) .to_str_lossy() .as_ref(), ], diff --git a/gix/tests/gix/repository/reference.rs b/gix/tests/gix/repository/reference.rs index ea59aa9c6df..1b089a7dcdd 100644 --- a/gix/tests/gix/repository/reference.rs +++ b/gix/tests/gix/repository/reference.rs @@ -1,16 +1,16 @@ -use crate::Result; +use gix_testtools::TestResult; mod set_namespace { - use crate::Result; use gix::refs::transaction::PreviousValue; use gix_testtools::tempfile; + use gix_testtools::{Result, TestResult}; fn easy_repo_rw() -> Result<(gix::Repository, tempfile::TempDir)> { crate::repo_rw("make_references_repo.sh") } #[test] - fn affects_edits_and_iteration() -> Result { + fn affects_edits_and_iteration() -> TestResult { let (mut repo, _keep) = easy_repo_rw()?; assert_eq!( repo.references()?.all()?.count(), @@ -78,7 +78,7 @@ mod set_namespace { } #[test] -fn try_find_reference_with_existing_ref_as_path_prefix_returns_none() -> Result { +fn try_find_reference_with_existing_ref_as_path_prefix_returns_none() -> TestResult { let (repo, _tmp) = crate::repo_rw("make_references_repo.sh")?; std::fs::create_dir_all(repo.git_dir().join("refs/heads"))?; std::fs::write( @@ -94,15 +94,15 @@ fn try_find_reference_with_existing_ref_as_path_prefix_returns_none() -> Result } mod iter_references { - use crate::Result; use crate::util::hex_to_id; + use gix_testtools::{Result, TestResult}; fn repo() -> Result<gix::Repository> { crate::repo("make_references_repo.sh").map(|r| r.to_thread_local()) } #[test] - fn all() -> Result { + fn all() -> TestResult { let repo = repo()?; assert_eq!( repo.references()? @@ -132,7 +132,7 @@ mod iter_references { } #[test] - fn prefixed() -> Result { + fn prefixed() -> TestResult { let repo = repo()?; assert_eq!( repo.references()? @@ -163,7 +163,7 @@ mod iter_references { } #[test] - fn prefixed_and_peeled() -> Result { + fn prefixed_and_peeled() -> TestResult { let repo = repo()?; assert_eq!( repo.references()? @@ -200,7 +200,7 @@ mod iter_references { /// Regression test for https://github.com/GitoxideLabs/gitoxide/issues/2103 /// This only ensures we can return a reference, not that the code below is correct #[test] - fn tags() -> Result { + fn tags() -> TestResult { let repo = repo()?; let actual = repo .references()? @@ -215,13 +215,13 @@ mod iter_references { mod head { - use crate::Result; use gix_ref::transaction::PreviousValue; + use gix_testtools::TestResult; use crate::util::hex_to_id; #[test] - fn symbolic() -> Result { + fn symbolic() -> TestResult { let repo = crate::basic_repo()?; let head = repo.head()?; match &head.kind { @@ -239,7 +239,7 @@ mod head { } #[test] - fn detached() -> Result { + fn detached() -> TestResult { let (repo, _keep) = crate::basic_rw_repo()?; repo.reference( "HEAD", diff --git a/gix/tests/gix/repository/remote.rs b/gix/tests/gix/repository/remote.rs index f3a6fe117dc..bcc65e9f3a9 100644 --- a/gix/tests/gix/repository/remote.rs +++ b/gix/tests/gix/repository/remote.rs @@ -1,11 +1,11 @@ mod remote_at { - use crate::Result; use gix::remote::Direction; + use gix_testtools::TestResult; use crate::remote; #[test] - fn url_and_push_url() -> Result { + fn url_and_push_url() -> TestResult { let repo = remote::repo("base"); let fetch_url = "https://github.com/byron/gitoxide"; let remote = repo.remote_at(fetch_url)?; @@ -45,7 +45,7 @@ mod remote_at { } #[test] - fn url_rewrites_are_respected() -> Result { + fn url_rewrites_are_respected() -> TestResult { let repo = remote::repo("url-rewriting"); let remote = repo.remote_at("https://github.com/foobar/gitoxide")?; @@ -95,7 +95,7 @@ mod remote_at { } #[test] - fn url_rewrites_can_be_skipped() -> Result { + fn url_rewrites_can_be_skipped() -> TestResult { let repo = remote::repo("url-rewriting"); let remote = repo.remote_at_without_url_rewrite("https://github.com/foobar/gitoxide")?; @@ -150,7 +150,7 @@ mod remote_at { } #[test] - fn with_url_ignores_bad_push_fallback_rewrites() -> Result { + fn with_url_ignores_bad_push_fallback_rewrites() -> TestResult { let repo = remote::repo("bad-push-fallback-url-rewriting"); let remote = repo.remote_at("alias:one")?.with_url("alias:two")?; @@ -169,7 +169,7 @@ mod remote_at { } mod find_remote { - use crate::Result; + use gix_testtools::TestResult; use std::io::BufRead; use gix::{Repository, remote::Direction}; @@ -178,7 +178,7 @@ mod find_remote { use crate::remote; #[test] - fn tags_option() -> Result { + fn tags_option() -> TestResult { let repo = remote::repo("clone-no-tags"); for (remote_name, expected) in [ ("origin", gix::remote::fetch::Tags::None), @@ -192,7 +192,7 @@ mod find_remote { } #[test] - fn typical() -> Result { + fn typical() -> TestResult { let repo = remote::repo("clone"); let mut count = 0; let base_dir = base_dir(&repo); @@ -235,7 +235,7 @@ mod find_remote { } #[test] - fn missing_fetch_urls_only_fall_back_to_url_shaped_remote_names() -> Result { + fn missing_fetch_urls_only_fall_back_to_url_shaped_remote_names() -> TestResult { let repo = remote::repo("missing-urls"); for name in ["no-url", "reset-url"] { let remote = repo.find_remote(name)?; @@ -299,7 +299,7 @@ mod find_remote { } #[test] - fn instead_of_url_rewriting() -> Result { + fn instead_of_url_rewriting() -> TestResult { let repo = remote::repo("url-rewriting"); let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; @@ -344,7 +344,7 @@ mod find_remote { } #[test] - fn bad_url_rewriting_can_be_handled_much_like_git() -> Result { + fn bad_url_rewriting_can_be_handled_much_like_git() -> TestResult { let repo = remote::repo("bad-url-rewriting"); let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; @@ -398,7 +398,7 @@ mod find_remote { } #[test] - fn multiple_urls_are_preserved_in_order_and_single_url_matches_git_first_url() -> Result { + fn multiple_urls_are_preserved_in_order_and_single_url_matches_git_first_url() -> TestResult { let repo = remote::repo("multiple-urls"); let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; @@ -456,7 +456,7 @@ mod find_remote { } #[test] - fn multiple_url_rewriting_preserves_successful_rewrites_when_another_url_fails() -> Result { + fn multiple_url_rewriting_preserves_successful_rewrites_when_another_url_fails() -> TestResult { let repo = remote::repo("multiple-bad-url-rewriting"); let mut remote = repo.try_find_remote_without_url_rewrite("origin").expect("exists")?; @@ -477,7 +477,7 @@ mod find_remote { } #[test] - fn empty_url_values_reset_earlier_url_lists() -> Result { + fn empty_url_values_reset_earlier_url_lists() -> TestResult { let repo = remote::repo("multiple-urls-with-empty-reset"); let baseline = std::fs::read(repo.git_dir().join("baseline.git"))?; @@ -517,7 +517,7 @@ mod find_remote { } #[test] - fn bad_push_fallback_rewriting_does_not_break_fetch_remote() -> Result { + fn bad_push_fallback_rewriting_does_not_break_fetch_remote() -> TestResult { let repo = remote::repo("bad-push-fallback-url-rewriting"); let remote = repo.find_remote("origin")?; @@ -550,7 +550,7 @@ mod find_remote { } #[test] - fn bad_explicit_push_url_rewriting_is_reported_as_push_url() -> Result { + fn bad_explicit_push_url_rewriting_is_reported_as_push_url() -> TestResult { let repo = remote::repo("bad-explicit-push-url-rewriting"); insta::assert_debug_snapshot!(repo.find_remote("origin") @@ -591,24 +591,27 @@ mod find_remote { } fn base_dir(repo: &Repository) -> String { - gix_path::to_unix_separators_on_windows(gix::path::into_bstr( - gix::path::realpath(repo.workdir().unwrap()) - .unwrap() - .parent() - .unwrap() - .join("base"), - )) + gix_path::to_unix_separators_on_windows( + gix::path::into_bstr( + gix::path::realpath(repo.workdir().unwrap()) + .unwrap() + .parent() + .unwrap() + .join("base"), + ) + .expect("fixture paths can be represented as Git bytes"), + ) .into_owned() .to_string() } } mod find_fetch_remote { - use crate::Result; use crate::remote; + use gix_testtools::TestResult; #[test] - fn symbol_name() -> Result { + fn symbol_name() -> TestResult { let repo = remote::repo("clone-no-tags"); assert_eq!( repo.find_fetch_remote(Some("origin".into()))? @@ -621,7 +624,7 @@ mod find_fetch_remote { } #[test] - fn urls() -> Result { + fn urls() -> TestResult { let repo = remote::repo("clone-no-tags"); for url in [ "some-path", @@ -646,11 +649,11 @@ mod find_fetch_remote { mod find_default_remote { - use crate::Result; use crate::remote; + use gix_testtools::TestResult; #[test] - fn works_on_detached_heads() -> Result { + fn works_on_detached_heads() -> TestResult { let repo = remote::repo("detached-head"); assert_eq!( repo.find_default_remote(gix::remote::Direction::Fetch) diff --git a/gix/tests/gix/repository/revision.rs b/gix/tests/gix/repository/revision.rs index 3389c835ab9..1d6f033c13d 100644 --- a/gix/tests/gix/repository/revision.rs +++ b/gix/tests/gix/repository/revision.rs @@ -1,7 +1,7 @@ -use crate::Result; +use gix_testtools::TestResult; #[test] -fn missing_objects_info_does_not_prevent_merge_base() -> Result { +fn missing_objects_info_does_not_prevent_merge_base() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let info_dir = repo.objects.store_ref().path().join("info"); std::fs::create_dir_all(&info_dir)?; @@ -31,7 +31,7 @@ fn missing_objects_info_does_not_prevent_merge_base() -> Result { } #[test] -fn merge_base_variants_find_common_ancestors() -> Result { +fn merge_base_variants_find_common_ancestors() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let head_commit_id = repo.head_id()?; let parent_commit_id = repo.rev_parse_single("HEAD^")?; @@ -79,7 +79,7 @@ fn merge_base_variants_find_common_ancestors() -> Result { } #[test] -fn unrelated_histories_have_no_merge_base() -> Result { +fn unrelated_histories_have_no_merge_base() -> TestResult { let (repo, _tmp) = crate::util::basic_rw_repo()?; let head = repo.head_commit()?; let head_commit_id = head.id(); @@ -110,7 +110,7 @@ fn unrelated_histories_have_no_merge_base() -> Result { } #[test] -fn date() -> Result { +fn date() -> TestResult { let repo = crate::named_repo("make_rev_parse_repo.sh")?; let actual = repo .rev_parse_single("old@{20 years ago}") diff --git a/gix/tests/gix/repository/shallow.rs b/gix/tests/gix/repository/shallow.rs index 43897104b39..6fea5ac11c2 100644 --- a/gix/tests/gix/repository/shallow.rs +++ b/gix/tests/gix/repository/shallow.rs @@ -1,4 +1,4 @@ -use crate::Result; +use gix_testtools::{Result, TestResult}; use serial_test::parallel; use crate::util::{hex_to_id, named_subrepo_opts}; @@ -12,10 +12,10 @@ fn shallow_ids(repo: &gix::Repository) -> Result<Vec<gix::ObjectId>> { #[test] #[parallel] -fn no() -> Result { +fn no() -> TestResult { for name in ["base", "empty"] { let repo = named_subrepo_opts("make_shallow_repo.sh", name, crate::restricted())?; - assert!(!repo.is_shallow()); + assert!(!repo.is_shallow()?); assert!(repo.shallow_commits()?.is_none()); let commits: Vec<_> = repo .head_id()? @@ -39,10 +39,10 @@ fn no() -> Result { #[test] #[parallel] -fn yes() -> Result { +fn yes() -> TestResult { for name in ["shallow.git", "shallow"] { let repo = named_subrepo_opts("make_shallow_repo.sh", name, crate::restricted())?; - assert!(repo.is_shallow()); + assert!(repo.is_shallow()?); assert_eq!( shallow_ids(&repo)?, [hex_to_id("30887839de28edf7ab66c860e5c58b4d445f6b12")] @@ -52,7 +52,7 @@ fn yes() -> Result { } mod traverse { - use crate::Result; + use gix_testtools::TestResult; use gix_traverse::commit::simple::CommitTimeOrder; use serial_test::parallel; @@ -60,7 +60,7 @@ mod traverse { #[test] #[parallel] - fn boundary_is_detected_triggering_no_error() -> Result { + fn boundary_is_detected_triggering_no_error() -> TestResult { for sorting in [ gix::revision::walk::Sorting::BreadthFirst, gix::revision::walk::Sorting::ByCommitTime(CommitTimeOrder::NewestFirst), @@ -89,7 +89,7 @@ mod traverse { #[test] #[parallel] - fn complex_graphs_can_be_iterated_despite_multiple_shallow_boundaries() -> Result { + fn complex_graphs_can_be_iterated_despite_multiple_shallow_boundaries() -> TestResult { let base = gix_path::realpath(gix_testtools::scripted_fixture_read_only("make_remote_repos.sh")?.join("base"))?; let shallow_base = gix_testtools::scripted_fixture_read_only_with_args_single_archive( "make_complex_shallow_repo.sh", diff --git a/gix/tests/gix/repository/state.rs b/gix/tests/gix/repository/state.rs index f0b4b91c92e..828d02b4eb8 100644 --- a/gix/tests/gix/repository/state.rs +++ b/gix/tests/gix/repository/state.rs @@ -1,7 +1,7 @@ -use crate::{Result, named_repo}; +use crate::named_repo; #[test] -fn apply_mailbox() -> Result { +fn apply_mailbox() -> gix_testtools::TestResult { let repo = named_repo("make_am_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -10,7 +10,7 @@ fn apply_mailbox() -> Result { } #[test] -fn bisect() -> Result { +fn bisect() -> gix_testtools::TestResult { let repo = named_repo("make_bisect_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -20,7 +20,7 @@ fn bisect() -> Result { } #[test] -fn cherry_pick() -> Result { +fn cherry_pick() -> gix_testtools::TestResult { let repo = named_repo("make_cherry_pick_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -29,7 +29,7 @@ fn cherry_pick() -> Result { } #[test] -fn cherry_pick_sequence() -> Result { +fn cherry_pick_sequence() -> gix_testtools::TestResult { let repo = named_repo("make_cherry_pick_sequence_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -39,7 +39,7 @@ fn cherry_pick_sequence() -> Result { } #[test] -fn merge() -> Result { +fn merge() -> gix_testtools::TestResult { let repo = named_repo("make_merge_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -49,7 +49,7 @@ fn merge() -> Result { } #[test] -fn rebase_interactive() -> Result { +fn rebase_interactive() -> gix_testtools::TestResult { let repo = named_repo("make_rebase_i_repo.sh")?; assert!(repo.head()?.is_detached()); @@ -59,7 +59,7 @@ fn rebase_interactive() -> Result { } #[test] -fn revert() -> Result { +fn revert() -> gix_testtools::TestResult { let repo = named_repo("make_revert_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); @@ -69,7 +69,7 @@ fn revert() -> Result { } #[test] -fn revert_sequence() -> Result { +fn revert_sequence() -> gix_testtools::TestResult { let repo = named_repo("make_revert_sequence_repo.sh")?; assert_eq!(repo.head_name()?.unwrap().shorten(), "main"); diff --git a/gix/tests/gix/repository/submodule.rs b/gix/tests/gix/repository/submodule.rs index ae809765917..e9618e1383d 100644 --- a/gix/tests/gix/repository/submodule.rs +++ b/gix/tests/gix/repository/submodule.rs @@ -1,9 +1,9 @@ mod modules_file { - use crate::Result; use crate::submodule::repo; + use gix_testtools::TestResult; #[test] - fn overrides_respect_section_trust_for_every_modules_source() -> Result { + fn overrides_respect_section_trust_for_every_modules_source() -> TestResult { use gix::submodule::config::Update; use gix_sec::Trust; @@ -62,7 +62,7 @@ mod modules_file { } #[test] - fn none_if_not_present() -> Result { + fn none_if_not_present() -> TestResult { let repo = repo("module1")?; assert!(repo.open_modules_file()?.is_none(), "it's OK to not have such a file"); assert!( @@ -73,7 +73,7 @@ mod modules_file { } #[test] - fn is_read_from_worktree() -> Result { + fn is_read_from_worktree() -> TestResult { let repo = repo("with-submodules")?; let modules = repo.modules()?.expect("present"); assert_eq!( @@ -85,7 +85,7 @@ mod modules_file { } #[test] - fn is_read_from_index_if_not_in_worktree() -> Result { + fn is_read_from_index_if_not_in_worktree() -> TestResult { let repo = repo("with-submodules-in-index")?; assert!( repo.open_modules_file()?.is_none(), @@ -101,7 +101,7 @@ mod modules_file { } #[test] - fn is_read_from_tree_if_not_in_index() -> Result { + fn is_read_from_tree_if_not_in_index() -> TestResult { let repo = repo("with-submodules-in-tree")?; assert!( repo.open_modules_file()?.is_none(), @@ -118,13 +118,13 @@ mod modules_file { } mod submodules { - use crate::Result; use gix::bstr::BString; + use gix_testtools::TestResult; use crate::{submodule::repo, util::hex_to_id}; #[test] - fn all_modules_are_active_by_default() -> Result { + fn all_modules_are_active_by_default() -> TestResult { let repo = repo("with-submodules")?; let id = hex_to_id("e046f3e51d955840619fc7d01fbd9a469663de22"); assert_eq!( @@ -159,13 +159,13 @@ mod submodules { #[cfg(unix)] mod advisory { - use crate::Result; + use gix_testtools::TestResult; use gix_testtools::tempfile; /// Regression test for GHSA-pg4w-g64p-qwhj: a symlinked worktree `.gitmodules` must not allow /// attacker-controlled bytes outside the repository to define submodule configuration. #[test] - fn symlinked_gitmodules_are_rejected() -> Result { + fn symlinked_gitmodules_are_rejected() -> TestResult { use std::os::unix::fs as unix_fs; let temp = tempfile::tempdir()?; @@ -198,7 +198,7 @@ mod advisory { } #[test] - fn symlinked_gitmodules_fall_back_to_index_and_tree() -> Result { + fn symlinked_gitmodules_fall_back_to_index_and_tree() -> TestResult { let temp = tempfile::tempdir()?; let repo_dir = temp.path().join("repo"); crate::init_repo_isolated(&repo_dir, gix::create::Kind::WithWorktree)?; diff --git a/gix/tests/gix/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs index 6bffea8badd..9ce8e4b987e 100644 --- a/gix/tests/gix/repository/worktree.rs +++ b/gix/tests/gix/repository/worktree.rs @@ -1,5 +1,5 @@ -use crate::Result; use gix_ref::bstr; +use gix_testtools::{Result, TestResult}; #[cfg(feature = "worktree-mutation")] mod add { @@ -10,7 +10,7 @@ mod add { use gix_path::{into_bstr, to_unix_separators_on_windows}; #[test] - fn initial_head_reflog_respects_configuration_like_git() -> crate::Result { + fn initial_head_reflog_respects_configuration_like_git() -> gix_testtools::TestResult { for (log_all_ref_updates, worktree_config) in [ (None, false), (Some("true"), false), @@ -82,7 +82,7 @@ mod add { ); let head = created.find_reference("HEAD")?; assert_eq!( - head.log_exists(), + head.log_exists()?, logging_enabled, "new HEAD logging matches Git for attached={attached}, core.logAllRefUpdates={log_all_ref_updates:?}, worktreeConfig={worktree_config}" ); @@ -131,7 +131,7 @@ mod add { } #[test] - fn bisect_and_rebase_reserve_branches_in_main_and_linked_worktrees() -> crate::Result { + fn bisect_and_rebase_reserve_branches_in_main_and_linked_worktrees() -> gix_testtools::TestResult { for operation in ["bisect", "rebase-merge", "rebase-apply", "rebase-interactive"] { for occupied in ["main", "linked"] { let fixture = gix_testtools::scripted_fixture_writable_with_args( @@ -212,7 +212,7 @@ mod add { } #[test] - fn mailbox_application_does_not_reserve_a_rebase_branch() -> crate::Result { + fn mailbox_application_does_not_reserve_a_rebase_branch() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; std::fs::write(repo.git_dir().join("HEAD"), format!("{}\n", repo.head_id()?))?; @@ -226,13 +226,12 @@ mod add { gix::worktree::add::Head::Attached(branch("main")), gix::progress::Discard, &AtomicBool::default(), - ) - .expect("`git am` sequence editor usage doesn't prevent worktree creation"); + )?; Ok(()) } #[test] - fn worktree_config_is_inherited_before_checkout() -> crate::Result { + fn worktree_config_is_inherited_before_checkout() -> gix_testtools::TestResult { // MSYS sed's text mode would discard CRLF produced before smudge filtering. let sed = if cfg!(windows) { "sed -b" } else { "sed" }; let fixture = gix_testtools::scripted_fixture_writable_with_args( @@ -255,7 +254,7 @@ mod add { let mut config = source.config_file_mut(&config_path)?; config.set_raw_value("core.autocrlf", autocrlf)?; config.set_raw_value("core.bare", "false")?; - config.set_raw_value("core.worktree", to_unix_separators_on_windows(into_bstr(&work_dir)))?; + config.set_raw_value("core.worktree", to_unix_separators_on_windows(into_bstr(&work_dir)?))?; config.set_raw_value("filter.inherit.smudge", format!("{sed} s/hello/{name}/"))?; config.set_raw_value("filter.inherit.clean", format!("{sed} s/{name}/hello/"))?; config.set_raw_value("filter.inherit.required", "true")?; @@ -308,7 +307,7 @@ mod add { } #[test] - fn common_attributes_are_applied_during_checkout() -> crate::Result { + fn common_attributes_are_applied_during_checkout() -> gix_testtools::TestResult { // Preserve the checkout's line endings when the filter runs under MSYS. let sed = if cfg!(windows) { "sed -b" } else { "sed" }; let (mut source, _fixture) = crate::basic_rw_repo()?; @@ -354,7 +353,7 @@ mod add { } #[test] - fn worktree_config_is_optional_but_copy_errors_roll_back() -> crate::Result { + fn worktree_config_is_optional_but_copy_errors_roll_back() -> gix_testtools::TestResult { let (mut repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let source_config = repo.git_dir().join("config.worktree"); @@ -415,7 +414,7 @@ mod add { } #[test] - fn relative_links_survive_moving_the_repository_and_worktrees_together() -> crate::Result { + fn relative_links_survive_moving_the_repository_and_worktrees_together() -> gix_testtools::TestResult { use std::fs; let (repo, _fixture) = crate::basic_rw_repo()?; @@ -499,7 +498,7 @@ mod add { } #[test] - fn relative_worktrees_extension_does_not_enable_relative_links_by_itself() -> crate::Result { + fn relative_worktrees_extension_does_not_enable_relative_links_by_itself() -> gix_testtools::TestResult { let (mut repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let mut config = repo.config_file_mut(repo.common_dir().join("config"))?; @@ -522,7 +521,7 @@ mod add { std::fs::read_to_string(linked.git_dir().join("gitdir"))?, format!( "{}\n", - to_unix_separators_on_windows(into_bstr(linked.workdir().expect("linked checkout").join(".git"))) + to_unix_separators_on_windows(into_bstr(linked.workdir().expect("linked checkout").join(".git"))?) ), "the extension records compatibility; the worktree setting controls new links" ); @@ -531,7 +530,7 @@ mod add { } #[test] - fn invalid_repository_format_versions_reject_relative_worktrees() -> crate::Result { + fn invalid_repository_format_versions_reject_relative_worktrees() -> gix_testtools::TestResult { use gix::config::tree::Core; use gix_error::MetadataValue; @@ -590,7 +589,7 @@ mod add { } #[test] - fn failed_relative_config_update_rolls_back_worktree_addition() -> crate::Result { + fn failed_relative_config_update_rolls_back_worktree_addition() -> gix_testtools::TestResult { for (scenario, lock_config) in [ ("relative-config-lock", true), ("relative-config-unknown-extension", false), @@ -651,7 +650,7 @@ mod add { } #[test] - fn attached_and_detached_worktrees_are_checked_out_and_recognized_by_git() -> crate::Result { + fn attached_and_detached_worktrees_are_checked_out_and_recognized_by_git() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let commit_id = repo.head_id()?.detach(); @@ -693,18 +692,18 @@ mod add { let listing = gix_testtools::git(repo.workdir().expect("non-bare fixture"), "worktree list --porcelain")?; assert!( - listing.contains(to_unix_separators_on_windows(into_bstr(&attached_path)).to_str()?), + listing.contains(to_unix_separators_on_windows(into_bstr(&attached_path)?).to_str()?), "Git recognizes the attached worktree" ); assert!( - listing.contains(to_unix_separators_on_windows(into_bstr(&detached_path)).to_str()?), + listing.contains(to_unix_separators_on_windows(into_bstr(&detached_path)?).to_str()?), "Git recognizes the detached worktree" ); Ok(()) } #[test] - fn linked_worktree_cannot_check_out_a_branch_occupied_in_any_worktree() -> crate::Result { + fn linked_worktree_cannot_check_out_a_branch_occupied_in_any_worktree() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; assert_eq!( repo.worktrees_including_main()?.collect::<gix::Result<Vec<_>>>()?, @@ -769,7 +768,7 @@ mod add { } #[test] - fn lock_suffix_only_destination_names_match_git() -> crate::Result { + fn lock_suffix_only_destination_names_match_git() -> gix_testtools::TestResult { let (source, _fixture) = crate::basic_rw_repo()?; let work_dir = source.workdir().expect("source checkout"); gix_testtools::git(work_dir, "worktree add --detach git/.lock.lock HEAD")?; @@ -810,7 +809,7 @@ mod add { } #[test] - fn attached_worktrees_ignore_reference_namespaces_like_git() -> crate::Result { + fn attached_worktrees_ignore_reference_namespaces_like_git() -> gix_testtools::TestResult { for (namespace, from_config) in [("foo", false), ("foo/bar", true)] { let (mut source, _fixture) = crate::basic_rw_repo()?; let work_dir = source.workdir().expect("source checkout").to_owned(); @@ -918,7 +917,7 @@ mod add { } #[test] - fn separate_git_dir_preserves_main_worktree_and_its_occupied_branch() -> crate::Result { + fn separate_git_dir_preserves_main_worktree_and_its_occupied_branch() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable_with_args( "make_worktree_add_repos.sh", ["separate-git-dir"], @@ -975,7 +974,7 @@ mod add { } #[test] - fn adds_a_worktree_from_a_bare_parent() -> crate::Result { + fn adds_a_worktree_from_a_bare_parent() -> gix_testtools::TestResult { let Some(fixture) = gix_testtools::scripted_fixture_writable_with_args_with_git_version( "make_worktree_repo.sh", ["bare"], @@ -1030,7 +1029,7 @@ mod add { } #[test] - fn validation_failures_leave_the_destination_absent() -> crate::Result { + fn validation_failures_leave_the_destination_absent() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("rejected"); @@ -1093,7 +1092,7 @@ mod add { } #[test] - fn registered_destinations_are_rejected_even_when_missing_or_empty() -> crate::Result { + fn registered_destinations_are_rejected_even_when_missing_or_empty() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("registered"); @@ -1144,7 +1143,7 @@ mod add { #[test] #[cfg(unix)] - fn registered_destinations_are_matched_through_symlinked_parents() -> crate::Result { + fn registered_destinations_are_matched_through_symlinked_parents() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let actual_parent = destinations.path().join("actual"); @@ -1192,7 +1191,7 @@ mod remove { }; #[test] - fn rejects_empty_targets() -> crate::Result { + fn rejects_empty_targets() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let err = repo .remove_worktree("", Force::Never, gix::progress::Discard) @@ -1209,7 +1208,7 @@ mod remove { } #[test] - fn removes_worktrees_from_a_repository_opened_with_a_relative_path() -> crate::Result { + fn removes_worktrees_from_a_repository_opened_with_a_relative_path() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1244,7 +1243,7 @@ mod remove { let _moved_cwd = gix_testtools::set_current_dir(elsewhere.path())?; assert_eq!( - repo.worktree_proxy_by_id(proxy.id()) + repo.worktree_proxy_by_id(proxy.id()?)? .expect("registration lookup uses the repository CWD") .git_dir(), proxy.git_dir(), @@ -1269,7 +1268,7 @@ mod remove { } #[test] - fn resolves_registered_paths_after_changing_current_directory() -> crate::Result { + fn resolves_registered_paths_after_changing_current_directory() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -1324,7 +1323,7 @@ mod remove { } #[test] - fn removes_a_clean_worktree_by_suffix_without_deleting_its_branch() -> crate::Result { + fn removes_a_clean_worktree_by_suffix_without_deleting_its_branch() -> gix_testtools::TestResult { let (mut repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("nested/topic-checkout"); @@ -1368,7 +1367,7 @@ mod remove { } #[test] - fn permits_removing_the_current_linked_worktree_but_not_the_main_worktree() -> crate::Result { + fn permits_removing_the_current_linked_worktree_but_not_the_main_worktree() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("current"); @@ -1396,7 +1395,7 @@ mod remove { } #[test] - fn dirty_and_locked_worktrees_require_the_corresponding_force_level() -> crate::Result { + fn dirty_and_locked_worktrees_require_the_corresponding_force_level() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let dirty_path = gix_path::realpath(destinations.path().join("dirty"))?; @@ -1444,7 +1443,7 @@ mod remove { } #[test] - fn untracked_files_hidden_by_status_configuration_require_force() -> crate::Result { + fn untracked_files_hidden_by_status_configuration_require_force() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = gix_path::realpath(destinations.path().join("hidden-untracked"))?; @@ -1499,7 +1498,7 @@ mod remove { } #[test] - fn initialized_submodules_require_force() -> crate::Result { + fn initialized_submodules_require_force() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("submodules"); @@ -1525,7 +1524,7 @@ mod remove { } #[test] - fn backlink_validation_is_never_forced_and_missing_checkouts_are_unregistered() -> crate::Result { + fn backlink_validation_is_never_forced_and_missing_checkouts_are_unregistered() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let invalid_path = destinations.path().join("invalid-backlink"); @@ -1589,7 +1588,7 @@ mod remove { } #[test] - fn ambiguous_suffixes_can_be_disambiguated_with_an_exact_path() -> crate::Result { + fn ambiguous_suffixes_can_be_disambiguated_with_an_exact_path() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let first_path = destinations.path().join("one/shared"); @@ -1628,7 +1627,7 @@ mod remove { #[test] #[cfg(windows)] - fn canonical_paths_resolve_git_registered_worktrees() -> crate::Result { + fn canonical_paths_resolve_git_registered_worktrees() -> gix_testtools::TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; let destinations = gix_testtools::tempfile::TempDir::new()?; let destination = destinations.path().join("topic"); @@ -1676,7 +1675,7 @@ fn expected_buffer_length(repo: &gix::Repository) -> usize { #[test] #[cfg(feature = "worktree-stream")] -fn stream() -> Result { +fn stream() -> TestResult { let repo = crate::named_repo("make_packed_and_loose.sh")?; let mut stream = repo.worktree_stream(repo.head_commit()?.tree_id()?)?.0.into_read(); assert_eq!( @@ -1689,7 +1688,7 @@ fn stream() -> Result { #[test] #[cfg(feature = "worktree-archive")] -fn archive() -> Result { +fn archive() -> TestResult { let repo = crate::named_repo("make_packed_and_loose.sh")?; let (stream, _index) = repo.worktree_stream(repo.head_commit()?.tree_id()?)?; let mut buf = Vec::<u8>::new(); @@ -1706,12 +1705,12 @@ fn archive() -> Result { } mod with_core_worktree_config { - use crate::Result; + use gix_testtools::TestResult; use std::io::BufRead; #[test] #[cfg(feature = "index")] - fn relative() -> Result { + fn relative() -> TestResult { for (name, is_relative) in [("absolute-worktree", false), ("relative-worktree", true)] { let repo = repo(name); @@ -1787,7 +1786,7 @@ mod with_core_worktree_config { #[test] #[cfg(feature = "index")] - fn bare_relative() -> Result { + fn bare_relative() -> TestResult { let repo = repo("bare-relative-worktree"); assert_eq!( @@ -1806,7 +1805,7 @@ mod with_core_worktree_config { #[test] #[cfg(unix)] // symlinks are used here, let's not try our luck on Windows. - fn relative_through_symlinked_ancestor_keeps_callers_path_namespace() -> Result { + fn relative_through_symlinked_ancestor_keeps_callers_path_namespace() -> TestResult { let link = gix_testtools::scripted_fixture_read_only("make_core_worktree_repo.sh")?.join("symlinked-ancestor"); let repo = gix::open_opts(link.join("relative-worktree"), crate::restricted())?; @@ -1821,7 +1820,7 @@ mod with_core_worktree_config { #[test] #[cfg(unix)] // symlinks are used here, let's not try our luck on Windows. - fn relative_from_symlinked_git_dir() -> Result { + fn relative_from_symlinked_git_dir() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_core_worktree_repo.sh")?; let root = fixture.join("linked-git-dir-detached-worktree"); let repo = gix::open_opts(root.join("home"), crate::restricted())?; @@ -1888,7 +1887,9 @@ mod baseline { type Item = Worktree; fn next(&mut self) -> Option<Self::Item> { - let root = gix_path::from_bstr(Cow::Borrowed(fields(self.lines.next()?).1)).into_owned(); + let root = gix_path::from_bstr(Cow::Borrowed(fields(self.lines.next()?).1)) + .expect("Git baseline paths can be represented natively") + .into_owned(); let mut bare = false; let mut branch = None; let mut peeled = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); @@ -1931,7 +1932,7 @@ mod baseline { } #[test] -fn worktree_listing_errors_preserve_io_errors() -> Result { +fn worktree_listing_errors_preserve_io_errors() -> TestResult { let (repo, _fixture) = crate::basic_rw_repo()?; std::fs::write(repo.common_dir().join("worktrees"), b"not a directory")?; @@ -1954,35 +1955,35 @@ fn worktree_listing_errors_preserve_io_errors() -> Result { } #[test] -fn from_bare_parent_repo() { +fn from_bare_parent_repo() -> TestResult { let Some(dir) = gix_testtools::scripted_fixture_read_only_with_args_with_git_version( "make_worktree_repo.sh", ["bare"], |version| version >= (2, 31, 0), - ) - .unwrap() else { - return; + )? + else { + return Ok(()); }; - let repo = gix::open_opts(dir.join("repo.git"), crate::restricted()).expect("fixture repository opens"); + let repo = gix::open_opts(dir.join("repo.git"), crate::restricted())?; - run_assertions(repo, true /* bare */); + Ok(run_assertions(repo, true /* bare */)?) } #[test] -fn from_nonbare_parent_repo() { +fn from_nonbare_parent_repo() -> TestResult { let Some(dir) = gix_testtools::scripted_fixture_read_only_with_git_version("make_worktree_repo.sh", |version| { version >= (2, 31, 0) - }) - .unwrap() else { - return; + })? + else { + return Ok(()); }; - let repo = gix::open_opts(dir.join("repo"), crate::restricted()).expect("fixture repository opens"); + let repo = gix::open_opts(dir.join("repo"), crate::restricted())?; - run_assertions(repo, false /* bare */); + Ok(run_assertions(repo, false /* bare */)?) } #[test] -fn linked_worktree_proxy_base_with_relative_linking_files() -> Result { +fn linked_worktree_proxy_base_with_relative_linking_files() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?; let main = fixture.join("main"); let linked = fixture.join("linked"); @@ -2013,7 +2014,7 @@ fn linked_worktree_proxy_base_with_relative_linking_files() -> Result { #[test] #[cfg(unix)] -fn linked_worktree_proxy_base_with_symlinked_main_repo() -> Result { +fn linked_worktree_proxy_base_with_symlinked_main_repo() -> TestResult { let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?; let linked = fixture.join("actual/linked"); let main_symlink = fixture.join("main-symlink"); @@ -2038,22 +2039,22 @@ fn linked_worktree_proxy_base_with_symlinked_main_repo() -> Result { } #[test] -fn from_nonbare_parent_repo_set_workdir() -> gix_testtools::Result { +fn from_nonbare_parent_repo_set_workdir() -> gix_testtools::TestResult { let Some(dir) = gix_testtools::scripted_fixture_read_only_with_git_version("make_worktree_repo.sh", |version| { version >= (2, 31, 0) })? else { return Ok(()); }; - let mut repo = gix::open_opts(dir.join("repo"), crate::restricted()).expect("fixture repository opens"); + let mut repo = gix::open_opts(dir.join("repo"), crate::restricted())?; assert!(repo.worktree().is_some_and(|wt| wt.is_main()), "we have main worktree"); let worktrees = repo.worktrees()?; assert_eq!(worktrees.len(), 6); - let linked_wt_dir = worktrees.first().unwrap().base().expect("this linked worktree exists"); - repo.set_workdir(linked_wt_dir).expect("works as the dir exists"); + let linked_wt_dir = worktrees.first().unwrap().base()?; + repo.set_workdir(linked_wt_dir)?; assert!( repo.worktree().is_some_and(|wt| wt.is_main()), @@ -2075,7 +2076,7 @@ fn from_nonbare_parent_repo_set_workdir() -> gix_testtools::Result { Ok(()) } -fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { +fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) -> Result { assert_eq!(main_repo.is_bare(), should_be_bare); assert_eq!(main_repo.kind(), gix::repository::Kind::Common); let mut baseline = Baseline::collect( @@ -2154,7 +2155,7 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { "prunability matches `git worktree list --porcelain`" ); // TODO: check id of expected worktree, but need access to .gitdir from worktree base - let proxy_id = actual.id().to_owned(); + let proxy_id = actual.id()?.to_owned(); assert_eq!( base.is_dir(), expected.prunable.is_none(), @@ -2162,7 +2163,7 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { ); assert_eq!( - main_repo.worktree_proxy_by_id(actual.id()).expect("exists").git_dir(), + main_repo.worktree_proxy_by_id(actual.id()?)?.expect("exists").git_dir(), actual.git_dir(), "we can basically get the same proxy by its ID explicitly" ); @@ -2186,7 +2187,7 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { assert!(!worktree.is_main()); assert_eq!(worktree.lock_reason(), proxy_lock_reason); assert_eq!(worktree.is_locked(), proxy_is_locked); - assert_eq!(worktree.id(), Some(proxy_id.as_ref())); + assert_eq!(worktree.id()?, Some(proxy_id.as_ref())); assert_eq!( repo.main_repo().unwrap(), main_repo, @@ -2194,7 +2195,7 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { ); let proxy_by_id = repo - .worktree_proxy_by_id(actual.id()) + .worktree_proxy_by_id(actual.id()?)? .expect("can get the proxy from a linked repo as well"); assert_eq!( proxy_by_id.git_dir(), @@ -2207,4 +2208,5 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { "the git directories are effectively the same" ); } + Ok(()) } diff --git a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs index 457ed4cbabc..cfe9afc7843 100644 --- a/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs +++ b/gix/tests/gix/revision/spec/from_bytes/ambiguous.rs @@ -1,5 +1,4 @@ use super::{missing_reference_names, repo}; -use crate::Result; use crate::{ revision::spec::from_bytes::{ normalize_repo_path, parse_spec, parse_spec_better_than_baseline, parse_spec_no_baseline, @@ -14,9 +13,10 @@ use gix::{ spec::parse::{CandidateInfo, Error, Options, RefsHint}, }, }; +use gix_testtools::TestResult; #[test] -fn prefix() -> Result { +fn prefix() -> TestResult { let mut error_snapshots = Vec::new(); { let repo = repo("blob.prefix")?; @@ -64,7 +64,7 @@ fn prefix() -> Result { assert!( source.downcast_any_ref::<gix_error::Message>().is_some(), "the owned lookup error retains concrete causes" - ); + ); } insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&format_args!("{}", normalize_repo_path(&format!("{err:#?}"), &repo)), &[]), "ambiguous prefixes retain the lookup failure for each malformed candidate", @" Short id bad0 is ambiguous. Candidates are: @@ -82,7 +82,7 @@ fn prefix() -> Result { } #[test] -fn fully_failed_disambiguation_still_yields_an_ambiguity_error() -> Result { +fn fully_failed_disambiguation_still_yields_an_ambiguity_error() -> TestResult { let repo = repo("ambiguous_blob_tree_commit")?; let err = parse_spec("0000000000^{tag}", &repo).expect_err("none of the candidates can peel to a tag"); @@ -141,7 +141,7 @@ fn ranges_are_auto_disambiguated_by_committish() { } #[test] -fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> Result { +fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; let repo = gix::open_opts(fixture.path().join("ambiguous_blob_tree_commit"), crate::restricted())?; std::fs::write(repo.git_dir().join("refs/heads/alias"), b"ref: refs/heads/missing\n")?; @@ -156,7 +156,7 @@ fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> Result { assert!( err.iter_errors().any(|cause| matches!( cause.downcast_ref::<Error>(), - Some(Error::MissingReference { name }) if name == std::path::Path::new("refs/heads/missing") + Some(Error::MissingReference { name }) if name == gix::bstr::BStr::new("refs/heads/missing") )), "rejected candidates do not hide the parser-owned missing-reference error" ); @@ -164,7 +164,7 @@ fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> Result { err.downcast_any_ref::<gix::refs::file::find::NotFound>() .expect("the lookup error survives rejected-candidate errors") .name, - std::path::Path::new("refs/heads/missing"), + gix::bstr::BStr::new("refs/heads/missing"), "the final spec conversion retains the actual lookup failure" ); insta::assert_debug_snapshot!(err, @r#" @@ -174,14 +174,14 @@ fn resolved_ambiguity_does_not_hide_a_missing_symbolic_referent() -> Result { 0: Last encountered object 0000000000b was blob while trying to peel to commit 1: Last encountered object 0000000000c was tree while trying to peel to commit 2: Could not peel 'refs/heads/alias' to obtain its target - ├─0: Reference refs/heads/missing could not be found + ├─0: Reference "refs/heads/missing" could not be found └─1: The ref partially named "refs/heads/missing" could not be found "#); Ok(()) } #[test] -fn missing_references_survive_other_endpoint_ambiguity() -> Result { +fn missing_references_survive_other_endpoint_ambiguity() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; for (fixture_name, remains_ambiguous) in [ ("ambiguous_blob_tree_commit", false), @@ -195,14 +195,14 @@ fn missing_references_survive_other_endpoint_ambiguity() -> Result { .expect_err("the right endpoint's symbolic referent is missing"); assert_eq!( missing_reference_names(&err), - [std::path::Path::new("refs/heads/missing")], + [gix::bstr::BStr::new("refs/heads/missing")], "{fixture_name}: {input} retains missing references during callbacks and finalization: {err}" ); assert_eq!( err.downcast_any_ref::<gix::refs::file::find::NotFound>() .expect("the original lookup cause is retained alongside parser recovery errors") .name, - std::path::Path::new("refs/heads/missing"), + gix::bstr::BStr::new("refs/heads/missing"), "aggregation does not discard the original missing-reference cause" ); assert_eq!( @@ -290,7 +290,7 @@ fn tags_can_be_disambiguated_with_commit_specific_transformations() { } #[test] -fn duplicates_are_deduplicated_across_all_odb_types() -> Result { +fn duplicates_are_deduplicated_across_all_odb_types() -> TestResult { let repo = repo("duplicate_ambiguous_objects")?; let err = parse_spec_no_baseline("0000000000", &repo).expect_err("multiple distinct candidates match"); let Some(Error::AmbiguousPrefix { candidates, .. }) = err.downcast_any_ref::<Error>() else { @@ -354,7 +354,7 @@ fn duplicates_are_deduplicated_across_all_odb_types() -> Result { } #[test] -fn malformed_commit_and_tag_candidates_retain_decode_errors() -> Result { +fn malformed_commit_and_tag_candidates_retain_decode_errors() -> TestResult { use gix_object::Write; let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; @@ -509,7 +509,7 @@ fn ambiguous_short_refs_are_dereferenced() { } #[test] -fn reference_collisions_retain_multiple_object_candidates() -> Result { +fn reference_collisions_retain_multiple_object_candidates() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; let repo = gix::open_opts(fixture.path().join("ambiguous_blob_tree_commit"), crate::restricted())?; repo.reference( diff --git a/gix/tests/gix/revision/spec/from_bytes/mod.rs b/gix/tests/gix/revision/spec/from_bytes/mod.rs index 68771cac6ce..f7f9163a230 100644 --- a/gix/tests/gix/revision/spec/from_bytes/mod.rs +++ b/gix/tests/gix/revision/spec/from_bytes/mod.rs @@ -1,8 +1,9 @@ -use crate::Result; use gix::{ + bstr::ByteSlice, prelude::ObjectIdExt, revision::{Spec, spec::parse::Error}, }; +use gix_testtools::TestResult; pub use util::*; use crate::util::hex_to_id_sha1_only; @@ -16,7 +17,7 @@ mod traverse; mod peel; -fn missing_reference_names(err: &gix::Error) -> Vec<&std::path::Path> { +fn missing_reference_names(err: &gix::Error) -> Vec<&gix::bstr::BStr> { err.iter_errors() .filter_map(|cause| match cause.downcast_ref::<Error>() { Some(missing_reference @ Error::MissingReference { name }) => { @@ -24,7 +25,7 @@ fn missing_reference_names(err: &gix::Error) -> Vec<&std::path::Path> { gix_error::classify(missing_reference).is_not_found(), "the missing-reference variant is intrinsically classified as not found" ); - Some(name.as_path()) + Some(name.as_bstr()) } _ => None, }) @@ -32,14 +33,14 @@ fn missing_reference_names(err: &gix::Error) -> Vec<&std::path::Path> { } mod sibling_branch { - use crate::Result; use crate::{ revision::spec::from_bytes::{missing_reference_names, parse_spec, repo}, util::hex_to_id_sha1_only, }; + use gix_testtools::TestResult; #[test] - fn explicit_head_uses_the_current_branch() -> gix_error::TestResult { + fn explicit_head_uses_the_current_branch() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_tracking_branch_revspecs.sh")?; let repo = gix::open_opts(fixture, crate::restricted())?; for op in ["upstream", "u", "push"] { @@ -59,7 +60,7 @@ mod sibling_branch { } #[test] - fn push_and_upstream() -> Result { + fn push_and_upstream() -> TestResult { let repo = repo("complex_graph").unwrap(); for op in ["upstream", "push"] { for branch in ["", "main"] { @@ -76,7 +77,7 @@ mod sibling_branch { } #[test] - fn missing_tracking_references_are_classified() -> Result { + fn missing_tracking_references_are_classified() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_rev_spec_parse_repos.sh")?; let repo = gix::open_opts(fixture.path().join("complex_graph"), crate::restricted())?; repo.find_reference("refs/remotes/origin/main")?.delete()?; @@ -93,14 +94,14 @@ mod sibling_branch { ); assert_eq!( missing_reference_names(&err), - [std::path::Path::new("refs/remotes/origin/main")], + [gix::bstr::BStr::new("refs/remotes/origin/main")], "{revspec} exposes the mapped tracking reference, not the local branch" ); assert_eq!( err.downcast_any_ref::<gix::refs::file::find::NotFound>() .expect("the original tracking-reference lookup failure remains available") .name, - std::path::Path::new("refs/remotes/origin/main"), + gix::bstr::BStr::new("refs/remotes/origin/main"), "{revspec} retains the original missing tracking reference name" ); } @@ -183,7 +184,7 @@ fn names_are_made_available_via_references() { } #[test] -fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() -> Result { +fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() -> TestResult { let repo = repo("complex_graph")?; let err = repo .rev_parse("README.md") @@ -192,7 +193,7 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - couldn't parse revision, input="README.md" Caused by: - 0: Reference README.md could not be found + 0: Reference "README.md" could not be found 1: The ref partially named "README.md" could not be found "#); @@ -202,7 +203,7 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - ); assert_eq!( missing_reference_names(&err), - [std::path::Path::new("README.md")], + [gix::bstr::BStr::new("README.md")], "the parser exposes the unresolved reference name for typed path fallback" ); let not_found = err @@ -211,7 +212,7 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - assert_eq!( not_found.name, - std::path::Path::new("README.md"), + gix::bstr::BStr::new("README.md"), "the missing reference carries the unresolved revspec for path fallback" ); @@ -220,9 +221,8 @@ fn missing_revision_keeps_reference_lookup_error_available_for_path_fallback() - #[cfg(unix)] #[test] -fn non_utf8_missing_reference_names_are_preserved() -> Result { +fn non_utf8_missing_reference_names_are_preserved() -> TestResult { use gix::bstr::ByteSlice; - use std::os::unix::ffi::OsStrExt; let (repo, _keep) = crate::basic_rw_repo()?; std::fs::write( @@ -243,15 +243,14 @@ fn non_utf8_missing_reference_names_are_preserved() -> Result { ); assert_eq!( missing_reference_names(&err), - [std::path::Path::new(std::ffi::OsStr::from_bytes(expected_name))], + [expected_name.as_bstr()], "the parser preserves the missing name's bytes for direct and symbolic lookups" ); assert_eq!( err.downcast_any_ref::<gix::refs::file::find::NotFound>() .expect("the original non-UTF-8 lookup failure remains available") .name - .as_os_str() - .as_bytes(), + .as_slice(), expected_name, "the original lookup failure also preserves the missing name's bytes" ); @@ -260,7 +259,7 @@ fn non_utf8_missing_reference_names_are_preserved() -> Result { } #[test] -fn missing_symbolic_referents_keep_their_name() -> Result { +fn missing_symbolic_referents_keep_their_name() -> TestResult { let mut error_snapshots = Vec::new(); let (repo, _keep) = crate::basic_rw_repo()?; std::fs::write(repo.git_dir().join("refs/heads/alias"), b"ref: refs/heads/missing\n")?; @@ -274,14 +273,14 @@ fn missing_symbolic_referents_keep_their_name() -> Result { ); assert_eq!( missing_reference_names(&err), - [std::path::Path::new("refs/heads/missing")], + [gix::bstr::BStr::new("refs/heads/missing")], "{revspec} exposes the missing symbolic referent rather than the input revspec" ); assert_eq!( err.downcast_any_ref::<gix::refs::file::find::NotFound>() .expect("the missing referent remains available for path fallback") .name, - std::path::Path::new("refs/heads/missing"), + gix::bstr::BStr::new("refs/heads/missing"), "the missing reference name is not necessarily the input revspec" ); } @@ -291,31 +290,31 @@ fn missing_symbolic_referents_keep_their_name() -> Result { Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Reference refs/heads/missing could not be found + 1: Reference "refs/heads/missing" could not be found 2: The ref partially named "refs/heads/missing" could not be found, The rev-spec is malformed and misses a ref name Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Reference refs/heads/missing could not be found + 1: Reference "refs/heads/missing" could not be found 2: The ref partially named "refs/heads/missing" could not be found, The rev-spec is malformed and misses a ref name Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Reference refs/heads/missing could not be found + 1: Reference "refs/heads/missing" could not be found 2: The ref partially named "refs/heads/missing" could not be found, The rev-spec is malformed and misses a ref name Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Reference refs/heads/missing could not be found + 1: Reference "refs/heads/missing" could not be found 2: The ref partially named "refs/heads/missing" could not be found, The rev-spec is malformed and misses a ref name Caused by: 0: Could not peel 'refs/heads/alias' to obtain its target - 1: Reference refs/heads/missing could not be found + 1: Reference "refs/heads/missing" could not be found 2: The ref partially named "refs/heads/missing" could not be found, ] "#); @@ -323,7 +322,7 @@ fn missing_symbolic_referents_keep_their_name() -> Result { } #[test] -fn both_missing_symbolic_referents_are_retained() -> Result { +fn both_missing_symbolic_referents_are_retained() -> TestResult { let (repo, _keep) = crate::basic_rw_repo()?; std::fs::write( repo.git_dir().join("refs/heads/first"), @@ -345,13 +344,13 @@ fn both_missing_symbolic_referents_are_retained() -> Result { let missing_names: Vec<_> = err .iter_errors() .filter_map(|cause| cause.downcast_ref::<gix::refs::file::find::NotFound>()) - .map(|cause| cause.name.as_path()) + .map(|cause| cause.name.as_bstr()) .collect(); assert_eq!( missing_names, [ - std::path::Path::new("refs/heads/missing-first"), - std::path::Path::new("refs/heads/missing-second"), + gix::bstr::BStr::new("refs/heads/missing-first"), + gix::bstr::BStr::new("refs/heads/missing-second"), ], "final spec conversion preserves both lookup failures" ); @@ -365,7 +364,7 @@ fn both_missing_symbolic_referents_are_retained() -> Result { } #[test] -fn missing_objects_are_classified_without_a_missing_reference() -> Result { +fn missing_objects_are_classified_without_a_missing_reference() -> TestResult { let mut error_snapshots = Vec::new(); let (repo, _keep) = crate::basic_rw_repo()?; let mut missing_commit_id = repo.object_hash().null(); @@ -419,7 +418,7 @@ fn missing_objects_are_classified_without_a_missing_reference() -> Result { } #[test] -fn missing_tree_and_index_paths_are_not_missing_references() -> Result { +fn missing_tree_and_index_paths_are_not_missing_references() -> TestResult { let repo = repo("complex_graph")?; repo.rev_parse("HEAD:file")?; repo.rev_parse(":file")?; @@ -527,7 +526,7 @@ fn invalid_head() { Caused by: 0: Could not peel 'HEAD' to obtain its target - ├─0: Reference refs/heads/main could not be found + ├─0: Reference "refs/heads/main" could not be found │ └─0: The ref partially named "refs/heads/main" could not be found └─1: Couldn't get object at internal index 0 "#); @@ -542,7 +541,7 @@ fn invalid_head() { Caused by: 0: Could not peel 'HEAD' to obtain its target - 1: Reference refs/heads/main could not be found + 1: Reference "refs/heads/main" could not be found 2: The ref partially named "refs/heads/main" could not be found "#); } diff --git a/gix/tests/gix/revision/spec/from_bytes/reflog.rs b/gix/tests/gix/revision/spec/from_bytes/reflog.rs index 4f8db1fcadb..289bc083fb2 100644 --- a/gix/tests/gix/revision/spec/from_bytes/reflog.rs +++ b/gix/tests/gix/revision/spec/from_bytes/reflog.rs @@ -1,5 +1,5 @@ -use crate::Result; use gix::{prelude::ObjectIdExt, revision::Spec}; +use gix_testtools::TestResult; use crate::{ revision::spec::from_bytes::{parse_spec, parse_spec_no_baseline, repo}, @@ -7,12 +7,12 @@ use crate::{ }; #[test] -fn symbolic_references_use_their_own_log_or_the_final_targets() -> gix_error::TestResult { +fn symbolic_references_use_their_own_log_or_the_final_targets() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_read_only("make_symbolic_ref_reflogs.sh")?; let repo = gix::open_opts(fixture, crate::restricted())?; for name in ["refs/symref", "refs/symref-chain"] { assert!( - !repo.find_reference(name)?.log_exists(), + !repo.find_reference(name)?.log_exists()?, "{name} must exercise fallback to the final target's reflog" ); for query in ["0", "1", "2", "1979-02-26 00:00:00 +0000"] { @@ -25,7 +25,7 @@ fn symbolic_references_use_their_own_log_or_the_final_targets() -> gix_error::Te } } assert!( - repo.find_reference("refs/heads/symref")?.log_exists(), + repo.find_reference("refs/heads/symref")?.log_exists()?, "a symbolic branch has its own reflog" ); assert!( @@ -59,7 +59,7 @@ fn nth_prior_checkout() { } #[test] -fn nth_prior_checkout_to_deleted_branch_fails_like_git() -> Result { +fn nth_prior_checkout_to_deleted_branch_fails_like_git() -> TestResult { let repo = repo("deleted_prior_checkout")?; let err = parse_spec("@{-1}", &repo).expect_err("deleted prior checkout branch must not resolve by object id"); insta::assert_debug_snapshot!(err.probable_cause(), "error should explain that the reflog name no longer resolves", @r#" @@ -71,7 +71,7 @@ fn nth_prior_checkout_to_deleted_branch_fails_like_git() -> Result { } #[test] -fn nth_prior_checkout_to_deleted_branch_named_like_object_matches_git() -> Result { +fn nth_prior_checkout_to_deleted_branch_named_like_object_matches_git() -> TestResult { let repo = repo("deleted_prior_checkout_named_like_object")?; assert_eq!( parse_spec("@{-1}", &repo)?, diff --git a/gix/tests/gix/revision/spec/from_bytes/regex.rs b/gix/tests/gix/revision/spec/from_bytes/regex.rs index 4121a88d77e..5f77653aca9 100644 --- a/gix/tests/gix/revision/spec/from_bytes/regex.rs +++ b/gix/tests/gix/revision/spec/from_bytes/regex.rs @@ -73,11 +73,11 @@ mod with_known_revision { mod empty_pattern { use super::*; - use crate::Result; use crate::revision::spec::from_bytes::{parse_spec, repo}; + use gix_testtools::TestResult; #[test] - fn matches_everything_and_peels_to_a_commit() -> Result { + fn matches_everything_and_peels_to_a_commit() -> TestResult { let repo = repo("complex_graph")?; assert_eq!( diff --git a/gix/tests/gix/revision/spec/from_bytes/traverse.rs b/gix/tests/gix/revision/spec/from_bytes/traverse.rs index 7221db0a475..dccf925be86 100644 --- a/gix/tests/gix/revision/spec/from_bytes/traverse.rs +++ b/gix/tests/gix/revision/spec/from_bytes/traverse.rs @@ -1,5 +1,5 @@ -use crate::Result; use gix::{prelude::ObjectIdExt, revision::Spec}; +use gix_testtools::TestResult; use crate::{ revision::spec::from_bytes::{parse_spec, parse_spec_no_baseline, repo}, @@ -7,7 +7,7 @@ use crate::{ }; #[test] -fn complex() -> Result { +fn complex() -> TestResult { let repo = &repo("complex_graph")?; assert_eq!(parse_spec("b", repo)?, parse_spec("a~1", repo)?); @@ -23,7 +23,7 @@ fn complex() -> Result { } #[test] -fn freestanding_negation_yields_descriptive_error() -> Result { +fn freestanding_negation_yields_descriptive_error() -> TestResult { let mut error_snapshots = Vec::new(); let repo = repo("complex_graph")?; for revspec in ["^^", "^^HEAD"] { @@ -42,7 +42,7 @@ fn freestanding_negation_yields_descriptive_error() -> Result { couldn't parse revision, input="!" Caused by: - 0: Reference ! could not be found + 0: Reference "!" could not be found 1: The ref partially named "!" could not be found "#); assert!(err.is_not_found(), "the missing anchor reference remains classified"); @@ -59,7 +59,7 @@ fn freestanding_negation_yields_descriptive_error() -> Result { Ok(()) } #[test] -fn freestanding_double_or_triple_dot_defaults_to_head_refs() -> Result { +fn freestanding_double_or_triple_dot_defaults_to_head_refs() -> TestResult { let repo = repo("complex_graph")?; assert_eq!( parse_spec_no_baseline("..", &repo)?, // git can't communicate what it does here @@ -85,7 +85,7 @@ fn parent() { } #[test] -fn tags_navigate_from_their_commit() -> Result { +fn tags_navigate_from_their_commit() -> TestResult { let repo = repo("complex_graph")?; for (spec, expected) in [ ("b-tag^", "d"), diff --git a/gix/tests/gix/status.rs b/gix/tests/gix/status.rs index 16890c6fbf8..ad2f61c1b5a 100644 --- a/gix/tests/gix/status.rs +++ b/gix/tests/gix/status.rs @@ -18,9 +18,9 @@ pub fn repo(name: &str) -> Result<gix::Repository> { } mod into_iter { - use crate::Result; use gix::status::{Item, Submodule, tree_index::TrackRenames}; use gix_diff::Rewrites; + use gix_testtools::TestResult; use gix_testtools::size_ok; use crate::status::{repo, submodule_repo}; @@ -38,7 +38,7 @@ mod into_iter { } #[test] - fn submodule_tree_index_modification() -> Result { + fn submodule_tree_index_modification() -> TestResult { let repo = submodule_repo("git-mv-and-untracked-and-submodule-head-changed-and-modified")?; let mut status = repo .status(gix::progress::Discard)? @@ -97,7 +97,7 @@ mod into_iter { } #[test] - fn submodule_fully_ignored_by_override() -> Result { + fn submodule_fully_ignored_by_override() -> TestResult { let repo = submodule_repo("git-mv-and-untracked-and-submodule-head-changed-and-modified")?; let mut status = repo .status(gix::progress::Discard)? @@ -163,7 +163,7 @@ mod into_iter { Ok(()) } #[test] - fn submodule_fully_ignored_by_configuration() -> Result { + fn submodule_fully_ignored_by_configuration() -> TestResult { let repo = submodule_repo("git-mv-and-untracked-and-submodule-head-changed-and-modified-ignore-all")?; let mut status = repo .status(gix::progress::Discard)? @@ -204,7 +204,7 @@ mod into_iter { } #[test] - fn tree_index_modification_worktree_modification_racy_git() -> Result { + fn tree_index_modification_worktree_modification_racy_git() -> TestResult { let repo = repo("racy-git")?; let mut status = repo.status(gix::progress::Discard)?.into_iter(None)?; let mut items: Vec<_> = status.by_ref().filter_map(std::result::Result::ok).collect(); @@ -214,7 +214,7 @@ mod into_iter { } #[test] - fn untracked_unborn() -> Result { + fn untracked_unborn() -> TestResult { let repo = repo("untracked-unborn")?; let mut status = repo.status(gix::progress::Discard)?.into_iter(None)?; let mut items: Vec<_> = status.by_ref().filter_map(std::result::Result::ok).collect(); @@ -244,7 +244,7 @@ mod into_iter { } #[test] - fn added_unborn() -> Result { + fn added_unborn() -> TestResult { let repo = repo("added-unborn")?; let mut status = repo.status(gix::progress::Discard)?.into_iter(None)?; let mut items: Vec<_> = status.by_ref().filter_map(std::result::Result::ok).collect(); @@ -278,7 +278,7 @@ mod into_iter { } #[test] - fn untracked_added() -> Result { + fn untracked_added() -> TestResult { let repo = repo("untracked-added")?; let mut status = repo.status(gix::progress::Discard)?.into_iter(None)?; let mut items: Vec<_> = status.by_ref().filter_map(std::result::Result::ok).collect(); @@ -303,7 +303,7 @@ mod into_iter { #[test] #[cfg(unix)] - fn submodule_assume_unchanged_replaced_with_symlink_is_ignored() -> Result { + fn submodule_assume_unchanged_replaced_with_symlink_is_ignored() -> TestResult { let repo = repo("submodule-assume-unchanged-symlink")?; let mut status = repo.status(gix::progress::Discard)?.into_iter(None)?; let items: Vec<_> = status.by_ref().filter_map(std::result::Result::ok).collect(); @@ -315,7 +315,7 @@ mod into_iter { } #[test] - fn uninitialized_submodule_with_files_is_clean() -> Result { + fn uninitialized_submodule_with_files_is_clean() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_uninitialized_submodule.sh")?.join("uninitialized"); assert_eq!( gix_testtools::git(&root, "--no-optional-locks status --porcelain=v1 --untracked-files=all")?, @@ -343,7 +343,7 @@ mod into_iter { #[test] #[cfg_attr(not(target_os = "macos"), ignore = "Needs Git's macOS Unicode precomposition")] - fn emoji_with_decomposed_unicode_matches_git() -> Result { + fn emoji_with_decomposed_unicode_matches_git() -> TestResult { let root = gix_testtools::scripted_fixture_read_only_needs_archive("make_emoji_with_decomposed_unicode.sh")?; assert_eq!( gix_testtools::git(&root, "--no-optional-locks status --porcelain=v1 --untracked-files=all")?, @@ -364,7 +364,7 @@ mod into_iter { } #[test] - fn error_during_tree_traversal_causes_failure() -> Result { + fn error_during_tree_traversal_causes_failure() -> TestResult { let mut error_snapshots = Vec::new(); let repo = repo("untracked-only")?; let invalid_tree_id = repo.object_hash().empty_blob(); @@ -404,8 +404,8 @@ mod into_iter { mod index_worktree { mod iter { - use crate::Result; use gix::status::index_worktree::Item; + use gix_testtools::TestResult; use gix_testtools::size_ok; use pretty_assertions::assert_eq; @@ -424,7 +424,7 @@ mod index_worktree { } #[test] - fn submodule_modification() -> Result { + fn submodule_modification() -> TestResult { let repo = submodule_repo("modified-untracked-and-submodule-head-changed-and-modified")?; let mut status = repo .status(gix::progress::Discard)? @@ -439,7 +439,7 @@ mod index_worktree { } #[test] - fn untracked_files_collapse_by_default() -> Result { + fn untracked_files_collapse_by_default() -> TestResult { let repo = repo("untracked-only")?; let status = repo .status(gix::progress::Discard)? @@ -482,7 +482,7 @@ mod index_worktree { } #[test] - fn untracked_files_settings_none() -> Result { + fn untracked_files_settings_none() -> TestResult { let mut repo = repo("untracked-only")?; repo.config_snapshot_mut() .set_value(&gix::config::tree::Status::SHOW_UNTRACKED_FILES, "no")?; @@ -505,7 +505,7 @@ mod index_worktree { } #[test] - fn early_drop_for_is_dirty_emulation() -> Result { + fn early_drop_for_is_dirty_emulation() -> TestResult { let repo = submodule_repo("modified-untracked-and-submodule-head-changed-and-modified")?; let is_dirty = repo .status(gix::progress::Discard)? @@ -523,7 +523,7 @@ mod index_worktree { #[test] #[cfg(unix)] // symlinks are used here, let's not try our luck on Windows. - fn tracked_entries_under_a_symlinked_directory_are_reported_as_removed() -> Result { + fn tracked_entries_under_a_symlinked_directory_are_reported_as_removed() -> TestResult { let repo = repo("symlink-replaces-tracked-dir")?; let mut items: Vec<_> = repo .status(gix::progress::Discard)? @@ -582,25 +582,25 @@ mod index_worktree { } mod is_dirty { - use crate::Result; use crate::status::{repo, submodule_repo}; + use gix_testtools::TestResult; #[test] - fn various_changes_positive() -> Result { + fn various_changes_positive() -> TestResult { let repo = submodule_repo("modified-untracked-and-submodule-head-changed-and-modified")?; assert!(repo.is_dirty()?, "The repository has various changes"); Ok(()) } #[test] - fn submodule_changes_are_picked_up() -> Result { + fn submodule_changes_are_picked_up() -> TestResult { let repo = submodule_repo("submodule-head-changed")?; assert!(repo.is_dirty()?, "head-changes are also discovered"); Ok(()) } #[test] - fn untracked_files_are_excluded() -> Result { + fn untracked_files_are_excluded() -> TestResult { let repo = submodule_repo("module1")?; assert_eq!( repo.status(gix::progress::Discard)? @@ -617,7 +617,7 @@ mod is_dirty { } #[test] - fn unborn_head_is_not_an_error() -> Result { + fn unborn_head_is_not_an_error() -> TestResult { let repo = repo("untracked-unborn")?; assert!( @@ -628,7 +628,7 @@ mod is_dirty { } #[test] - fn added_files_in_unborn_head_are_dirty() -> Result { + fn added_files_in_unborn_head_are_dirty() -> TestResult { let repo = repo("added-unborn")?; assert!( @@ -639,7 +639,7 @@ mod is_dirty { } #[test] - fn index_changed() -> Result { + fn index_changed() -> TestResult { let repo = repo("git-mv")?; assert!( repo.is_dirty()?, diff --git a/gix/tests/gix/submodule.rs b/gix/tests/gix/submodule.rs index a4a2ec76ff6..279877b9a92 100644 --- a/gix/tests/gix/submodule.rs +++ b/gix/tests/gix/submodule.rs @@ -9,7 +9,7 @@ pub fn repo(name: &str) -> Result<gix::Repository> { } mod open { - use crate::Result; + use gix_testtools::TestResult; use std::io::Write; use gix_sec::Trust; @@ -19,7 +19,7 @@ mod open { use crate::{submodule::repo, util::named_subrepo_opts}; #[test] - fn empty_worktree_config_is_not_an_uninitialized_submodule() -> Result { + fn empty_worktree_config_is_not_an_uninitialized_submodule() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_submodule_with_worktree.sh")?; let repo = gix::open_opts( fixture.path().join("submodule-with-extra-worktree-host"), @@ -63,7 +63,7 @@ mod open { } #[test] - fn various() -> Result { + fn various() -> TestResult { for (name, expected) in [ ( "with-submodules", @@ -176,7 +176,7 @@ mod open { } #[test] - fn absolute_looking_names_remain_below_the_modules_directory() -> Result { + fn absolute_looking_names_remain_below_the_modules_directory() -> TestResult { let repo = repo("absolute-looking-submodule-names")?; let modules_dir = repo.common_dir().join("modules"); let mut count = 0; @@ -199,7 +199,7 @@ mod open { /// `git_dir_trust` from the parent repository because doing so skips recomputing /// trust for the submodule git-dir and can bypass ownership-based trust checks. #[test] - fn trust_is_recomputed_for_opened_submodules() -> Result { + fn trust_is_recomputed_for_opened_submodules() -> TestResult { let repo = named_subrepo_opts( "make_submodules.sh", "with-submodules", @@ -226,7 +226,7 @@ mod open { } #[test] - fn gitlink_target_takes_precedence_over_name_in_git_dir_resolution() -> Result { + fn gitlink_target_takes_precedence_over_name_in_git_dir_resolution() -> TestResult { let repo = repo("submodule-with-divergent-gitlink")?; let sm = repo .submodules()? @@ -290,7 +290,7 @@ mod open { } #[test] - fn status_uses_detached_worktree_from_symlinked_git_dir() -> Result { + fn status_uses_detached_worktree_from_symlinked_git_dir() -> TestResult { let root = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?.join("linked-git-dir-detached-worktree"); let repo = gix::open_opts(root.join("home"), gix::open::Options::isolated())?; @@ -327,7 +327,7 @@ mod open { #[test] #[cfg(unix)] // symlinks are used here, let's not try our luck on Windows. - fn keeps_callers_path_namespace_when_opened_through_symlinked_ancestor() -> Result { + fn keeps_callers_path_namespace_when_opened_through_symlinked_ancestor() -> TestResult { let link = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?.join("symlinked-ancestor"); for parent_name in ["with-submodules", "with-submodule-uninitialized-checkout"] { @@ -347,7 +347,7 @@ mod open { ); assert_eq!( sm_repo.workdir(), - Some(worktree.join(gix_path::from_bstr(sm.path()?).as_ref())).as_deref(), + Some(worktree.join(gix_path::from_bstr(sm.path()?)?.as_ref())).as_deref(), "the submodule workdir stays in the same namespace instead of being canonicalized, \ so it can be related to the parent worktree and the submodule git dir with prefix logic" ); @@ -358,7 +358,7 @@ mod open { } #[test] - fn broken_gitlink_target_is_reported() -> Result { + fn broken_gitlink_target_is_reported() -> TestResult { let mut error_snapshots = Vec::new(); let repo = repo("submodule-with-missing-gitlink-target")?; let work_dir = repo.workdir().expect("fixture has a worktree"); @@ -415,7 +415,7 @@ mod open { } #[test] - fn malformed_gitlink_target_is_ignored_by_ignore_all_status() -> Result { + fn malformed_gitlink_target_is_ignored_by_ignore_all_status() -> TestResult { let repo = repo("submodule-with-malformed-gitlink")?; let work_dir = repo.workdir().expect("fixture has a worktree"); let sm = repo @@ -464,11 +464,11 @@ mod open { #[cfg(feature = "status")] mod status { - use crate::Result; use crate::{submodule::repo, util::hex_to_id}; + use gix_testtools::TestResult; #[test] - fn changed_head_compared_to_superproject_index() -> Result { + fn changed_head_compared_to_superproject_index() -> TestResult { let repo = repo("submodule-head-changed")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); let mut status = sm.status(gix::submodule::config::Ignore::None, false)?; @@ -524,7 +524,7 @@ mod open { } #[test] - fn modified_in_index_only() -> Result { + fn modified_in_index_only() -> TestResult { let mut repo: gix::Repository = repo("submodule-index-changed")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); @@ -578,7 +578,7 @@ mod open { } #[test] - fn modified_and_untracked() -> Result { + fn modified_and_untracked() -> TestResult { let repo = repo("modified-and-untracked")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); @@ -635,7 +635,7 @@ mod open { } #[test] - fn changed_head_empty_worktree() -> Result { + fn changed_head_empty_worktree() -> TestResult { let repo = repo("submodule-head-changed-no-worktree")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); @@ -668,7 +668,7 @@ mod open { } #[test] - fn is_dirty_skips_expensive_checks() -> Result { + fn is_dirty_skips_expensive_checks() -> TestResult { let repo = repo("submodule-head-changed-and-modified")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); @@ -692,7 +692,7 @@ mod open { } #[test] - fn not_a_submodule() -> Result { + fn not_a_submodule() -> TestResult { let repo = repo("not-a-submodule")?; let sm = repo.submodules()?.into_iter().flatten().next().expect("one submodule"); assert!(sm.open()?.is_some(), "repo available as it was cloned"); @@ -702,7 +702,7 @@ mod open { } #[test] - fn in_unborn() -> Result { + fn in_unborn() -> TestResult { let repo = repo("unborn")?; assert_eq!( repo.submodules()?.into_iter().flatten().count(), @@ -714,7 +714,7 @@ mod open { #[test] #[cfg(feature = "revision")] - fn submodule_worktrees() -> Result { + fn submodule_worktrees() -> TestResult { let sm_repo = crate::util::named_subrepo_opts( "make_submodule_with_worktree.sh", "worktree-of-submodule", @@ -736,7 +736,7 @@ mod open { #[test] #[cfg(feature = "revision")] - fn list_submodule_worktrees() -> Result { + fn list_submodule_worktrees() -> TestResult { let sm_repo = crate::util::named_subrepo_opts( "make_submodule_with_worktree.sh", "submodule-with-extra-worktree-host/m1", @@ -757,7 +757,7 @@ mod open { } #[test] - fn old_form() -> Result { + fn old_form() -> TestResult { for name in ["old-form-invalid-worktree-path", "old-form"] { let repo = repo(name)?; let sm = repo @@ -793,14 +793,14 @@ mod open { #[cfg(unix)] mod advisory { - use crate::Result; + use gix_testtools::TestResult; /// Reproducer for GHSA-p3hw-mv63-rf9w and GHSA-fr8x-3vfx-f45h: a crafted submodule name with /// traversal components is reused to derive `.git/modules/<name>`, so `Submodule::state()` and /// `Submodule::open()` can be redirected to another repository outside the intended modules /// directory. #[test] - fn traversal_names_do_not_escape_the_modules_directory() -> Result { + fn traversal_names_do_not_escape_the_modules_directory() -> TestResult { let mut error_snapshots = Vec::new(); let fixture = gix_testtools::scripted_fixture_writable("make_submodule_traversal_advisory.sh")?; let repo_dir = fixture.path().join("victim-repo"); @@ -862,7 +862,7 @@ mod advisory { /// after `git submodule init`: `gix` must reject them from `.gitmodules` instead of exposing /// them as executable updates. #[test] - fn update_commands_from_gitmodules_are_rejected_after_init() -> Result { + fn update_commands_from_gitmodules_are_rejected_after_init() -> TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_submodule_update_advisory.sh")?; let victim = fixture.path().join("victim"); diff --git a/gix/tests/interrupt.rs b/gix/tests/interrupt.rs index 4a971e7c027..3fd19153fbf 100644 --- a/gix/tests/interrupt.rs +++ b/gix/tests/interrupt.rs @@ -1,6 +1,6 @@ use gix::error::retryable; #[test] -fn iterator_errors_use_the_crate_result() -> gix_testtools::Result { +fn iterator_errors_use_the_crate_result() -> gix_testtools::TestResult { if gix_testtools::run_in_isolated_process()? { return Ok(()); } @@ -29,7 +29,7 @@ fn iterator_errors_use_the_crate_result() -> gix_testtools::Result { } #[test] -fn iterator_errors_accept_exceptions_and_preserve_retry_classification() -> gix_testtools::Result { +fn iterator_errors_accept_exceptions_and_preserve_retry_classification() -> gix_testtools::TestResult { use gix::error::ErrorExt; if gix_testtools::run_in_isolated_process()? { @@ -68,7 +68,7 @@ mod needs_feature { use signal_hook::consts::SIGTERM; #[test] - fn multi_registration() -> gix_testtools::Result { + fn multi_registration() -> gix_testtools::TestResult { static V1: AtomicUsize = AtomicUsize::new(0); static V2: AtomicBool = AtomicBool::new(false); @@ -77,22 +77,20 @@ mod needs_feature { gix::interrupt::init_handler(3, || { V1.fetch_add(1, Ordering::SeqCst); }) - } - .expect("succeeds"); + }?; assert!(!gix::interrupt::is_triggered()); assert_eq!(V1.load(Ordering::Relaxed), 0); // SAFETY: The closure doesn't use mutexes or memory allocation, so it should be safe to call from a signal handler. - let reg2 = unsafe { gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)) } - .expect("multi-initialization is OK"); + let reg2 = unsafe { gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)) }?; assert!(!V2.load(Ordering::Relaxed)); - signal_hook::low_level::raise(SIGTERM).expect("signal can be raised"); + signal_hook::low_level::raise(SIGTERM)?; assert!(gix::interrupt::is_triggered(), "this happens automatically"); assert_eq!(V1.load(Ordering::Relaxed), 1, "the first trigger is invoked"); assert!(!V2.load(Ordering::Relaxed), "the second trigger was ignored"); reg1.deregister()?; - signal_hook::low_level::raise(SIGTERM).expect("signal can be raised"); + signal_hook::low_level::raise(SIGTERM)?; assert_eq!(V1.load(Ordering::Relaxed), 2, "the first trigger is still invoked"); assert!(gix::interrupt::is_triggered(), "this happens automatically"); @@ -108,15 +106,13 @@ mod needs_feature { gix::interrupt::init_handler(3, || { V1.fetch_add(1, Ordering::SeqCst); }) - } - .expect("succeeds"); + }?; assert_eq!(V1.load(Ordering::Relaxed), 2, "nothing changed yet"); // SAFETY: The closure doesn't use mutexes or memory allocation, so it should be safe to call from a signal handler. - let reg2 = unsafe { gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)) } - .expect("multi-initialization is OK"); + let reg2 = unsafe { gix::interrupt::init_handler(2, || V2.store(true, Ordering::SeqCst)) }?; assert!(!V2.load(Ordering::Relaxed)); - signal_hook::low_level::raise(SIGTERM).expect("signal can be raised"); + signal_hook::low_level::raise(SIGTERM)?; assert_eq!(V1.load(Ordering::Relaxed), 3, "the first trigger is invoked"); assert!(!V2.load(Ordering::Relaxed), "the second trigger was ignored"); diff --git a/gix/tests/repository.rs b/gix/tests/repository.rs index ddec71fda3a..740713a3407 100644 --- a/gix/tests/repository.rs +++ b/gix/tests/repository.rs @@ -6,7 +6,7 @@ use serial_test::serial; #[test] #[serial] -fn config_path_uses_repository_options_for_global_sources() -> gix_testtools::Result { +fn config_path_uses_repository_options_for_global_sources() -> gix_testtools::TestResult { let mut diagnostics = Vec::new(); use gix::config::Source; @@ -99,7 +99,7 @@ fn config_path_uses_repository_options_for_global_sources() -> gix_testtools::Re #[test] #[serial] -fn config_paths_use_the_opening_cwd() -> gix_testtools::Result { +fn config_paths_use_the_opening_cwd() -> gix_testtools::TestResult { use gix::config::Source; let fixture = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_config_repos.sh")?)?; @@ -137,7 +137,7 @@ fn config_paths_use_the_opening_cwd() -> gix_testtools::Result { #[test] #[serial] -fn config_file_paths_use_the_cwd_captured_while_opening() -> gix_testtools::Result { +fn config_file_paths_use_the_cwd_captured_while_opening() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_config_repo.sh")?; let elsewhere = gix_testtools::tempfile::tempdir()?; let _cwd = gix_testtools::set_current_dir(fixture.path())?; @@ -164,7 +164,7 @@ fn config_file_paths_use_the_cwd_captured_while_opening() -> gix_testtools::Resu #[test] #[serial] #[cfg(target_os = "macos")] -fn config_file_paths_follow_a_precomposed_opening_cwd() -> gix_testtools::Result { +fn config_file_paths_follow_a_precomposed_opening_cwd() -> gix_testtools::TestResult { let tmp = gix_testtools::tempfile::tempdir()?; let decomposed = tmp.path().join("a\u{308}"); std::fs::create_dir(&decomposed)?; @@ -204,7 +204,7 @@ fn config_file_paths_follow_a_precomposed_opening_cwd() -> gix_testtools::Result #[test] #[serial] -fn relative_paths_use_the_cwd_captured_when_opening() -> gix_testtools::Result { +fn relative_paths_use_the_cwd_captured_when_opening() -> gix_testtools::TestResult { let root = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?)?; let nested = root.join("some/very"); @@ -257,13 +257,13 @@ fn relative_paths_use_the_cwd_captured_when_opening() -> gix_testtools::Result { #[test] #[serial] -fn paths_cannot_leave_the_repository() -> gix_testtools::Result { +fn paths_cannot_leave_the_repository() -> gix_testtools::TestResult { let root = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?)?; let nested = root.join("some"); let _cwd = gix_testtools::set_current_dir(&nested)?; let repo = gix::discover_opts(".", Default::default(), gix::open::Options::isolated())?; - let absolute = gix::path::into_bstr(root.join("some-with-file/very/deeply/nested/subdir/empty-file")); + let absolute = gix::path::into_bstr(root.join("some-with-file/very/deeply/nested/subdir/empty-file"))?; assert_eq!( repo.normalize_path(&absolute)?.as_bstr(), "some-with-file/very/deeply/nested/subdir/empty-file", @@ -290,11 +290,11 @@ fn paths_cannot_leave_the_repository() -> gix_testtools::Result { #[test] #[serial] -fn absolute_paths_outside_the_repository_are_rejected() -> gix_testtools::Result { +fn absolute_paths_outside_the_repository_are_rejected() -> gix_testtools::TestResult { let root = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?)?; let repo = gix::discover_opts(&root, Default::default(), gix::open::Options::isolated())?; let outside = root.parent().expect("fixture has a parent").to_owned(); - let outside_as_bstr = gix::path::into_bstr(outside.clone()); + let outside_as_bstr = gix::path::into_bstr(outside.clone())?; let err = repo .normalize_path(&outside_as_bstr) @@ -319,7 +319,7 @@ fn absolute_paths_outside_the_repository_are_rejected() -> gix_testtools::Result #[test] #[cfg(feature = "status")] #[serial] -fn is_dirty_sees_index_changes_outside_the_current_working_directory() -> gix_testtools::Result { +fn is_dirty_sees_index_changes_outside_the_current_working_directory() -> gix_testtools::TestResult { let root = gix::path::realpath( gix_testtools::scripted_fixture_read_only("make_status_repos.sh")?.join("index-changed-outside-subdir"), )?; @@ -336,7 +336,7 @@ fn is_dirty_sees_index_changes_outside_the_current_working_directory() -> gix_te #[test] #[cfg(feature = "revision")] #[serial] -fn revspec_paths_starting_with_a_dot_are_relative_to_the_current_directory() -> gix_testtools::Result { +fn revspec_paths_starting_with_a_dot_are_relative_to_the_current_directory() -> gix_testtools::TestResult { let root = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?)?; let nested = root.join("some/very"); @@ -385,7 +385,7 @@ fn revspec_paths_starting_with_a_dot_are_relative_to_the_current_directory() -> #[test] #[cfg(feature = "revision")] #[serial] -fn revspec_paths_starting_with_a_dot_need_a_worktree_to_stay_within() -> gix_testtools::Result { +fn revspec_paths_starting_with_a_dot_need_a_worktree_to_stay_within() -> gix_testtools::TestResult { let root = gix::path::realpath(gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?)?; let _cwd = gix_testtools::set_current_dir(&root)?; @@ -420,7 +420,7 @@ fn revspec_paths_starting_with_a_dot_need_a_worktree_to_stay_within() -> gix_tes #[test] #[serial] -fn open_options_preset_system_config_paths_avoid_running_git() -> gix_testtools::Result { +fn open_options_preset_system_config_paths_avoid_running_git() -> gix_testtools::TestResult { let temp = tempfile::tempdir()?; let git_dir = temp.path().join("repo.git"); fs::create_dir_all(git_dir.join("objects"))?; diff --git a/gix/tests/submodule_open.rs b/gix/tests/submodule_open.rs index 85015e3f84e..e6f1d59f939 100644 --- a/gix/tests/submodule_open.rs +++ b/gix/tests/submodule_open.rs @@ -5,7 +5,7 @@ windows, ignore = "for now, windows gets Os { code: 267, kind: NotADirectory, message: 'The directory name is invalid.' } (maybe because of relative symlink)?" )] -fn on_nested_symlink() -> gix_testtools::Result { +fn on_nested_symlink() -> gix_testtools::TestResult { let symlink_root = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?.join("link-to-dir-in-changed-parent-repo"); // Note: even though this refers to a symlink, the CWD that is actually set will be the resolved directory. diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index a62db971be5..40590f01b14 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -1434,7 +1434,7 @@ mod tests { #[test] #[cfg(feature = "tix")] - fn tix_aliases_are_visible_and_route_to_tix() { + fn tix_aliases_are_visible_and_route_to_tix() -> gix::error::TestResult { use clap::{CommandFactory, Parser}; let command = Args::command(); @@ -1445,7 +1445,7 @@ mod tests { "all aliases are shown in help" ); for name in ["tix", "tui", "interactive", "i"] { - let args = Args::try_parse_from(["gix", name]).expect("the command or alias parses"); + let args = Args::try_parse_from(["gix", name])?; assert!( matches!(args.cmd, Subcommands::Tix(_)), "{name} routes to the tix command" @@ -1454,8 +1454,7 @@ mod tests { #[cfg(feature = "tracing")] { - let args = Args::try_parse_from(["gix", "-tt", "tix"]) - .expect("the outer parser accepts tracing even though tix manages its own diagnostics"); + let args = Args::try_parse_from(["gix", "-tt", "tix"])?; assert_eq!(args.trace, 2); assert_eq!( Args::try_parse_from(["gix", "tix", "-t"]) @@ -1471,10 +1470,7 @@ mod tests { vec!["gix", "tix", "spill"], ] { assert!( - matches!( - Args::try_parse_from(arguments).expect("shared tix arguments parse").cmd, - Subcommands::Tix(_) - ), + matches!(Args::try_parse_from(arguments)?.cmd, Subcommands::Tix(_)), "the complete tix command is delegated" ); } @@ -1482,8 +1478,7 @@ mod tests { (vec!["gix", "tix", "worktrunk"], true), (vec!["gix", "tix", "worktrunk", "shell-init", "bash"], false), ] { - let Subcommands::Tix(command) = Args::try_parse_from(arguments).expect("worktrunk command parses").cmd - else { + let Subcommands::Tix(command) = Args::try_parse_from(arguments)?.cmd else { panic!("worktrunk routes to the tix command") }; assert_eq!( @@ -1508,5 +1503,6 @@ mod tests { "embedded tix supports {help}" ); } + Ok(()) } } diff --git a/src/shared.rs b/src/shared.rs index f777ebb5e1a..0e60edcd64c 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -169,7 +169,7 @@ pub mod pretty { let output = TraceOutput::default(); let dispatch = gitoxide_core::trace::subscriber(1, output.clone(), None)?; tracing::dispatcher::with_default(&dispatch, || tracing::info!("visible event")); - let output = output.lock().expect("trace output lock is not poisoned"); + let output = output.lock().map_err(|err| err.to_string())?; assert!( output.contains_str(b"\x1b["), "forest terminal traces contain ANSI styling" @@ -290,7 +290,7 @@ mod clap { fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> { OsStringValueParser::new() .try_map(|arg| -> gix::Result<_> { - let arg = gix::path::into_bstr(std::path::PathBuf::from(arg)); + let arg = gix::path::into_bstr(std::path::PathBuf::from(arg))?; gix::pathspec::parse(arg.as_ref(), *PATHSPEC_DEFAULTS)?; Ok(arg.into_owned()) }) @@ -312,7 +312,7 @@ mod clap { fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> { OsStringValueParser::new() .try_map(|arg| -> gix::Result<_> { - let arg = gix::path::into_bstr(std::path::PathBuf::from(arg)); + let arg = gix::path::into_bstr(std::path::PathBuf::from(arg))?; gix::pathspec::parse(arg.as_ref(), Default::default())?; Ok(arg.into_owned()) }) diff --git a/tests/it/src/args.rs b/tests/it/src/args.rs index 737cc4a32b7..b2d2325583a 100644 --- a/tests/it/src/args.rs +++ b/tests/it/src/args.rs @@ -192,7 +192,7 @@ impl TypedValueParser for AsPathSpec { OsStringValueParser::new() .try_map(move |arg| { let arg: &std::path::Path = arg.as_os_str().as_ref(); - gix::pathspec::parse(gix::path::into_bstr(arg).as_ref(), pathspec_defaults) + gix::pathspec::parse(gix::path::into_bstr(arg)?.as_ref(), pathspec_defaults) }) .parse_ref(cmd, arg, value) } diff --git a/tests/it/src/commands/copy_royal.rs b/tests/it/src/commands/copy_royal.rs index 84e481b01fd..f535e2577ff 100644 --- a/tests/it/src/commands/copy_royal.rs +++ b/tests/it/src/commands/copy_royal.rs @@ -31,7 +31,7 @@ pub(super) mod function { .index_entries_with_paths(&index) .ok_or_raise(|| message("Didn't find a single entry to copy"))? { - let rela_path = gix::path::from_bstr(rela_path); + let rela_path = gix::path::from_bstr(rela_path)?; let src = worktree_dir.join(&rela_path); stack .make_relative_path_current(&*rela_path, &mut create_dir) diff --git a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure index 9477e92963f..264644f190e 100644 --- a/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure +++ b/tests/snapshots/porcelain/estimate-hours/invalid-branch-name-failure @@ -1,5 +1,5 @@ Error: couldn't parse revision, input="foobar" Caused by: - 0: Reference foobar could not be found + 0: Reference "foobar" could not be found 1: The ref partially named "foobar" could not be found \ No newline at end of file diff --git a/tests/tools/src/lib.rs b/tests/tools/src/lib.rs index fdee107cb55..ffd5feee448 100644 --- a/tests/tools/src/lib.rs +++ b/tests/tools/src/lib.rs @@ -1396,10 +1396,10 @@ pub fn scripted_fixture_writable_with_args_single_archive_with_post_with_git_ver /// ### Example /// /// ```no_run -/// use gix_testtools::{Result, FixtureState}; +/// use gix_testtools::{TestResult, FixtureState}; /// /// #[test] -/// fn test_with_rust_fixture() -> Result { +/// fn test_with_rust_fixture() -> TestResult { /// let (dir, _) = gix_testtools::rust_fixture_read_only("my_fixture", 1, |state| { /// if let FixtureState::Uninitialized(path) = state { /// std::fs::write(path.join("file.txt"), "content")?; @@ -1430,10 +1430,10 @@ where /// ### Example /// /// ```no_run -/// use gix_testtools::{Result, Creation, FixtureState}; +/// use gix_testtools::{TestResult, Creation, FixtureState}; /// /// #[test] -/// fn test_with_writable_rust_fixture() -> Result { +/// fn test_with_writable_rust_fixture() -> TestResult { /// let (dir, ()) = gix_testtools::rust_fixture_writable("my_fixture", 1, Creation::CopyFromReadOnly, |state| { /// if let FixtureState::Uninitialized(path) = state { /// std::fs::write(path.join("file.txt"), "content")?; @@ -1805,8 +1805,8 @@ where let mut cmd = if let Some(file) = prelude_file.as_ref() { let mut cmd = command_with_environment_snapshot(bash_program()); let prelude_path = gix_path::from_bstring( - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(file.path())).into_owned(), - ); + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(file.path())?).into_owned(), + )?; cmd.arg("-c") .arg("source \"$1\" || exit; shift; source \"$0\" \"$@\"") .arg(&script_absolute_path) diff --git a/tests/tools/src/repository/gix.rs b/tests/tools/src/repository/gix.rs index 7bf3cc23ddc..91667dcd18c 100644 --- a/tests/tools/src/repository/gix.rs +++ b/tests/tools/src/repository/gix.rs @@ -100,7 +100,7 @@ fn worktree_from_config( else { return Ok(discovered); }; - let path = gix_path::from_bstr(path.trim().as_bstr()).into_owned(); + let path = gix_path::from_bstr(path.trim().as_bstr())?.into_owned(); if path.as_os_str().is_empty() { return Err("core.worktree cannot be empty".into()); } diff --git a/tests/tools/src/repository/mod.rs b/tests/tools/src/repository/mod.rs index 27249cde68e..c9d9dec2645 100644 --- a/tests/tools/src/repository/mod.rs +++ b/tests/tools/src/repository/mod.rs @@ -176,7 +176,7 @@ fn normalize_config_paths(input: &BStr, root: &Path) -> Result<BString> { .get()? .into_iter() .map(|value| normalize_config_path(value.as_bstr(), root)) - .collect(); + .collect::<Result<_>>()?; for (index, value) in normalized.into_iter().enumerate() { values.set_at(index, value)?; } @@ -186,8 +186,8 @@ fn normalize_config_paths(input: &BStr, root: &Path) -> Result<BString> { Ok(config.into()) } -fn normalize_config_path(value: &BStr, root: &Path) -> BString { - let path = gix_path::from_bstr(value).into_owned(); +fn normalize_config_path(value: &BStr, root: &Path) -> Result<BString> { + let path = gix_path::from_bstr(value)?.into_owned(); let relative = if path.is_absolute() { path.strip_prefix(root) .map(Path::to_owned) @@ -203,20 +203,20 @@ fn normalize_config_path(value: &BStr, root: &Path) -> BString { .find_byte(b':') .is_some_and(|colon| !value[..colon].contains(&b'/')) { - return "<normalized>".into(); + return Ok("<normalized>".into()); } Some(path) }; let Some(relative) = relative else { - return "<normalized>".into(); + return Ok("<normalized>".into()); }; let relative = portable_path(&relative); if relative.is_empty() { - return "<normalized>".into(); + return Ok("<normalized>".into()); } let mut out = b"<normalized>/".to_vec(); out.extend_from_slice(&relative); - out.into() + Ok(out.into()) } fn relative_to_repository_sibling(path: &Path, git_dir: &Path) -> Option<PathBuf> { @@ -284,7 +284,7 @@ fn normalize_config_paths(input: &BStr, root: &Path) -> Result<BString> { }; if is_location_key(section, key) { out.extend_from_slice(&line[..value_start]); - out.extend_from_slice(&normalize_config_path(line[value_start..value_end].as_bstr(), root)); + out.extend_from_slice(&normalize_config_path(line[value_start..value_end].as_bstr(), root)?); out.extend_from_slice(&line[value_end..]); } else { out.extend_from_slice(line); @@ -698,7 +698,11 @@ fn commit_depth( } fn portable_path(path: &Path) -> Cow<'_, BStr> { - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(path)) + // Snapshot formatting cannot report encoding failures; retain raw Unix bytes and + // replace unpaired Windows surrogates only in this human-readable representation. + gix_path::to_unix_separators_on_windows( + gix_path::into_bstr(path).unwrap_or_else(|_| Cow::Owned(path.to_string_lossy().as_bytes().into())), + ) } fn worktree(root: Option<&Path>) -> Result<Vec<WorktreeEntry>> { diff --git a/tests/tools/src/tests.rs b/tests/tools/src/tests.rs index 1475f44d4d8..803018d0959 100644 --- a/tests/tools/src/tests.rs +++ b/tests/tools/src/tests.rs @@ -1,17 +1,19 @@ use super::*; #[test] -fn parse_version() { - assert_eq!(git_version_from_bytes(b"git version 2.37.2").unwrap(), (2, 37, 2)); +fn parse_version() -> TestResult { + assert_eq!(git_version_from_bytes(b"git version 2.37.2")?, (2, 37, 2)); assert_eq!( - git_version_from_bytes(b"git version 2.32.1 (Apple Git-133)").unwrap(), + git_version_from_bytes(b"git version 2.32.1 (Apple Git-133)")?, (2, 32, 1) ); + Ok(()) } #[test] -fn parse_version_with_trailing_newline() { - assert_eq!(git_version_from_bytes(b"git version 2.37.2\n").unwrap(), (2, 37, 2)); +fn parse_version_with_trailing_newline() -> TestResult { + assert_eq!(git_version_from_bytes(b"git version 2.37.2\n")?, (2, 37, 2)); + Ok(()) } const SCOPE_ENV_VALUE: &str = "gitconfig"; @@ -37,8 +39,8 @@ fn populate_ad_hoc_config_files(dir: &Path) { } #[test] -fn configure_command_clears_external_config() { - let temp = tempfile::TempDir::new().expect("can create temp dir"); +fn configure_command_clears_external_config() -> TestResult { + let temp = tempfile::TempDir::new()?; populate_ad_hoc_config_files(temp.path()); for (count, parameters) in [ @@ -60,11 +62,10 @@ fn configure_command_clears_external_config() { temp.path(), ); - let output = cmd.output().expect("can run git"); + let output = cmd.output()?; let lines: Vec<_> = output .stdout - .to_str() - .expect("valid UTF-8") + .to_str()? .lines() .filter(|line| !line.starts_with("command line:\t")) .collect(); @@ -76,10 +77,11 @@ fn configure_command_clears_external_config() { "inherited command-scope configuration must also be discarded" ); } + Ok(()) } #[test] -fn configure_command_keeps_destructive_git_operations_in_the_fixture() -> Result { +fn configure_command_keeps_destructive_git_operations_in_the_fixture() -> TestResult { let fixture = tempfile::TempDir::new()?; let outside = tempfile::TempDir::new()?; for dir in [fixture.path(), outside.path()] { @@ -119,7 +121,7 @@ fn configure_command_keeps_destructive_git_operations_in_the_fixture() -> Result } #[test] -fn isolated_process_runs_the_test_with_fixture_defaults() -> Result { +fn isolated_process_runs_the_test_with_fixture_defaults() -> TestResult { // Exercise sanitization in the child process itself, rather than changing this test runner's environment. if run_in_isolated_process()? { return Ok(()); @@ -134,7 +136,7 @@ fn isolated_process_runs_the_test_with_fixture_defaults() -> Result { } #[test] -fn isolated_process_waits_for_serial_environment_changes() -> Result { +fn isolated_process_waits_for_serial_environment_changes() -> TestResult { #[serial_test::serial] fn launch_while_locked() -> Result<std::thread::JoinHandle<Result<bool>>> { let name = "tests::isolated_process_runs_the_test_with_fixture_defaults"; @@ -163,7 +165,7 @@ fn isolated_process_waits_for_serial_environment_changes() -> Result { } #[test] -fn configure_command_clears_external_git_templates() -> Result { +fn configure_command_clears_external_git_templates() -> TestResult { let temp = tempfile::TempDir::new()?; let template = temp.path().join("template"); std::fs::create_dir(&template)?; @@ -182,8 +184,8 @@ fn configure_command_clears_external_git_templates() -> Result { } #[test] -fn an_absolute_selected_git_is_preferred_in_path() { - let temp = tempfile::TempDir::new().expect("can create temp dir"); +fn an_absolute_selected_git_is_preferred_in_path() -> TestResult { + let temp = tempfile::TempDir::new()?; let git = temp.path().join("bin").join("git"); let mut command = std::process::Command::new("fixture-script"); @@ -199,6 +201,7 @@ fn an_absolute_selected_git_is_preferred_in_path() { git.parent(), "the selected Git executable's directory is searched first" ); + Ok(()) } #[test] @@ -216,7 +219,7 @@ fn a_path_resolved_selected_git_does_not_override_path() { } #[test] -fn configure_command_overrides_xdg_config_home() -> gix_error::TestResult { +fn configure_command_overrides_xdg_config_home() -> TestResult { let current_dir = env::current_dir()?; let temp = tempfile::tempdir_in(¤t_dir)?; let config_dir = temp.path().join(".gix-testtools-xdg-config/git"); @@ -249,14 +252,11 @@ fn configure_command_overrides_xdg_config_home() -> gix_error::TestResult { #[test] #[cfg(windows)] -fn bash_program_ok_for_platform() { +fn bash_program_ok_for_platform() -> TestResult { let path = bash_program(); assert!(path.is_absolute()); - let for_version = std::process::Command::new(path) - .arg("--version") - .output() - .expect("can pass it `--version`"); + let for_version = std::process::Command::new(path).arg("--version").output()?; assert!(for_version.status.success(), "passing `--version` succeeds"); for_version .stdout @@ -264,16 +264,14 @@ fn bash_program_ok_for_platform() { .nth(0) .expect("`--version` output has first line"); - let for_uname_os = std::process::Command::new(path) - .args(["-c", "uname -o"]) - .output() - .expect("can tell it to run `uname -o`"); + let for_uname_os = std::process::Command::new(path).args(["-c", "uname -o"]).output()?; assert!(for_uname_os.status.success(), "telling it to run `uname -o` succeeds"); assert_eq!( for_uname_os.stdout.trim_end(), b"Msys", "it runs commands in an MSYS environment" ); + Ok(()) } #[test] @@ -311,32 +309,31 @@ fn bash_program_absolute_or_unrooted() { } #[test] -fn invoke_bash_runs_in_given_working_directory() { - let dir = tempfile::TempDir::new().expect("can create temp dir"); +fn invoke_bash_runs_in_given_working_directory() -> TestResult { + let dir = tempfile::TempDir::new()?; invoke_bash(dir.path(), "printf '%s' hello > out"); - assert_eq!( - std::fs::read(dir.path().join("out")).expect("script wrote output"), - b"hello" - ); + assert_eq!(std::fs::read(dir.path().join("out"))?, b"hello"); + Ok(()) } #[test] -fn invoke_bash_disables_auto_maintenance_for_git_commands() { - let dir = tempfile::TempDir::new().expect("can create temp dir"); +fn invoke_bash_disables_auto_maintenance_for_git_commands() -> TestResult { + let dir = tempfile::TempDir::new()?; invoke_bash( dir.path(), "git config --get maintenance.auto > out && git config --get gc.auto >> out", ); assert_eq!( - std::fs::read_to_string(dir.path().join("out")).expect("script wrote output"), + std::fs::read_to_string(dir.path().join("out"))?, "false\n0\n", "Git commands run from the shell should not run automatic maintenance" ); + Ok(()) } #[test] -fn run_git_disables_auto_maintenance() -> Result { - let dir = tempfile::TempDir::new().expect("can create temp dir"); +fn run_git_disables_auto_maintenance() -> TestResult { + let dir = tempfile::TempDir::new()?; let status = run_git(dir.path(), &["config", "--get", "maintenance.auto"])?; assert!(status.success(), "command-scope maintenance.auto should be visible"); let status = run_git(dir.path(), &["config", "--get", "gc.auto"])?; @@ -345,8 +342,8 @@ fn run_git_disables_auto_maintenance() -> Result { } #[test] -fn git_helper_disables_auto_maintenance() -> Result { - let dir = tempfile::TempDir::new().expect("can create temp dir"); +fn git_helper_disables_auto_maintenance() -> TestResult { + let dir = tempfile::TempDir::new()?; assert_eq!( git(dir.path(), "config --get maintenance.auto")?, "false\n", @@ -361,27 +358,26 @@ fn git_helper_disables_auto_maintenance() -> Result { } #[test] -fn split_git_arguments_handles_multiline_whitespace() { +fn split_git_arguments_handles_multiline_whitespace() -> TestResult { assert_eq!( split_git_arguments( "log --graph --oneline", - ) - .expect("valid arguments"), + )?, ["log", "--graph", "--oneline"] ); + Ok(()) } #[test] -fn split_git_arguments_handles_quoted_arguments() { +fn split_git_arguments_handles_quoted_arguments() -> TestResult { assert_eq!( split_git_arguments( "commit -m 'subject with spaces' --author=\"A U Thor <author@example.com>\"", - ) - .expect("valid arguments"), + )?, [ "commit", "-m", @@ -389,34 +385,32 @@ fn split_git_arguments_handles_quoted_arguments() { "--author=A U Thor <author@example.com>" ] ); + Ok(()) } #[test] -fn split_git_arguments_handles_empty_quoted_arguments() { - assert_eq!( - split_git_arguments("diff -- pathspec:''").expect("valid arguments"), - ["diff", "--", "pathspec:"] - ); - assert_eq!( - split_git_arguments("diff -- ''").expect("valid arguments"), - ["diff", "--", ""] - ); +fn split_git_arguments_handles_empty_quoted_arguments() -> TestResult { + assert_eq!(split_git_arguments("diff -- pathspec:''")?, ["diff", "--", "pathspec:"]); + assert_eq!(split_git_arguments("diff -- ''")?, ["diff", "--", ""]); + Ok(()) } #[test] -fn split_git_arguments_handles_escaped_whitespace() { +fn split_git_arguments_handles_escaped_whitespace() -> TestResult { assert_eq!( - split_git_arguments(r"add path\ with\ spaces").expect("valid arguments"), + split_git_arguments(r"add path\ with\ spaces")?, ["add", "path with spaces"] ); + Ok(()) } #[test] -fn split_git_arguments_concatenates_quoted_and_unquoted_parts() { +fn split_git_arguments_concatenates_quoted_and_unquoted_parts() -> TestResult { assert_eq!( - split_git_arguments(r#"commit -m prefix" quoted "suffix"#).expect("valid arguments"), + split_git_arguments(r#"commit -m prefix" quoted "suffix"#)?, ["commit", "-m", "prefix quoted suffix"] ); + Ok(()) } #[test] @@ -427,9 +421,9 @@ fn split_git_arguments_rejects_unterminated_quotes() { #[test] #[cfg(feature = "sha1")] -fn normalize_debug_snapshot_returns_replaced_ids_by_placeholder_index() { - let first = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").expect("valid SHA1"); - let second = gix_hash::ObjectId::from_hex(b"496d6428b9cf92981dc9495211e6e1120fb6f2ba").expect("valid SHA1"); +fn normalize_debug_snapshot_returns_replaced_ids_by_placeholder_index() -> TestResult { + let first = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")?; + let second = gix_hash::ObjectId::from_hex(b"496d6428b9cf92981dc9495211e6e1120fb6f2ba")?; let (snapshot, ids) = normalize_debug_snapshot(&vec![first, first, second, first]); assert_eq!(ids, vec![first, second]); @@ -442,14 +436,14 @@ fn normalize_debug_snapshot_returns_replaced_ids_by_placeholder_index() { Oid(1), ]"# ); + Ok(()) } #[test] #[cfg(all(feature = "sha1", feature = "sha256"))] -fn normalize_hashes_replaces_raw_object_ids() { - let sha1 = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").expect("valid SHA1"); - let sha256 = gix_hash::ObjectId::from_hex(b"473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813") - .expect("valid SHA256"); +fn normalize_hashes_replaces_raw_object_ids() -> TestResult { + let sha1 = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")?; + let sha256 = gix_hash::ObjectId::from_hex(b"473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813")?; let (snapshot, ids) = normalize_hashes( "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 \ @@ -459,6 +453,7 @@ fn normalize_hashes_replaces_raw_object_ids() { assert_eq!(ids, vec![sha1, sha256]); assert_eq!(snapshot, "Oid(1) Oid(2) Oid(1)"); + Ok(()) } #[test] @@ -593,8 +588,8 @@ fn write_test_archive(source: &Path, archive: &Path, identity: u32) { } #[test] -fn required_archives_never_fall_back_to_fixture_generation() { - let temp = tempfile::TempDir::new().expect("temporary directory can be created"); +fn required_archives_never_fall_back_to_fixture_generation() -> TestResult { + let temp = tempfile::TempDir::new()?; let archive = temp.path().join("missing.tar"); let destination = temp.path().join("fixture"); let mut generator_was_called = false; @@ -611,8 +606,7 @@ fn required_archives_never_fall_back_to_fixture_generation() { generator_was_called = true; Ok(()) }, - ) - .expect("a missing required archive is not an error"); + )?; assert!(result.is_none(), "the unavailable fixture is reported to the caller"); assert!( @@ -623,15 +617,16 @@ fn required_archives_never_fall_back_to_fixture_generation() { !destination.exists(), "an unavailable archive leaves no reusable cache directory" ); + Ok(()) } #[test] #[serial_test::serial] -fn required_archives_are_extracted_even_when_archives_are_ignored() { - let temp = tempfile::TempDir::new().expect("temporary directory can be created"); +fn required_archives_are_extracted_even_when_archives_are_ignored() -> TestResult { + let temp = tempfile::TempDir::new()?; let source = temp.path().join("source"); - std::fs::create_dir(&source).expect("source directory can be created"); - std::fs::write(source.join("payload"), "from archive").expect("payload can be written"); + std::fs::create_dir(&source)?; + std::fs::write(source.join("payload"), "from archive")?; let archive = temp.path().join(tar_extension()); write_test_archive(&source, &archive, 42); let destination = temp.path().join("fixture"); @@ -649,10 +644,10 @@ fn required_archives_are_extracted_even_when_archives_are_ignored() { assert!(matches!(state, FixtureState::Fresh(_)), "the generator is not invoked"); std::fs::read_to_string(state.path().join("payload")).map_err(Into::into) }, - ) - .expect("the required archive can be extracted"); + )?; assert_eq!(result.as_deref(), Some("from archive")); + Ok(()) } /// Verify that forced execution with normal archive policy honors the explicit destination instead of extracting a @@ -660,14 +655,14 @@ fn required_archives_are_extracted_even_when_archives_are_ignored() { /// absolute paths: extracting an archive into a new location would leave those paths pointing at the archived /// location. In-place execution must also leave the canonical archive unchanged. #[test] -fn forced_normal_fixtures_execute_in_place_instead_of_extracting_archives() { - let temp = tempfile::TempDir::new().expect("temporary directory can be created"); +fn forced_normal_fixtures_execute_in_place_instead_of_extracting_archives() -> TestResult { + let temp = tempfile::TempDir::new()?; let source = temp.path().join("source"); - std::fs::create_dir(&source).expect("source directory can be created"); - std::fs::write(source.join("payload"), "from archive").expect("archive payload can be written"); + std::fs::create_dir(&source)?; + std::fs::write(source.join("payload"), "from archive")?; let archive = temp.path().join(tar_extension()); write_test_archive(&source, &archive, 42); - let archived_contents = std::fs::read(&archive).expect("archive can be read"); + let archived_contents = std::fs::read(&archive)?; let destination = temp.path().join("fixture"); let result = run_fixture_generator_with_marker_handling( @@ -690,31 +685,31 @@ fn forced_normal_fixtures_execute_in_place_instead_of_extracting_archives() { std::fs::write(state.path().join("payload"), "from script")?; std::fs::read_to_string(state.path().join("payload")).map_err(Into::into) }, - ) - .expect("the fixture can be generated in place"); + )?; assert_eq!(result.as_deref(), Some("from script")); assert_eq!( - std::fs::read(&archive).expect("archive can still be read"), + std::fs::read(&archive)?, archived_contents, "executing in a writable location does not replace the canonical archive" ); + Ok(()) } #[test] #[serial_test::serial] -fn version_incompatible_writable_fixtures_use_required_archives_in_both_creation_modes() -> Result { +fn version_incompatible_writable_fixtures_use_required_archives_in_both_creation_modes() -> TestResult { let _environment = isolate_git_environment()?; - let temp = tempfile::TempDir::new().expect("temporary directory can be created"); + let temp = tempfile::TempDir::new()?; let fixture_base = temp.path().join("tests/fixtures"); let archive_dir = fixture_base.join(ARCHIVE_DIR_NAME); - std::fs::create_dir_all(&archive_dir).expect("fixture directories can be created"); + std::fs::create_dir_all(&archive_dir)?; let script = b"#!/bin/sh\nprintf from-script >payload\n"; - std::fs::write(fixture_base.join("make_required.sh"), script).expect("fixture script can be written"); + std::fs::write(fixture_base.join("make_required.sh"), script)?; let source = temp.path().join("archive-source"); - std::fs::create_dir(&source).expect("archive source can be created"); - std::fs::write(source.join("payload"), "from archive").expect("archive payload can be written"); + std::fs::create_dir(&source)?; + std::fs::write(source.join("payload"), "from archive")?; let crc = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM); let mut digest = crc.digest(); digest.update(script); @@ -730,16 +725,15 @@ fn version_incompatible_writable_fixtures_use_required_archives_in_both_creation digest.finalize(), ); - let _cwd = set_current_dir(temp.path()).expect("temporary fixture root is accessible"); + let _cwd = set_current_dir(temp.path())?; let _environment = _environment.set("GIX_TEST_IGNORE_ARCHIVES", "1"); for mode in [Creation::CopyFromReadOnly, Creation::Execute] { let fixture = - scripted_fixture_writable_with_args_with_git_version("make_required.sh", None::<String>, mode, |_| false) - .expect("required archive can be loaded") + scripted_fixture_writable_with_args_with_git_version("make_required.sh", None::<String>, mode, |_| false)? .expect("matching required archive is available"); assert_eq!( - std::fs::read_to_string(fixture.path().join("payload")).expect("archived payload can be read"), + std::fs::read_to_string(fixture.path().join("payload"))?, "from archive", "the fixture comes from the archive instead of the incompatible script" ); @@ -755,10 +749,10 @@ fn hash_kinds_are_classified_independently_of_gix_testtools_features() { } #[test] -fn stale_required_archives_are_unavailable_instead_of_generated() { - let temp = tempfile::TempDir::new().expect("temporary directory can be created"); +fn stale_required_archives_are_unavailable_instead_of_generated() -> TestResult { + let temp = tempfile::TempDir::new()?; let source = temp.path().join("source"); - std::fs::create_dir(&source).expect("source directory can be created"); + std::fs::create_dir(&source)?; let archive = temp.path().join(tar_extension()); write_test_archive(&source, &archive, 41); let destination = temp.path().join("fixture"); @@ -776,14 +770,14 @@ fn stale_required_archives_are_unavailable_instead_of_generated() { generator_was_called = true; Ok(()) }, - ) - .expect("a stale required archive is not an error"); + )?; assert!(result.is_none(), "the stale fixture is reported to the caller"); assert!( !generator_was_called, "a stale archive must not fall back to generation" ); + Ok(()) } #[test] diff --git a/tests/tools/tests/env.rs b/tests/tools/tests/env.rs index d5ba24b54e7..93c3d9b8c76 100644 --- a/tests/tools/tests/env.rs +++ b/tests/tools/tests/env.rs @@ -155,7 +155,7 @@ mod isolate_git_environment { #[test] #[serial] - fn policy_and_tracked_restoration() -> gix_testtools::Result { + fn policy_and_tracked_restoration() -> gix_testtools::TestResult { let _restore = RestoreEnvironment::new(); let disposable = tempfile::tempdir()?; let unused = disposable.path().join("unused").to_string_lossy().into_owned(); @@ -246,7 +246,7 @@ mod isolate_git_environment { #[test] #[serial] - fn git_commands_do_not_inherit_later_environment_overrides() -> gix_testtools::Result { + fn git_commands_do_not_inherit_later_environment_overrides() -> gix_testtools::TestResult { let _environment = gix_testtools::isolate_git_environment()?; let repo = tempfile::tempdir()?; let outside = tempfile::tempdir()?; @@ -287,7 +287,7 @@ mod isolate_git_environment { #[test] #[serial] - fn nested_guards_restore_the_enclosing_scope() -> gix_testtools::Result { + fn nested_guards_restore_the_enclosing_scope() -> gix_testtools::TestResult { let _restore = RestoreEnvironment::new(); let _env = Env::new().set(VAR1, "original").set(VAR2, "").unset(VAR3); let before = snapshot(); @@ -330,7 +330,7 @@ mod isolate_git_environment { #[test] #[serial] - fn early_and_error_returns_restore_the_environment() -> gix_testtools::Result { + fn early_and_error_returns_restore_the_environment() -> gix_testtools::TestResult { fn leave_scope(fail: bool) -> gix_testtools::Result { let _guard = gix_testtools::isolate_git_environment()? .set(VAR1, "changed before returning") @@ -365,7 +365,7 @@ mod isolate_git_environment { #[test] #[serial] - fn panic_unwinding_restores_the_environment() -> gix_testtools::Result { + fn panic_unwinding_restores_the_environment() -> gix_testtools::TestResult { let _restore = RestoreEnvironment::new(); let _env = Env::new().set(VAR1, "original").set(VAR2, "").unset(VAR3); let before = snapshot(); @@ -387,7 +387,7 @@ mod isolate_git_environment { #[cfg(unix)] #[test] #[serial] - fn non_utf8_names_and_values_are_preserved() -> gix_testtools::Result { + fn non_utf8_names_and_values_are_preserved() -> gix_testtools::TestResult { use std::os::unix::ffi::OsStringExt; let _restore = RestoreEnvironment::new(); diff --git a/tests/tools/tests/sbom.rs b/tests/tools/tests/sbom.rs index aa20f5a2e93..56010fb28a0 100644 --- a/tests/tools/tests/sbom.rs +++ b/tests/tools/tests/sbom.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeSet, fs, path::Path, process::Command}; -use gix_testtools::{Result, tempfile::TempDir}; +use gix_testtools::{Result, TestResult, tempfile::TempDir}; use serde_json::Value; fn isolated(command: &mut Command, root: &Path) { @@ -115,7 +115,7 @@ windows-backend = { path = "../windows-backend", optional = true } } #[test] -fn help_and_invalid_selection() -> Result { +fn help_and_invalid_selection() -> TestResult { let root = fixture()?; let help = jtt(root.path(), &["--help"])?; assert!( @@ -268,7 +268,7 @@ fn check_documents(output: &Path, scope: &str) -> Result<BTreeSet<String>> { #[test] #[ignore = "requires cargo-cyclonedx and sbom-tools; run just sbom-test"] -fn package_features_and_workspace() -> Result { +fn package_features_and_workspace() -> TestResult { let root = fixture()?; let lock = fs::read(root.path().join("Cargo.lock"))?; let output_dir = root.path().join("target/sbom"); diff --git a/tests/tools/tests/tools/main.rs b/tests/tools/tests/tools/main.rs index 08c6d36e434..4e192e68b32 100644 --- a/tests/tools/tests/tools/main.rs +++ b/tests/tools/tests/tools/main.rs @@ -1,8 +1,8 @@ mod isolation { - use gix_testtools::{Creation, Result, redact_debug_snapshot}; + use gix_testtools::{Creation, TestResult, redact_debug_snapshot}; #[test] - fn script_configuration_adds_to_the_isolation() -> Result { + fn script_configuration_adds_to_the_isolation() -> TestResult { let dir = gix_testtools::scripted_fixture_writable_with_args( "make_config_isolation.sh", None::<String>, diff --git a/tests/tools/tests/tools/repository.rs b/tests/tools/tests/tools/repository.rs index a4bd4d534dd..ff89fbed384 100644 --- a/tests/tools/tests/tools/repository.rs +++ b/tests/tools/tests/tools/repository.rs @@ -5,7 +5,7 @@ mod snapshot { use gix_testtools::repository::{Head, WorktreeEntryKind}; #[test] - fn captures_refs_commits_index_tree_and_exact_worktree() -> gix_testtools::Result { + fn captures_refs_commits_index_tree_and_exact_worktree() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_repository_state.sh")?; let state = gix_testtools::repository::snapshot(fixture.path())?; @@ -69,7 +69,7 @@ mod snapshot { } #[test] - fn local_config_paths_are_normalized_and_portable_values_are_stabilized() -> gix_testtools::Result { + fn local_config_paths_are_normalized_and_portable_values_are_stabilized() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_repository_state.sh")?; let config_path = fixture.path().join(".git/config"); let included_config = fixture.path().join(".git/included-config"); @@ -82,9 +82,10 @@ mod snapshot { std::fs::write(&included_config, b"[included]\n\tvalue = true\n")?; let mut config = std::fs::OpenOptions::new().append(true).open(&config_path)?; let outside_repository_for_config = - gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&outside_repository)); - let sibling_source_for_config = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&sibling_source)); - let included_config_for_config = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&included_config)); + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&outside_repository)?); + let sibling_source_for_config = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&sibling_source)?); + let included_config_for_config = + gix_path::to_unix_separators_on_windows(gix_path::into_bstr(&included_config)?); write!( config, "\n# retained comment\n[snapshot]\n\tstable = keep\n\ @@ -161,7 +162,7 @@ mod snapshot { } #[test] - fn portable_snapshots_alias_annotated_tags() -> gix_testtools::Result { + fn portable_snapshots_alias_annotated_tags() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_repository_state.sh")?; gix_testtools::git(fixture.path(), "tag -a annotated -m annotated")?; let tag_id = gix_testtools::git(fixture.path(), "rev-parse refs/tags/annotated")? @@ -191,7 +192,7 @@ mod snapshot { } #[test] - fn broken_references_are_ignored() -> gix_testtools::Result { + fn broken_references_are_ignored() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_repository_state.sh")?; let refs_dir = fixture.path().join(".git/refs"); std::fs::write(refs_dir.join("broken"), b"notahexsha\n")?; @@ -211,7 +212,7 @@ mod snapshot { } #[test] - fn repository_variants_are_captured_portably() -> gix_testtools::Result { + fn repository_variants_are_captured_portably() -> gix_testtools::TestResult { // Sparse indexes were introduced in Git 2.34 and are one of the repository forms created by this fixture. let Some(fixture) = gix_testtools::scripted_fixture_writable_with_args_with_git_version( "make_repository_variants.sh", @@ -426,7 +427,7 @@ mod snapshot { } #[test] - fn state_is_a_stable_visual_snapshot() -> gix_testtools::Result { + fn state_is_a_stable_visual_snapshot() -> gix_testtools::TestResult { let fixture = gix_testtools::scripted_fixture_writable("make_repository_state.sh")?; #[cfg(unix)] { diff --git a/tests/tools/tests/tools/rust_fixture.rs b/tests/tools/tests/tools/rust_fixture.rs index b34dcf2c9e1..d0b74892a73 100644 --- a/tests/tools/tests/tools/rust_fixture.rs +++ b/tests/tools/tests/tools/rust_fixture.rs @@ -1,7 +1,7 @@ -use gix_testtools::{Creation, FixtureState, Result}; +use gix_testtools::{Creation, FixtureState, TestResult}; #[test] -fn rust_fixture_read_only_creates_and_caches_fixture() -> Result { +fn rust_fixture_read_only_creates_and_caches_fixture() -> TestResult { // First call should create the fixture let (dir, (a, b, c)) = gix_testtools::rust_fixture_read_only("test_fixture_read_only", 1, |fixture| { let dir = fixture.path(); @@ -41,7 +41,7 @@ fn rust_fixture_read_only_creates_and_caches_fixture() -> Result { } #[test] -fn rust_fixture_read_only_version_change_invalidates_cache() -> Result { +fn rust_fixture_read_only_version_change_invalidates_cache() -> TestResult { // Create fixture with version 1 let (dir1, _) = gix_testtools::rust_fixture_read_only("test_fixture_version", 1, |fixture| { if let FixtureState::Uninitialized(dir) = fixture { @@ -71,7 +71,7 @@ fn rust_fixture_read_only_version_change_invalidates_cache() -> Result { } #[test] -fn rust_fixture_writable() -> Result { +fn rust_fixture_writable() -> TestResult { for creation in [Creation::CopyFromReadOnly, Creation::Execute] { let (tmp, _) = gix_testtools::rust_fixture_writable("test_fixture_writable_copy", 1, creation, |fixture| { if let FixtureState::Uninitialized(dir) = fixture { diff --git a/tests/tools/tests/tools/scripted_fixture_with_post.rs b/tests/tools/tests/tools/scripted_fixture_with_post.rs index 2fda15a8262..e7b3d5705ab 100644 --- a/tests/tools/tests/tools/scripted_fixture_with_post.rs +++ b/tests/tools/tests/tools/scripted_fixture_with_post.rs @@ -1,9 +1,9 @@ -use gix_testtools::{Creation, Result}; +use gix_testtools::{Creation, TestResult}; const SCRIPT_NAME: &str = "make_basic.sh"; #[test] -fn scripted_fixture_read_only_with_post_returns_value() -> Result { +fn scripted_fixture_read_only_with_post_returns_value() -> TestResult { let (dir, value) = gix_testtools::scripted_fixture_read_only_with_post(SCRIPT_NAME, 1, |fixture| { let dir = fixture.path(); // The script should have already created these files @@ -25,7 +25,7 @@ fn scripted_fixture_read_only_with_post_returns_value() -> Result { } #[test] -fn scripted_fixture_writable_with_post_returns_value() -> Result { +fn scripted_fixture_writable_with_post_returns_value() -> TestResult { let (tmp, value) = gix_testtools::scripted_fixture_writable_with_args_with_post( SCRIPT_NAME, None::<String>, @@ -50,7 +50,7 @@ fn scripted_fixture_writable_with_post_returns_value() -> Result { } #[test] -fn scripted_fixture_with_post_can_return_complex_types() -> Result { +fn scripted_fixture_with_post_can_return_complex_types() -> TestResult { #[derive(Debug, PartialEq)] struct FixtureInfo { file_count: usize, @@ -80,7 +80,7 @@ fn scripted_fixture_with_post_can_return_complex_types() -> Result { } #[test] -fn version_compatible_writable_fixtures_support_both_creation_modes() -> Result { +fn version_compatible_writable_fixtures_support_both_creation_modes() -> TestResult { let copied = gix_testtools::scripted_fixture_writable_with_args_with_git_version( SCRIPT_NAME, ["version-compatible-copy"], @@ -105,7 +105,7 @@ fn version_compatible_writable_fixtures_support_both_creation_modes() -> Result } #[test] -fn version_incompatible_writable_fixtures_never_run_the_script() -> Result { +fn version_incompatible_writable_fixtures_never_run_the_script() -> TestResult { assert!( gix_testtools::scripted_fixture_writable_with_args_with_git_version( SCRIPT_NAME, diff --git a/tests/tools/tests/umask.rs b/tests/tools/tests/umask.rs index 11f546b219e..6314cb55b09 100644 --- a/tests/tools/tests/umask.rs +++ b/tests/tools/tests/umask.rs @@ -4,7 +4,7 @@ not(any(target_os = "linux", target_os = "android")), ignore = "The test itself uses /proc" )] -fn umask() { +fn umask() -> gix_testtools::TestResult { use std::{ fs::File, io::{BufRead, BufReader}, @@ -12,13 +12,13 @@ fn umask() { use bstr::ByteSlice; // Check against the umask obtained via a less portable but also completely safe method. - let less_portable = BufReader::new(File::open("/proc/self/status").expect("can open")) + let less_portable = BufReader::new(File::open("/proc/self/status")?) .split(b'\n') - .find_map(|line| line.expect("can read").strip_prefix(b"Umask:\t").map(ToOwned::to_owned)) - .expect("has umask line") - .to_str() - .expect("umask line is valid UTF-8") + .find_map(|line| line.map(|l| l.strip_prefix(b"Umask:\t").map(Vec::from)).transpose()) + .expect("has umask line")? + .to_str()? .to_owned(); let more_portable = format!("{:04o}", gix_testtools::umask()); assert_eq!(more_portable, less_portable); + Ok(()) } From 7efb8120244cbd4ce6b96dcf5f0ad5d49fb61cef Mon Sep 17 00:00:00 2001 From: Byron <sebastian.thiel@icloud.com> Date: Sun, 4 Oct 2026 06:44:18 +0200 Subject: [PATCH 27/28] rename(gix-parallel)!: specialize `gix-features` for parallel execution <!-- agent --> `gix-features` once coordinated several alternative implementations. Serial versus threaded execution is its remaining shared implementation policy; tracing, hashing, compression, and filesystem operations already have their own owners. Move progress, interruption, byte pipes, decoding, iterator chunks, and cache diagnostics into `gix-utils`. Keep utilities that add dependencies behind feature toggles, including `interrupt` for typed cancellation via `gix-error`. Use `crc32fast` directly in `gix-pack` and `gix-odb`. Rename the remaining crate to `gix-parallel` and expose its computation helpers, reducers, and shared ownership primitives at the crate root. Replace `gix::features` and `gix::threading` with the existing owner exports, including `gix::parallel` and `gix::utils`, and adapt all workspace callers. Expose both progress formatting choices individually on `gix`, retaining `comfort` as their bundle. Forward tracing directly to `gix-trace` and cache diagnostics to `gix-utils`. Update documentation, feature checks, the pack fuzz harness, and package-size checks for the new ownership. Assisted-by: GPT 6.1 Sol Co-authored-by: GPT 6.1 Sol <codex@openai.com> --- .github/workflows/ci.yml | 9 +- AGENTS.md | 2 +- Cargo.lock | 89 +++++++++------ Cargo.toml | 9 +- DEVELOPMENT.md | 12 +- README.md | 4 +- STABILITY.md | 2 +- crate-status.md | 12 +- etc/discovery/odb.md | 2 +- etc/scripts/cargo-check-all.sh | 18 ++- etc/scripts/check-package-size.sh | 4 +- gitoxide-core/src/corpus/engine.rs | 9 +- gitoxide-core/src/corpus/run.rs | 4 +- gitoxide-core/src/hours/mod.rs | 2 +- gitoxide-core/src/query/engine/update.rs | 4 +- gitoxide-core/src/repository/odb.rs | 2 +- gix-attributes/Cargo.toml | 4 +- gix-attributes/src/lib.rs | 2 +- gix-attributes/src/name.rs | 2 +- gix-attributes/src/search/mod.rs | 2 +- gix-attributes/src/search/outcome.rs | 2 +- gix-config/Cargo.toml | 3 +- gix-config/src/file/access/mutate.rs | 2 +- gix-config/src/file/access/read_only.rs | 2 +- gix-config/src/file/includes/mod.rs | 2 +- gix-config/src/file/init/from_paths.rs | 4 +- gix-config/src/file/init/mod.rs | 2 +- gix-config/src/file/mod.rs | 2 +- gix-config/src/file/section/mod.rs | 2 +- gix-config/src/types.rs | 2 +- gix-config/tests/config/file/access/mutate.rs | 2 +- gix-features/Cargo.toml | 108 ------------------ gix-features/src/hash.rs | 23 ---- gix-features/src/lib.rs | 61 ---------- gix-features/tests/features.rs | 1 - gix-features/tests/trace/mod.rs | 19 --- gix-fs/Cargo.toml | 2 +- gix-fs/src/snapshot.rs | 2 +- gix-glob/Cargo.toml | 2 +- gix-glob/src/wildmatch.rs | 2 +- gix-hash/Cargo.toml | 2 +- gix-hash/src/io.rs | 12 +- gix-hash/tests/hash/hasher.rs | 2 +- gix-index/Cargo.toml | 9 +- gix-index/src/access/mod.rs | 4 +- gix-index/src/decode/mod.rs | 16 +-- gix-index/src/extension/tree/verify.rs | 2 +- gix-index/src/file/init.rs | 6 +- gix-index/src/file/verify.rs | 4 +- gix-index/src/file/write.rs | 4 +- gix-index/src/init.rs | 2 +- gix-index/src/lib.rs | 2 +- gix-index/src/verify.rs | 2 +- gix-index/src/write.rs | 2 +- gix-object/Cargo.toml | 5 +- gix-object/src/find.rs | 2 +- gix-object/src/lib.rs | 2 +- gix-object/tests/object/main.rs | 4 +- gix-odb/Cargo.toml | 7 +- gix-odb/src/lib.rs | 4 +- gix-odb/src/store_impls/dynamic/handle.rs | 6 +- gix-odb/src/store_impls/dynamic/init.rs | 2 +- gix-odb/src/store_impls/dynamic/load_index.rs | 2 +- gix-odb/src/store_impls/dynamic/prefix.rs | 2 +- gix-odb/src/store_impls/dynamic/structure.rs | 2 +- gix-odb/src/store_impls/dynamic/types.rs | 7 +- gix-odb/src/store_impls/dynamic/verify.rs | 16 +-- gix-odb/src/store_impls/loose/verify.rs | 6 +- gix-odb/tests/odb/error.rs | 20 ++-- gix-odb/tests/odb/store/dynamic.rs | 4 +- gix-odb/tests/odb/store/loose.rs | 8 +- gix-pack/Cargo.toml | 7 +- gix-pack/benches/pack_create.rs | 3 +- gix-pack/fuzz/Cargo.toml | 5 +- gix-pack/fuzz/fuzz_targets/data_file.rs | 2 +- gix-pack/fuzz/fuzz_targets/index_file.rs | 2 +- .../fuzz/fuzz_targets/multi_index_file.rs | 2 +- gix-pack/src/bundle/mod.rs | 2 +- gix-pack/src/bundle/write/mod.rs | 18 +-- gix-pack/src/cache/delta/from_offsets.rs | 2 +- gix-pack/src/cache/delta/traverse/mod.rs | 8 +- gix-pack/src/cache/delta/traverse/resolve.rs | 36 +++--- gix-pack/src/cache/delta/tree.rs | 12 +- gix-pack/src/cache/lru.rs | 8 +- gix-pack/src/cache/mod.rs | 2 +- gix-pack/src/cache/object.rs | 4 +- gix-pack/src/data/entry/decode.rs | 2 +- gix-pack/src/data/file/init.rs | 2 +- gix-pack/src/data/file/verify.rs | 4 +- gix-pack/src/data/input/bytes_to_entries.rs | 6 +- gix-pack/src/data/input/entries_to_bytes.rs | 2 +- gix-pack/src/data/input/entry.rs | 6 +- gix-pack/src/data/mod.rs | 2 +- gix-pack/src/data/output/bytes.rs | 2 +- gix-pack/src/data/output/count/objects/mod.rs | 18 +-- .../src/data/output/count/objects/reduce.rs | 2 +- .../src/data/output/entry/iter_from_counts.rs | 31 +++-- gix-pack/src/index/encode.rs | 10 +- gix-pack/src/index/traverse/mod.rs | 7 +- gix-pack/src/index/traverse/reduce.rs | 10 +- gix-pack/src/index/traverse/with_index.rs | 12 +- gix-pack/src/index/traverse/with_lookup.rs | 14 +-- gix-pack/src/index/util.rs | 2 +- gix-pack/src/index/verify.rs | 8 +- gix-pack/src/index/write/mod.rs | 7 +- gix-pack/src/multi_index/verify.rs | 21 +--- gix-pack/src/multi_index/write.rs | 14 +-- gix-pack/src/verify.rs | 2 +- gix-pack/tests/pack/bundle.rs | 2 +- gix-pack/tests/pack/data/file.rs | 2 +- .../pack/data/output/count_and_entries.rs | 6 +- gix-pack/tests/pack/index.rs | 6 +- gix-pack/tests/pack/multi_index/fuzzed.rs | 2 +- gix-pack/tests/pack/multi_index/verify.rs | 2 +- gix-pack/tests/pack/multi_index/write.rs | 2 +- {gix-features => gix-parallel}/CHANGELOG.md | 0 gix-parallel/Cargo.toml | 56 +++++++++ {gix-features => gix-parallel}/LICENSE-APACHE | 0 {gix-features => gix-parallel}/LICENSE-MIT | 0 .../src}/eager_iter.rs | 2 +- .../parallel => gix-parallel/src}/in_order.rs | 0 .../src}/in_parallel.rs | 18 +-- .../mod.rs => gix-parallel/src/lib.rs | 26 +++-- .../parallel => gix-parallel/src}/reduce.rs | 20 ++-- .../parallel => gix-parallel/src}/serial.rs | 2 +- .../src/threading.rs | 6 +- .../tests/parallel/in_order_iter.rs | 2 +- .../tests/parallel/in_parallel_with_slice.rs | 2 +- .../tests/parallel/mod.rs | 8 +- .../tests/parallel_shared.rs | 0 .../tests/parallel_shared_threaded.rs | 0 .../tests/parallel_threaded.rs | 6 +- .../tests/tracing.rs | 11 +- gix-protocol/Cargo.toml | 5 +- gix-protocol/src/fetch/function.rs | 6 +- gix-protocol/src/fetch/types.rs | 2 +- gix-protocol/src/handshake/function.rs | 6 +- gix-protocol/src/handshake/mod.rs | 2 +- gix-protocol/src/ls_refs.rs | 6 +- gix-protocol/src/remote_progress.rs | 4 +- gix-protocol/tests/protocol/fetch/_impl.rs | 6 +- gix-protocol/tests/protocol/fetch/mod.rs | 4 +- gix-protocol/tests/protocol/fetch/response.rs | 2 +- gix-protocol/tests/protocol/fetch/v1.rs | 2 +- gix-protocol/tests/protocol/fetch/v2.rs | 2 +- gix-ref/Cargo.toml | 4 +- gix-ref/src/store/file/packed.rs | 2 +- gix-status/Cargo.toml | 6 +- gix-status/src/index_as_worktree/function.rs | 12 +- gix-status/src/index_as_worktree/traits.rs | 2 +- .../src/index_as_worktree_with_renames/mod.rs | 18 +-- gix-status/tests/status/index_as_worktree.rs | 10 +- .../status/index_as_worktree_with_renames.rs | 2 +- gix-tix/src/command.rs | 4 +- gix-tix/src/command/rebase.rs | 2 +- gix-tix/src/edit/mod.rs | 4 +- gix-tix/src/edit/time_travel.rs | 4 +- gix-tix/src/history.rs | 51 +++------ gix-tix/src/lib.rs | 31 ++--- gix-tix/src/logging.rs | 2 +- gix-tix/src/ref_tree.rs | 4 +- gix-tix/src/worktrunk/mod.rs | 4 +- gix-trace/Cargo.toml | 2 +- gix-trace/src/lib.rs | 4 +- gix-transport/Cargo.toml | 5 +- gix-transport/src/client/async_io/request.rs | 8 +- gix-transport/src/client/blocking_io/file.rs | 6 +- .../src/client/blocking_io/http/curl/mod.rs | 2 +- .../client/blocking_io/http/curl/remote.rs | 6 +- .../client/blocking_io/http/reqwest/remote.rs | 4 +- .../src/client/blocking_io/request.rs | 10 +- .../src/client/blocking_io/ssh/mod.rs | 2 +- gix-utils/Cargo.toml | 32 +++++- {gix-features => gix-utils}/src/cache.rs | 0 {gix-features => gix-utils}/src/decode.rs | 0 {gix-features => gix-utils}/src/interrupt.rs | 0 {gix-features => gix-utils}/src/io.rs | 0 gix-utils/src/iter.rs | 29 +++++ gix-utils/src/lib.rs | 19 ++- {gix-features => gix-utils}/src/progress.rs | 0 {gix-features => gix-utils}/tests/pipe.rs | 6 +- gix-worktree-state/Cargo.toml | 6 +- gix-worktree-state/src/checkout/chunk.rs | 6 +- gix-worktree-state/src/checkout/entry.rs | 4 +- gix-worktree-state/src/checkout/function.rs | 15 +-- .../tests/worktree-state/checkout.rs | 6 +- gix-worktree-stream/Cargo.toml | 3 +- gix-worktree-stream/src/from_tree/mod.rs | 4 +- gix-worktree-stream/src/from_tree/traverse.rs | 2 +- gix-worktree-stream/src/lib.rs | 6 +- gix-worktree-stream/src/protocol.rs | 4 +- gix-worktree/Cargo.toml | 10 +- gix-worktree/src/remove.rs | 22 ++-- gix-worktree/tests/worktree/remove.rs | 20 ++-- gix/Cargo.toml | 24 ++-- gix/src/clone/checkout.rs | 8 +- gix/src/clone/fetch/util.rs | 2 +- gix/src/config/mod.rs | 2 +- gix/src/config/snapshot/access.rs | 2 +- gix/src/interrupt.rs | 8 +- gix/src/lib.rs | 10 +- gix/src/open/repository.rs | 4 +- gix/src/prelude.rs | 2 +- gix/src/progress.rs | 4 +- .../remote/connection/fetch/receive_pack.rs | 4 +- gix/src/remote/connection/ref_map.rs | 2 +- gix/src/remote/url/rewrite.rs | 2 +- gix/src/repository/branch.rs | 2 +- gix/src/repository/submodule.rs | 2 +- gix/src/repository/worktree.rs | 4 +- gix/src/shallow.rs | 2 +- gix/src/status/index_worktree.rs | 4 +- gix/src/status/iter/mod.rs | 2 +- gix/src/status/iter/types.rs | 2 +- gix/src/status/mod.rs | 6 +- gix/src/status/platform.rs | 2 +- gix/src/submodule/mod.rs | 8 +- gix/src/types.rs | 2 +- gix/src/worktree/add.rs | 2 +- gix/src/worktree/mod.rs | 2 +- gix/src/worktree/remove.rs | 4 +- gix/tests/gix/remote/fetch.rs | 2 +- gix/tests/gix/remote/ref_map.rs | 2 +- gix/tests/gix/repository/worktree.rs | 2 +- justfile | 4 +- src/shared.rs | 2 +- 226 files changed, 788 insertions(+), 879 deletions(-) delete mode 100644 gix-features/Cargo.toml delete mode 100644 gix-features/src/hash.rs delete mode 100644 gix-features/src/lib.rs delete mode 100644 gix-features/tests/features.rs delete mode 100644 gix-features/tests/trace/mod.rs rename {gix-features => gix-parallel}/CHANGELOG.md (100%) create mode 100644 gix-parallel/Cargo.toml rename {gix-features => gix-parallel}/LICENSE-APACHE (100%) rename {gix-features => gix-parallel}/LICENSE-MIT (100%) rename {gix-features/src/parallel => gix-parallel/src}/eager_iter.rs (98%) rename {gix-features/src/parallel => gix-parallel/src}/in_order.rs (100%) rename {gix-features/src/parallel => gix-parallel/src}/in_parallel.rs (97%) rename gix-features/src/parallel/mod.rs => gix-parallel/src/lib.rs (88%) rename {gix-features/src/parallel => gix-parallel/src}/reduce.rs (93%) rename {gix-features/src/parallel => gix-parallel/src}/serial.rs (99%) rename {gix-features => gix-parallel}/src/threading.rs (95%) rename {gix-features => gix-parallel}/tests/parallel/in_order_iter.rs (97%) rename {gix-features => gix-parallel}/tests/parallel/in_parallel_with_slice.rs (99%) rename {gix-features => gix-parallel}/tests/parallel/mod.rs (92%) rename {gix-features => gix-parallel}/tests/parallel_shared.rs (100%) rename {gix-features => gix-parallel}/tests/parallel_shared_threaded.rs (100%) rename {gix-features => gix-parallel}/tests/parallel_threaded.rs (94%) rename gix-features/tests/trace/parallel.rs => gix-parallel/tests/tracing.rs (89%) rename {gix-features => gix-utils}/src/cache.rs (100%) rename {gix-features => gix-utils}/src/decode.rs (100%) rename {gix-features => gix-utils}/src/interrupt.rs (100%) rename {gix-features => gix-utils}/src/io.rs (100%) create mode 100644 gix-utils/src/iter.rs rename {gix-features => gix-utils}/src/progress.rs (100%) rename {gix-features => gix-utils}/tests/pipe.rs (95%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a21cf5a4cb7..ff8eb92c0b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -578,11 +578,14 @@ jobs: for crate in "${crates[@]}"; do cargo build -p "$crate" --target "$TARGET" done - - name: features of gix-features + - name: features of gix-parallel and gix-utils run: | set -x - for feature in progress parallel io-pipe crc32 cache-efficiency-debug; do - cargo build -p gix-features --features "$feature" --target "$TARGET" + for feature in '' parallel once_cell parallel,once_cell; do + cargo build -p gix-parallel --no-default-features --features "$feature" --target "$TARGET" + done + for feature in '' progress progress,progress-unit-bytes progress,progress-unit-human-numbers interrupt io-pipe cache-efficiency-debug; do + cargo build -p gix-utils --no-default-features --features "$feature" --target "$TARGET" done - name: crates with 'sha1' and 'wasm' feature run: | diff --git a/AGENTS.md b/AGENTS.md index c8e4d4c71ba..e437159565d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,7 +113,7 @@ Follow "purposeful conventional commits" style: - Prefer references in plumbing crates to avoid expensive clones - Avoid calling `.detach()` unless an owned value is explicitly required. Many `gix` APIs accept attached ids and references directly, so prefer keeping repository-backed handles like `gix::Id` when possible. - Name variables holding untyped Git object IDs `<type>_id` or `*_<type>_id` (for example, `commit_id`, `root_tree_id`, or `note_blob_id`) so the object kind is always explicit. -- Use `gix_features::threading::*` for interior mutability primitives +- Use `gix_parallel::*` for interior mutability primitives ### Path Handling diff --git a/Cargo.lock b/Cargo.lock index 949fe5e4237..d9dba8d16cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,7 +1476,6 @@ dependencies = [ "futures-lite", "gitoxide-core", "gix", - "gix-features", "gix-tix", "prodash", "serde_derive", @@ -1544,7 +1543,6 @@ dependencies = [ "gix-dir", "gix-discover", "gix-error", - "gix-features", "gix-filter", "gix-fs", "gix-glob", @@ -1560,6 +1558,7 @@ dependencies = [ "gix-object", "gix-odb", "gix-pack", + "gix-parallel", "gix-path", "gix-pathspec", "gix-prompt", @@ -1644,9 +1643,9 @@ dependencies = [ "criterion", "document-features", "gix-error", - "gix-features", "gix-fs", "gix-glob", + "gix-parallel", "gix-path", "gix-quote", "gix-testtools", @@ -1739,12 +1738,13 @@ dependencies = [ "gix-config", "gix-config-value", "gix-error", - "gix-features", "gix-glob", + "gix-parallel", "gix-path", "gix-ref", "gix-sec", "gix-testtools", + "gix-trace", "gix-utils", "insta", "serde", @@ -1890,25 +1890,6 @@ dependencies = [ "insta", ] -[[package]] -name = "gix-features" -version = "0.50.0" -dependencies = [ - "bytes", - "bytesize", - "crc32fast", - "crossbeam-channel", - "document-features", - "gix-error", - "gix-testtools", - "gix-trace", - "insta", - "once_cell", - "parking_lot", - "prodash", - "tracing-subscriber", -] - [[package]] name = "gix-fetchhead" version = "0.0.0" @@ -1944,7 +1925,7 @@ dependencies = [ "crossbeam-channel", "document-features", "gix-error", - "gix-features", + "gix-parallel", "gix-path", "gix-testtools", "gix-utils", @@ -1976,10 +1957,10 @@ dependencies = [ "bstr", "criterion", "document-features", - "gix-features", "gix-fs", "gix-path", "gix-testtools", + "gix-trace", "gix-utils", "serde", ] @@ -1992,9 +1973,9 @@ dependencies = [ "document-features", "faster-hex", "gix-error", - "gix-features", "gix-hash", "gix-testtools", + "gix-utils", "insta", "serde", "sha1dc", @@ -2051,14 +2032,15 @@ dependencies = [ "fnv", "gix-bitmap", "gix-error", - "gix-features", "gix-fs", "gix-hash", "gix-hashtable", "gix-lock", "gix-object", "gix-odb", + "gix-parallel", "gix-testtools", + "gix-trace", "gix-traverse", "gix-utils", "gix-validate", @@ -2189,7 +2171,6 @@ dependencies = [ "gix-command", "gix-date", "gix-error", - "gix-features", "gix-hash", "gix-hashtable", "gix-object", @@ -2212,22 +2193,25 @@ name = "gix-odb" version = "0.85.0" dependencies = [ "arc-swap", + "crc32fast", "crossbeam-channel", "document-features", "filetime", "gix-actor", "gix-date", "gix-error", - "gix-features", "gix-fs", "gix-hash", "gix-hashtable", "gix-object", "gix-odb", "gix-pack", + "gix-parallel", "gix-path", "gix-quote", "gix-testtools", + "gix-trace", + "gix-utils", "gix-zlib", "insta", "maplit", @@ -2244,22 +2228,25 @@ version = "0.75.0" dependencies = [ "bstr", "clru", + "crc32fast", "criterion", "crossbeam-deque", "document-features", "gix-chunk", "gix-diff", "gix-error", - "gix-features", "gix-hash", "gix-hashtable", "gix-object", "gix-odb", "gix-pack", + "gix-parallel", "gix-path", "gix-tempfile", "gix-testtools", + "gix-trace", "gix-traverse", + "gix-utils", "gix-zlib", "insta", "maplit", @@ -2293,6 +2280,20 @@ dependencies = [ "serde", ] +[[package]] +name = "gix-parallel" +version = "0.50.0" +dependencies = [ + "crossbeam-channel", + "document-features", + "gix-testtools", + "gix-trace", + "insta", + "once_cell", + "parking_lot", + "tracing-subscriber", +] + [[package]] name = "gix-path" version = "0.13.0" @@ -2353,7 +2354,6 @@ dependencies = [ "gix-credentials", "gix-date", "gix-error", - "gix-features", "gix-hash", "gix-lock", "gix-macros", @@ -2396,12 +2396,12 @@ dependencies = [ "gix-date", "gix-discover", "gix-error", - "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-odb", + "gix-parallel", "gix-path", "gix-tempfile", "gix-testtools", @@ -2503,17 +2503,19 @@ dependencies = [ "gix-diff", "gix-dir", "gix-error", - "gix-features", "gix-filter", "gix-fs", "gix-hash", "gix-index", "gix-object", "gix-odb", + "gix-parallel", "gix-path", "gix-pathspec", "gix-status", "gix-testtools", + "gix-trace", + "gix-utils", "gix-worktree", "hashbrown 0.16.1", "insta", @@ -2639,7 +2641,6 @@ dependencies = [ "gix-command", "gix-credentials", "gix-error", - "gix-features", "gix-hash", "gix-macros", "gix-pack", @@ -2648,8 +2649,10 @@ dependencies = [ "gix-quote", "gix-sec", "gix-testtools", + "gix-trace", "gix-transport", "gix-url", + "gix-utils", "insta", "parking_lot", "pin-project-lite", @@ -2703,10 +2706,17 @@ name = "gix-utils" version = "0.4.0" dependencies = [ "bstr", + "bytes", + "bytesize", "criterion", + "document-features", "fastrand", "getrandom 0.4.3", + "gix-error", + "gix-testtools", "gix-utils", + "insta", + "prodash", "unicode-normalization", ] @@ -2730,7 +2740,6 @@ dependencies = [ "gix-attributes", "gix-discover", "gix-error", - "gix-features", "gix-fs", "gix-glob", "gix-hash", @@ -2738,8 +2747,11 @@ dependencies = [ "gix-index", "gix-object", "gix-odb", + "gix-parallel", "gix-path", "gix-testtools", + "gix-trace", + "gix-utils", "gix-validate", "gix-worktree", "insta", @@ -2752,15 +2764,17 @@ version = "0.35.0" dependencies = [ "bstr", "gix-error", - "gix-features", "gix-filter", "gix-fs", "gix-hash", "gix-index", "gix-object", "gix-odb", + "gix-parallel", "gix-path", "gix-testtools", + "gix-trace", + "gix-utils", "gix-worktree", "gix-worktree-state", "insta", @@ -2774,7 +2788,6 @@ version = "0.37.0" dependencies = [ "gix-attributes", "gix-error", - "gix-features", "gix-filter", "gix-fs", "gix-hash", @@ -2782,7 +2795,9 @@ dependencies = [ "gix-odb", "gix-path", "gix-testtools", + "gix-trace", "gix-traverse", + "gix-utils", "gix-worktree", "gix-worktree-stream", "insta", diff --git a/Cargo.toml b/Cargo.toml index f8b3b470556..3bcfc874a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ lean-async = ["hashes", "fast", "tracing", "pretty-cli", "tix", "gitoxide-core-t #! ### Package Maintainers #! `*-control` features leave it to you to configure C libraries, involving choices for HTTP transport implementation. #! -#! Additional features *can* be provided with `--features` and are handled by the [`gix-features` crate](https://docs.rs/gix-features/latest). +#! Additional features *can* be provided with `--features` and are handled by the [`gix` crate](https://docs.rs/gix/latest/gix/#feature-flags). #! Note that only one HTTP transport can be enabled at a time. See the *Building Blocks for mutually exclusive networking* headline. #! ### Building Blocks @@ -109,7 +109,7 @@ fast = ["gix/max-performance", "gix/comfort"] fast-safe = ["fast"] ## Enable tracing in `gitoxide-core`. -tracing = ["dep:anstream", "dep:tracing", "gitoxide-core/tracing", "gix-features/tracing", "gix-features/tracing-detail"] +tracing = ["dep:anstream", "dep:tracing", "gitoxide-core/tracing", "gix/tracing-detail"] ## Also measure user and kernel CPU time in trace trees on supported Unix platforms. ## Sampling adds overhead on span entry and exit. @@ -127,7 +127,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", " ## Prints statistical information to inform about cache efficiency when those are dropped. ## Use this as a way to understand if bigger caches actually produce greater yields. -cache-efficiency-debug = ["gix-features/cache-efficiency-debug"] +cache-efficiency-debug = ["gix/cache-efficiency-debug"] ## A way to enable most `gitoxide-core` tools found in `ein tools`, namely `organize` and `estimate hours`. gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours", "gitoxide-core-tools-archive", "gitoxide-core-tools-clean"] @@ -169,7 +169,6 @@ gitoxide-core-async-client = ["gitoxide-core/async-client", "futures-lite"] [dependencies] gitoxide-core = { version = "^0.62.0", path = "gitoxide-core" } -gix-features = { version = "^0.50.0", path = "gix-features" } gix = { version = "^0.88.0", path = "gix", default-features = false, features = ["error-print-location"] } gix-tix = { version = "^0.4.0", path = "gix-tix", default-features = false, optional = true } @@ -236,7 +235,7 @@ members = [ "gix-config", "gix-config-value", "gix-discover", - "gix-features", + "gix-parallel", "gix-zlib", "gix-trace", "gix-commitgraph", diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d0ecaeba745..78c6113c73c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -183,8 +183,8 @@ including feature selection and matching inventories in both formats. * `blocking` can be used to make `Read` and `Iterator` async, or move any operation onto a thread which blends it into the async world. * Most operations are fast and 'interrupting' them is as easy as ignoring their result by cancelling their task. - * Long-running operations can be roughly interacted with using `gix_features::interrupt::trigger()` function, and after a moment - of waiting the flag can be unset with the `…::uninterrupt()` function to allow new long-running operations to work. + * Long-running operations can be roughly interacted with using the `gix::interrupt::trigger()` function, and after a moment + of waiting the flag can be unset with the `gix::interrupt::reset()` function to allow new long-running operations to work. Every long running operation supports this. * **server-side** * ~~Building a pack is CPU and at some point, IO bound, and it makes no sense to use async to handle more connections - git @@ -203,7 +203,7 @@ including feature selection and matching inventories in both formats. control of the name to the caller. However, call `.init(…)` to configure the iteration. * and when calling `add_child(…)` don't use the parent progress instance for anything else. * **interruption of long-running operations** - * Use `gix-features::interrupt::*` for building support for interruptions of long-running operations only. + * Use `gix_utils::interrupt::*` for building support for interruptions of long-running operations only. * It's up to the author to decide how to best integrate it, generally we use a poll-based mechanism to check whether an interrupt flag is set. * **this is a must if…** @@ -225,14 +225,14 @@ including feature selection and matching inventories in both formats. - in **plumbing**, do not use it at all but instead provide the mutable part (like caches, buffers) as arguments, pushing their handling entirely to the caller. - Set on top an optional abstraction that manages the above for you using **interior mutability only if part of the mutable state has to be returned as borrow** or if otherwise it wouldn't be possible to borrowcheck. Or in other words: start without interior mutability and try to do it the standard way, but switch when needed. - - When using primitives to support interior mutability, use the provided ones and utility functions in `gix_features::threading::*` exclusively to allow switching between + - When using primitives to support interior mutability, use the provided ones and utility functions in `gix_parallel::*` exclusively to allow switching between thread-safe and none-threadsafe versions at compile time. - The preferred way of using it is to start out as upgradable reader, and upgrading to write if needed, keeping contention to a minimum. - If _shared ownership_ is involved, one always needs _interior mutability_, but may still decide to use an API that requires `&mut self` if locally stored caches are involved. - - Types that are not thread-local must be `Sync`, but only if the `gix-features/parallel` is enabled due to the usage of `gix_features::threading::…` primitives which won't + - Types that are not thread-local must be `Sync`, but only if the `gix-parallel/parallel` is enabled due to the usage of `gix_parallel::…` primitives which won't be thread-safe without the feature. * **when to use shared ownership** - - Use `gix_features::threading::OwnShared` particularly when shared resources supposed to be used by thread-local handles. Going through a wrapper for shared ownership is fast + - Use `gix_parallel::OwnShared` particularly when shared resources supposed to be used by thread-local handles. Going through a wrapper for shared ownership is fast and won't be the bottleneck, as it's only about 16% slower than going through a shared reference on a single core. * **Path encoding** - For `git`, paths are just bytes no matter on which platform. We assume that on windows its path handling goes through some abstraction layer like `MSYS2` diff --git a/README.md b/README.md index 5677085b12a..b95c8e93c9d 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ is usable to some extent. * [gix-commitgraph](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-commitgraph) * [gix-diff](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-diff) * [gix-traverse](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-traverse) - * [gix-features](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-features) + * [gix-parallel](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-parallel) * [gix-credentials](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-credentials) * [gix-sec](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-sec) * [gix-quote](https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md#gix-quote) @@ -342,7 +342,7 @@ Project non-goals can change over time as we learn more, and they can be challen * **use async IO everywhere** * for the most part, git operations are heavily reliant on memory mapped IO as well as CPU to decompress data, which doesn't lend itself well to async IO out of the box. - * Use `blocking` as well as `gix-features::interrupt` to bring operations into the async world and to control + * Use `blocking` as well as `gix_utils::interrupt` to bring operations into the async world and to control long running operations. * When connecting or streaming over TCP connections, especially when receiving on the server, async seems like a must though, but behind a feature flag. diff --git a/STABILITY.md b/STABILITY.md index 503fc073d17..ae56ed8cf39 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -64,7 +64,7 @@ The following schematic helps to visualize what follows. ║ │ │ gix-object │ │ gix-lock │ │ ║ ║ │ └─────────────┘ └─────────────┘ │ ║ │ ║ │ ┌───────────────────────────────┐ │ ║ - ║ │ │ gix-features │ │ ║ │ + ║ │ │ gix-parallel │ │ ║ │ ║ │ └───────────────────────────────┘ │ ║ ║ └───────────────────────────────────┘ ║ │ ║ ║ diff --git a/crate-status.md b/crate-status.md index 6c1dd9f817c..5e3169edcba 100644 --- a/crate-status.md +++ b/crate-status.md @@ -240,10 +240,9 @@ The top-level crate that acts as hub to all functionality provided by the `gix-* * [x] hashset ### gix-utils -* **filesystem** - * [x] probe capabilities - * [x] symlink creation and removal - * [x] file snapshots +* Shared progress, interruption, decoding, iterator, and cache diagnostic utilities. +* Optional **progress**, **interrupt**, and **io-pipe** features enable their dependencies. +* Optional progress unit features select human-readable number and byte formatting. * [ ] **BString Interner with Arena-Backing and arbitrary value association** - probably based on [`internment`](https://docs.rs/internment/latest/internment/struct.Arena.html#), but needs `bumpalo` support to avoid item allocations/boxing, and avoid internal `Mutex`. (key type is pointer based). @@ -1059,9 +1058,8 @@ Provide a reftable backend for refs and reflogs as part of Git 3.0 compatibility [quarantine]: https://github.com/git/git/blob/master/Documentation/git-receive-pack.txt#L223:L223 -### gix-features -* **io-pipe** feature toggle - * a unix like pipeline for bytes +### gix-parallel +* Shared ownership and mutation primitives switch between `Rc`/`RefCell` and `Arc`/locks. * **parallel** feature toggle * _When on…_ * `in_parallel` diff --git a/etc/discovery/odb.md b/etc/discovery/odb.md index 8ce92bb1b06..d8c247f93e2 100644 --- a/etc/discovery/odb.md +++ b/etc/discovery/odb.md @@ -509,7 +509,7 @@ Please note that these are based on the following value system: the latter doesn't even scale that well due to the required dashmap to check for existing objects. In other words, currently there seems to be no actual benchmark for parallel usage. - In single-threaded operation the trait-bounds would prevent creation of packs unless they are adjusted as well, leading to `gix-pack` requiring its own feature toggle - which we really try hard to avoid, but probably can be placed on application level, which has to use that to setup gix-features accordingly, making it bearable. This + which we really try hard to avoid, but probably can be placed on application level, which has to use that to setup gix-parallel accordingly, making it bearable. This means though that we need to implement single-threaded and multi-threaded versions of everything important, like pack generation based on the count (which already has a single-threaded version). - Maybe… after some benchmarks, we can entirely drop the single-threaded version if it's not significantly faster on a single thread (without thread primiives) than the diff --git a/etc/scripts/cargo-check-all.sh b/etc/scripts/cargo-check-all.sh index b9a3a103c07..0481d40d499 100755 --- a/etc/scripts/cargo-check-all.sh +++ b/etc/scripts/cargo-check-all.sh @@ -77,12 +77,18 @@ cargo check -p gix-revision --no-default-features --features sha1,describe cargo check -p gix-mailmap --features serde cargo check -p gix-url --all-features cargo check -p gix-status --all-features -cargo check -p gix-features --all-features -cargo check -p gix-features --features parallel -cargo check -p gix-features --features progress -cargo check -p gix-features --features io-pipe -cargo check -p gix-features --features crc32 -cargo check -p gix-features --features cache-efficiency-debug +cargo check -p gix-parallel --no-default-features +cargo check -p gix-parallel --all-features +cargo check -p gix-parallel --features parallel +cargo check -p gix-parallel --features once_cell +cargo check -p gix-utils --no-default-features +cargo check -p gix-utils --all-features +cargo check -p gix-utils --features progress +cargo check -p gix-utils --features progress,progress-unit-bytes +cargo check -p gix-utils --features progress,progress-unit-human-numbers +cargo check -p gix-utils --features interrupt +cargo check -p gix-utils --features io-pipe +cargo check -p gix-utils --features cache-efficiency-debug cargo check -p gix-fs --no-default-features cargo check -p gix-fs --features walkdir cargo check -p gix-fs --all-features diff --git a/etc/scripts/check-package-size.sh b/etc/scripts/check-package-size.sh index 3a05c237c0e..84bb9cbc38b 100755 --- a/etc/scripts/check-package-size.sh +++ b/etc/scripts/check-package-size.sh @@ -19,7 +19,7 @@ echo 'in root: gitoxide CLI' (enter gix-actor && indent cargo diet -n --package-size-limit 10KB) (enter gix-archive && indent cargo diet -n --package-size-limit 10KB) (enter gix-worktree-stream && indent cargo diet -n --package-size-limit 40KB) -(enter gix-utils && indent cargo diet -n --package-size-limit 10KB) +(enter gix-utils && indent cargo diet -n --package-size-limit 20KB) (enter gix-fs && indent cargo diet -n --package-size-limit 15KB) (enter gix-pathspec && indent cargo diet -n --package-size-limit 30KB) (enter gix-refspec && indent cargo diet -n --package-size-limit 30KB) @@ -38,7 +38,7 @@ echo 'in root: gitoxide CLI' (enter gix-command && indent cargo diet -n --package-size-limit 10KB) (enter gix-hash && indent cargo diet -n --package-size-limit 30KB) (enter gix-chunk && indent cargo diet -n --package-size-limit 15KB) -(enter gix-features && indent cargo diet -n --package-size-limit 65KB) +(enter gix-parallel && indent cargo diet -n --package-size-limit 65KB) (enter gix-ref && indent cargo diet -n --package-size-limit 55KB) (enter gix-diff && indent cargo diet -n --package-size-limit 35KB) (enter gix-traverse && indent cargo diet -n --package-size-limit 15KB) diff --git a/gitoxide-core/src/corpus/engine.rs b/gitoxide-core/src/corpus/engine.rs index be60a08affa..8068b24f957 100644 --- a/gitoxide-core/src/corpus/engine.rs +++ b/gitoxide-core/src/corpus/engine.rs @@ -154,9 +154,8 @@ impl Engine { } else { let counter = repo_progress.counter(); let num_errors = AtomicUsize::default(); - let repo_progress = gix::threading::OwnShared::new(gix::threading::Mutable::new( - repo_progress.add_child("in parallel"), - )); + let repo_progress = + gix::parallel::OwnShared::new(gix::parallel::Mutable::new(repo_progress.add_child("in parallel"))); gix::parallel::in_parallel_with_slice( &mut repos, Some(threads), @@ -164,7 +163,7 @@ impl Engine { let shared_repo_progress = repo_progress.clone(); let db_path = db_path.clone(); move |tid| { - let mut progress = gix::threading::lock(&shared_repo_progress); + let mut progress = gix::parallel::lock(&shared_repo_progress); let lane_progress = progress.add_child(format!("{tid}")); let guard = tracing::dispatcher::set_default(subscriber); (guard, lane_progress, rusqlite::Connection::open(&db_path)) @@ -201,7 +200,7 @@ impl Engine { || (!gix::interrupt::is_triggered()).then(|| Duration::from_millis(100)), drop, )?; - let repo_progress = gix::threading::lock(&repo_progress); + let repo_progress = gix::parallel::lock(&repo_progress); repo_progress.show_throughput(task_start); let num_errors = num_errors.load(Ordering::Relaxed); if num_errors != 0 { diff --git a/gitoxide-core/src/corpus/run.rs b/gitoxide-core/src/corpus/run.rs index 2c8db5c577a..5cdd89dba0d 100644 --- a/gitoxide-core/src/corpus/run.rs +++ b/gitoxide-core/src/corpus/run.rs @@ -86,8 +86,8 @@ impl Execute for WorktreeStream { progress.init(None, gix::progress::bytes()); std::io::copy( &mut stream.into_read(), - &mut gix::features::interrupt::Write { - inner: gix::features::progress::Write { + &mut gix::utils::interrupt::Write { + inner: gix::progress::Write { inner: std::io::sink(), progress, }, diff --git a/gitoxide-core/src/hours/mod.rs b/gitoxide-core/src/hours/mod.rs index 3ecdf3dc69f..3c3a1cee585 100644 --- a/gitoxide-core/src/hours/mod.rs +++ b/gitoxide-core/src/hours/mod.rs @@ -122,7 +122,7 @@ where let commit_id = repo.rev_parse_single(rev_spec)?.detach(); let mut string_heap = BTreeSet::<&'static [u8]>::new(); let needs_stats = file_stats || line_stats; - let threads = gix::features::parallel::num_threads(threads); + let threads = gix::parallel::num_threads(threads); let (commit_authors, stats, is_shallow, skipped_merge_commits, num_commits) = { std::thread::scope(|scope| -> Result<_> { diff --git a/gitoxide-core/src/query/engine/update.rs b/gitoxide-core/src/query/engine/update.rs index 2aa475fd1e3..4d25a99328a 100644 --- a/gitoxide-core/src/query/engine/update.rs +++ b/gitoxide-core/src/query/engine/update.rs @@ -10,9 +10,9 @@ use gix::{ bstr::{BStr, BString, ByteSlice}, diff::{blob::platform::prepare_diff::Operation, rewrites::CopySource}, error::{ResultExt, bail}, - features::progress, parallel::{InOrderIter, SequenceId}, prelude::ObjectIdExt, + progress, }; use rusqlite::{Statement, Transaction, params}; @@ -30,7 +30,7 @@ pub fn update( }: Options, ) -> Result<Vec<gix::ObjectId>> { let commit_id = repo.head_id()?.detach(); - let threads = gix::features::parallel::num_threads(threads); + let threads = gix::parallel::num_threads(threads); let mut stat_progress = { let mut p = progress.add_child("extract stats"); diff --git a/gitoxide-core/src/repository/odb.rs b/gitoxide-core/src/repository/odb.rs index e54f44db65e..1452cc8a515 100644 --- a/gitoxide-core/src/repository/odb.rs +++ b/gitoxide-core/src/repository/odb.rs @@ -174,7 +174,7 @@ pub fn statistics( let mut stats = if gix::parallel::num_threads(thread_limit) > 1 { gix::parallel::in_parallel( gix::interrupt::Iter::new( - gix::features::iter::Chunks { + gix::utils::iter::Chunks { inner: object_ids, size: chunk_size, }, diff --git a/gix-attributes/Cargo.toml b/gix-attributes/Cargo.toml index cd6959ff741..8789a5209be 100644 --- a/gix-attributes/Cargo.toml +++ b/gix-attributes/Cargo.toml @@ -23,10 +23,10 @@ path = "./benches/lookup.rs" ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "bstr/serde", "gix-glob/serde"] ## Enable thread-safety, otherwise the Search cannot be passed between threads. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features" } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } gix-path = { version = "^0.13.0", path = "../gix-path" } gix-quote = { version = "^0.9.0", path = "../gix-quote" } diff --git a/gix-attributes/src/lib.rs b/gix-attributes/src/lib.rs index a03af92aa5b..b825cd7b242 100644 --- a/gix-attributes/src/lib.rs +++ b/gix-attributes/src/lib.rs @@ -98,7 +98,7 @@ pub enum State { /// Enable the `parallel` feature to make this type thread-safe. Without it, the name is backed by an `Rc<str>` and is /// neither `Send` nor `Sync`; with `parallel`, it is backed by an `Arc<str>` instead. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] -pub struct Name(pub(crate) gix_features::threading::OwnShared<str>); +pub struct Name(pub(crate) gix_parallel::OwnShared<str>); /// Holds a validated attribute name as a reference #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)] diff --git a/gix-attributes/src/name.rs b/gix-attributes/src/name.rs index 13e4626cb19..023b3325f7d 100644 --- a/gix-attributes/src/name.rs +++ b/gix-attributes/src/name.rs @@ -2,7 +2,7 @@ use std::borrow::Borrow; use bstr::{BStr, ByteSlice}; use gix_error::{OptionExt, Result, validation}; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{Name, NameRef}; diff --git a/gix-attributes/src/search/mod.rs b/gix-attributes/src/search/mod.rs index 8e8fbf3e4c2..fd412352c06 100644 --- a/gix-attributes/src/search/mod.rs +++ b/gix-attributes/src/search/mod.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use smallvec::SmallVec; use crate::{Assignment, AssignmentRef, Name}; diff --git a/gix-attributes/src/search/outcome.rs b/gix-attributes/src/search/outcome.rs index ecd7d4b8c82..b4f838c436d 100644 --- a/gix-attributes/src/search/outcome.rs +++ b/gix-attributes/src/search/outcome.rs @@ -1,6 +1,6 @@ use bstr::{BString, ByteSlice}; -use gix_features::threading::OwnShared; use gix_glob::Pattern; +use gix_parallel::OwnShared; use crate::{ AssignmentRef, NameRef, StateRef, diff --git a/gix-config/Cargo.toml b/gix-config/Cargo.toml index 6db62239658..e28e95580a1 100644 --- a/gix-config/Cargo.toml +++ b/gix-config/Cargo.toml @@ -22,7 +22,8 @@ sha256 = ["gix-ref/sha256"] serde = ["dep:serde", "bstr/serde", "gix-sec/serde", "gix-ref/serde", "gix-glob/serde", "gix-config-value/serde"] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features" } +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-config-value = { version = "^0.20.0", path = "../gix-config-value" } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } gix-path = { version = "^0.13.0", path = "../gix-path" } diff --git a/gix-config/src/file/access/mutate.rs b/gix-config/src/file/access/mutate.rs index 841b2c2b502..a9d7067f418 100644 --- a/gix-config/src/file/access/mutate.rs +++ b/gix-config/src/file/access/mutate.rs @@ -1,6 +1,6 @@ use bstr::BStr; use gix_error::{Result, ResultExt}; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{ AsBStrOpt, File, diff --git a/gix-config/src/file/access/read_only.rs b/gix-config/src/file/access/read_only.rs index 9a0e1a9ccea..4e061f50ad3 100644 --- a/gix-config/src/file/access/read_only.rs +++ b/gix-config/src/file/access/read_only.rs @@ -1,6 +1,6 @@ use bstr::{BStr, BString, ByteSlice}; use gix_error::Result; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use smallvec::SmallVec; use crate::{ diff --git a/gix-config/src/file/includes/mod.rs b/gix-config/src/file/includes/mod.rs index 27249041603..b02def33a80 100644 --- a/gix-config/src/file/includes/mod.rs +++ b/gix-config/src/file/includes/mod.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_error::{OptionExt, Result, ResultExt, message, not_found}; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use gix_ref::Category; use crate::{ diff --git a/gix-config/src/file/init/from_paths.rs b/gix-config/src/file/init/from_paths.rs index b7cb36f6388..a6992bda24c 100644 --- a/gix-config/src/file/init/from_paths.rs +++ b/gix-config/src/file/init/from_paths.rs @@ -82,7 +82,7 @@ impl File { path.display() )); if options.ignore_io_errors { - gix_features::trace::warn!("ignoring: {err:#?}"); + gix_trace::warn!("ignoring: {err:#?}"); continue; } else { return Err(err); @@ -98,7 +98,7 @@ impl File { path.display() )); if options.ignore_io_errors { - gix_features::trace::warn!("ignoring: {err:#?}"); + gix_trace::warn!("ignoring: {err:#?}"); buf.clear(); } else { return Err(err); diff --git a/gix-config/src/file/init/mod.rs b/gix-config/src/file/init/mod.rs index bec8c813b35..aca5587cd8e 100644 --- a/gix-config/src/file/init/mod.rs +++ b/gix-config/src/file/init/mod.rs @@ -1,5 +1,5 @@ use gix_error::Result; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{ File, diff --git a/gix-config/src/file/mod.rs b/gix-config/src/file/mod.rs index fc6a00bbade..56e016d4a47 100644 --- a/gix-config/src/file/mod.rs +++ b/gix-config/src/file/mod.rs @@ -6,7 +6,7 @@ use std::{ }; use bstr::BString; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; mod mutable; pub use mutable::{multi_value::MultiValueMut, section::SectionMut, value::ValueMut}; diff --git a/gix-config/src/file/section/mod.rs b/gix-config/src/file/section/mod.rs index b6b918eb0a6..91d1a3ba9ba 100644 --- a/gix-config/src/file/section/mod.rs +++ b/gix-config/src/file/section/mod.rs @@ -12,7 +12,7 @@ use crate::{ pub(crate) mod body; pub(crate) use body::BodyData; pub use body::{BodyRef, BodyRefIter}; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::file::{SectionId, write::platform_newline}; diff --git a/gix-config/src/types.rs b/gix-config/src/types.rs index 923041635bc..05878cefea9 100644 --- a/gix-config/src/types.rs +++ b/gix-config/src/types.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{ file, diff --git a/gix-config/tests/config/file/access/mutate.rs b/gix-config/tests/config/file/access/mutate.rs index 3b21d233596..3a44d1b055e 100644 --- a/gix-config/tests/config/file/access/mutate.rs +++ b/gix-config/tests/config/file/access/mutate.rs @@ -26,7 +26,7 @@ mod new_section { mod new_section_with_meta { use gix_config::{File, Source, file::Metadata}; - use gix_features::threading::OwnShared; + use gix_parallel::OwnShared; use gix_testtools::TestResult; #[test] diff --git a/gix-features/Cargo.toml b/gix-features/Cargo.toml deleted file mode 100644 index 2cec9a3a636..00000000000 --- a/gix-features/Cargo.toml +++ /dev/null @@ -1,108 +0,0 @@ -lints.workspace = true - -[package] -name = "gix-features" -description = "A crate to integrate various capabilities using compile-time feature flags" -repository = "https://github.com/GitoxideLabs/gitoxide" -version = "0.50.0" -authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] -license = "MIT OR Apache-2.0" -edition = "2024" -rust-version = "1.88" -include = ["/src/**/*", "/LICENSE-*"] - -[lib] -doctest = false -test = false - -[features] - -default = [] -## Provide traits and utilities for providing progress information. These can then be rendered -## using facilities of the `prodash` crate. -progress = ["prodash"] -## Provide human-readable numbers as well as easier to read byte units for progress bars. -progress-unit-human-numbers = ["prodash?/unit-human"] -## Provide human readable byte units for progress bars. -progress-unit-bytes = ["dep:bytesize", "prodash?/unit-bytes"] - -## Implement `tracing` with `tracing-core`, which provides applications with valuable performance details if they opt-in to it. -## -## Note that this may have overhead as well, thus instrumentations should be used stategically, only providing coarse tracing by default and adding details -## only where needed while marking them with the appropriate level. -tracing = ["gix-trace/tracing"] - -## If enabled, detailed tracing is also emitted, which can greatly increase insights but at a cost. -tracing-detail = ["gix-trace/tracing-detail"] - -## Use scoped threads and channels to parallelize common workloads on multiple objects. If enabled, it is used everywhere -## where it makes sense. -## As caches are likely to be used and instantiated per thread, more memory will be used on top of the costs for threads. -## The `threading` module will contain thread-safe primitives for shared ownership and mutation, otherwise these will be their single threaded counterparts. -## This way, single-threaded applications don't have to pay for threaded primitives. -parallel = ["dep:crossbeam-channel", "dep:parking_lot"] -## If enabled, OnceCell will be made available for interior mutability either in sync or unsync forms. -## Note: This still uses once_cell because std::sync::OnceLock::get_or_try_init() is not yet stable. -once_cell = ["dep:once_cell"] -#* an in-memory unidirectional pipe using `bytes` as efficient transfer mechanism. -io-pipe = ["dep:bytes"] -## provide a proven and fast `crc32` implementation. -crc32 = ["dep:crc32fast"] - -#! ### Other - -## Count cache hits and misses and print that debug information on drop. -## Caches implement this by default, which costs nothing unless this feature is enabled -cache-efficiency-debug = [] - -[[test]] -name = "parallel" -path = "tests/parallel_threaded.rs" -required-features = ["parallel"] - -[[test]] -name = "multi-threaded" -path = "tests/parallel_shared_threaded.rs" -required-features = ["parallel"] - -[[test]] -name = "single-threaded" -path = "tests/parallel_shared.rs" - -[[test]] -name = "pipe" -path = "tests/pipe.rs" -required-features = ["io-pipe"] - -[dependencies] -gix-error = { version = "^0.4.0", path = "../gix-error" } -gix-trace = { version = "^0.2.0", path = "../gix-trace" } - -# 'parallel' feature -crossbeam-channel = { version = "0.5.15", optional = true } -parking_lot = { version = "0.12.4", default-features = false, optional = true } - -# hashing -crc32fast = { version = "1.5.0", optional = true } - -# progress -prodash = { version = "31.0.1", optional = true } -bytesize = { version = "2.3.1", optional = true } - -# pipe -bytes = { version = "1.11.1", optional = true } - -# Note: once_cell is kept for OnceCell type because std::sync::OnceLock::get_or_try_init() is not yet stable. -# Once it's stabilized (tracking issue #109737), we can remove this dependency. -once_cell = { version = "1.21.3", optional = true } - -document-features = { version = "0.2.0", optional = true } - -[package.metadata.docs.rs] -all-features = true -features = ["document-features"] - -[dev-dependencies] -gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } -insta = "1.46.3" -tracing-subscriber = { version = "0.3.22", default-features = false, features = ["registry"] } diff --git a/gix-features/src/hash.rs b/gix-features/src/hash.rs deleted file mode 100644 index 27db01384fe..00000000000 --- a/gix-features/src/hash.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Hash functions and hash utilities - -/// Compute a CRC32 hash from the given `bytes`, returning the CRC32 hash. -/// -/// When calling this function for the first time, `previous_value` should be `0`. -/// Otherwise, it should be the previous return value of this function to provide a hash -/// of multiple sequential chunks of `bytes`. -#[cfg(feature = "crc32")] -pub fn crc32_update(previous_value: u32, bytes: &[u8]) -> u32 { - let mut h = crc32fast::Hasher::new_with_initial(previous_value); - h.update(bytes); - h.finalize() -} - -/// Compute a CRC32 value of the given input `bytes`. -/// -/// In case multiple chunks of `bytes` are present, one should use [`crc32_update()`] instead. -#[cfg(feature = "crc32")] -pub fn crc32(bytes: &[u8]) -> u32 { - let mut h = crc32fast::Hasher::new(); - h.update(bytes); - h.finalize() -} diff --git a/gix-features/src/lib.rs b/gix-features/src/lib.rs deleted file mode 100644 index 89644afc86a..00000000000 --- a/gix-features/src/lib.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! A crate providing foundational capabilities to other `git-*` crates with trade-offs between compile time, binary size or speed -//! selectable using cargo feature toggles. -//! -//! It's designed to allow the application level crate to configure feature toggles, affecting all other `git-*` crates using -//! this one. -//! -//! Thus all features provided here commonly have a 'cheap' base implementation, with the option to pull in -//! counterparts with higher performance. -//! ## Feature Flags -#![cfg_attr( - all(doc, feature = "document-features"), - doc = ::document_features::document_features!() -)] -#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] -#![deny(missing_docs)] - -/// -pub mod cache; -/// -pub mod decode; -pub mod hash; -pub mod interrupt; -#[cfg(feature = "io-pipe")] -pub mod io; -pub mod parallel; -#[cfg(feature = "progress")] -pub mod progress; -pub mod threading; -pub use gix_trace as trace; - -/// -pub mod iter { - /// An iterator over chunks of input, producing `Vec<Item>` with a size of `size`, with the last chunk being the remainder and thus - /// potentially smaller than `size`. - pub struct Chunks<I> { - /// The inner iterator to ask for items. - pub inner: I, - /// The size of chunks to produce - pub size: usize, - } - - impl<I, Item> Iterator for Chunks<I> - where - I: Iterator<Item = Item>, - { - type Item = Vec<Item>; - - fn next(&mut self) -> Option<Self::Item> { - let mut res = Vec::with_capacity(self.size); - let mut items_left = self.size; - for item in &mut self.inner { - res.push(item); - items_left -= 1; - if items_left == 0 { - break; - } - } - (!res.is_empty()).then_some(res) - } - } -} diff --git a/gix-features/tests/features.rs b/gix-features/tests/features.rs deleted file mode 100644 index fc1f25c367b..00000000000 --- a/gix-features/tests/features.rs +++ /dev/null @@ -1 +0,0 @@ -mod trace; diff --git a/gix-features/tests/trace/mod.rs b/gix-features/tests/trace/mod.rs deleted file mode 100644 index eb7b8fd19ce..00000000000 --- a/gix-features/tests/trace/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -use gix_features::trace::{coarse, detail, span}; - -#[cfg(feature = "tracing")] -mod parallel; - -#[test] -fn span() { - let _x = span!(gix_features::trace::Level::Coarse, "hello"); - span!(gix_features::trace::Level::Coarse, "hello", x = "value", y = 42); - span!(target: "other", gix_features::trace::Level::Coarse, "hello", x = "value", y = 42); - - let _x = coarse!("hello"); - coarse!("hello", x = "value", y = 42); - coarse!(target: "other", "hello", x = "value", y = 42); - - let _y = detail!("hello"); - detail!("hello", x = "value", y = 42); - detail!(target: "other", "hello", x = "value", y = 42); -} diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml index 9786da6e812..e9e49009d1b 100644 --- a/gix-fs/Cargo.toml +++ b/gix-fs/Cargo.toml @@ -22,9 +22,9 @@ serde = ["dep:serde"] walkdir = ["dep:walkdir"] [dependencies] +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } bstr = "1.12.0" gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features" } gix-utils = { version = "^0.4.0", path = "../gix-utils" } gix-error = { version = "^0.4.0", path = "../gix-error" } diff --git a/gix-fs/src/snapshot.rs b/gix-fs/src/snapshot.rs index e3db9ffaa5e..f37822ee1b8 100644 --- a/gix-fs/src/snapshot.rs +++ b/gix-fs/src/snapshot.rs @@ -1,7 +1,7 @@ // TODO: tests use std::ops::Deref; -use gix_features::threading::{MutableOnDemand, OwnShared, get_mut, get_ref}; +use gix_parallel::{MutableOnDemand, OwnShared, get_mut, get_ref}; /// A structure holding enough information to reload a value if its on-disk representation changes as determined by its modified time. #[derive(Debug)] diff --git a/gix-glob/Cargo.toml b/gix-glob/Cargo.toml index ce46e25ade9..0700b89d666 100644 --- a/gix-glob/Cargo.toml +++ b/gix-glob/Cargo.toml @@ -24,9 +24,9 @@ path = "./benches/wildmatch.rs" serde = ["dep:serde", "bstr/serde", "bitflags/serde"] [dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace" } gix-utils = { version = "^0.4.0", path = "../gix-utils" } gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features" } gix-fs = { version = "^0.23.0", path = "../gix-fs" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } bitflags = "2" diff --git a/gix-glob/src/wildmatch.rs b/gix-glob/src/wildmatch.rs index 1ac6bbd7c5d..d0eb8ecf2dd 100644 --- a/gix-glob/src/wildmatch.rs +++ b/gix-glob/src/wildmatch.rs @@ -405,7 +405,7 @@ pub(crate) mod function { pub fn wildmatch(pattern: &BStr, value: &BStr, mode: Mode) -> bool { let res = match_recursive(pattern, value, mode, 0); if res == Result::RecursionLimitReached { - gix_features::trace::error!("Recursion limit of {} reached for pattern '{pattern}'", RECURSION_LIMIT); + gix_trace::error!("Recursion limit of {} reached for pattern '{pattern}'", RECURSION_LIMIT); } res == Result::Match } diff --git a/gix-hash/Cargo.toml b/gix-hash/Cargo.toml index 9372833e64a..422d29b6e40 100644 --- a/gix-hash/Cargo.toml +++ b/gix-hash/Cargo.toml @@ -27,7 +27,7 @@ serde = ["dep:serde", "faster-hex/serde"] bstr = ["dep:bstr"] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["progress"] } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["progress"] } gix-error = { version = "^0.4.0", path = "../gix-error" } bstr = { version = "1.12.0", optional = true, default-features = false, features = ["alloc"] } diff --git a/gix-hash/src/io.rs b/gix-hash/src/io.rs index de3f61c7db1..33b38bc67f8 100644 --- a/gix-hash/src/io.rs +++ b/gix-hash/src/io.rs @@ -25,12 +25,12 @@ pub(super) mod _impl { /// /// # Note /// - /// * [Interrupts][gix_features::interrupt] are supported. + /// * Interruption is supported through `should_interrupt`. pub fn bytes_of_file( path: &std::path::Path, num_bytes_from_start: u64, kind: crate::Kind, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result<crate::ObjectId> { bytes( @@ -47,7 +47,7 @@ pub(super) mod _impl { read: &mut dyn std::io::Read, num_bytes_from_start: u64, kind: crate::Kind, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result<crate::ObjectId> { bytes_with_hasher(read, num_bytes_from_start, hasher(kind), progress, should_interrupt) @@ -58,14 +58,14 @@ pub(super) mod _impl { read: &mut dyn std::io::Read, num_bytes_from_start: u64, mut hasher: Hasher, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result<crate::ObjectId> { let start = std::time::Instant::now(); // init progress before the possibility for failure, as convenience in case people want to recover progress.init( - Some(num_bytes_from_start as gix_features::progress::prodash::progress::Step), - gix_features::progress::bytes(), + Some(num_bytes_from_start as gix_utils::progress::prodash::progress::Step), + gix_utils::progress::bytes(), ); const BUF_SIZE: usize = u16::MAX as usize; diff --git a/gix-hash/tests/hash/hasher.rs b/gix-hash/tests/hash/hasher.rs index c9979a2a81a..58ee8248192 100644 --- a/gix-hash/tests/hash/hasher.rs +++ b/gix-hash/tests/hash/hasher.rs @@ -8,7 +8,7 @@ fn interruption_preserves_its_io_error_kind() { &mut &b"x"[..], 1, gix_hash::Kind::shortest(), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::new(true), ) .expect_err("the interrupt flag is observed after reading a chunk"); diff --git a/gix-index/Cargo.toml b/gix-index/Cargo.toml index eaf1b9dd605..2c609dc5f07 100644 --- a/gix-index/Cargo.toml +++ b/gix-index/Cargo.toml @@ -30,12 +30,11 @@ sha256 = ["gix-hash/sha256"] serde = ["dep:serde", "smallvec/serde", "gix-hash/serde"] ## Enable parallel implementations of common algorithms. ## If not set, there can be no parallelism. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features", features = [ - "progress", -] } +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-error = { version = "^0.4.0", path = "../gix-error" } gix-bitmap = { version = "^0.5.0", path = "../gix-bitmap" } @@ -44,7 +43,7 @@ gix-validate = { version = "^0.12.0", path = "../gix-validate" } gix-traverse = { version = "^0.62.0", path = "../gix-traverse" } gix-lock = { version = "^25.0.0", path = "../gix-lock" } gix-fs = { version = "^0.23.0", path = "../gix-fs" } -gix-utils = { version = "^0.4.0", path = "../gix-utils" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["progress"] } hashbrown = "0.17.1" fnv = "1.0.7" diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index cc5b6db566e..420ee7f5420 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -143,7 +143,7 @@ impl State { /// It's required perform any case-insensitive lookup. /// TODO: needs multi-threaded insertion, raw-table to have multiple locks depending on bucket. pub fn prepare_icase_backing(&self) -> AccelerateLookup<'_> { - let _span = gix_features::trace::detail!("prepare_icase_backing", entries = self.entries.len()); + let _span = gix_trace::detail!("prepare_icase_backing", entries = self.entries.len()); let mut out = AccelerateLookup::with_capacity(self.entries.len()); for entry in &self.entries { let entry_path = entry.path(self); @@ -188,7 +188,7 @@ impl State { } } } - gix_features::trace::debug!(directories = out.icase_dirs.len(), "stored directories"); + gix_trace::debug!(directories = out.icase_dirs.len(), "stored directories"); out } diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index 7476e6273e8..a6b5dbab5ed 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -9,7 +9,7 @@ mod entries; pub mod header; use gix_error::{ResourceExhaustionKind, ResultExt, bail, corruption, message}; -use gix_features::parallel::InOrderIter; +use gix_parallel::InOrderIter; use crate::util::read_u32; @@ -49,7 +49,7 @@ impl State { alloc_limit_bytes, }: Options, ) -> Result<(Self, Option<gix_hash::ObjectId>)> { - let _span = gix_features::trace::detail!("gix_index::State::from_bytes()", options = ?_options); + let _span = gix_trace::detail!("gix_index::State::from_bytes()", options = ?_options); let (version, num_entries, post_header_data) = header::decode(data, object_hash)?; let start_of_extensions = extension::end_of_index_entry::decode(data, object_hash)?; if num_entries as usize > entries::max_entries_possible(data.len(), start_of_extensions, object_hash, version) { @@ -58,7 +58,7 @@ impl State { )); } - let mut num_threads = gix_features::parallel::num_threads(thread_limit); + let mut num_threads = gix_parallel::num_threads(thread_limit); let path_backing_buffer_size = entries::estimate_path_storage_requirements_in_bytes( num_entries, data.len(), @@ -78,16 +78,16 @@ impl State { Some(offset) if num_threads > 1 => { let extensions_data = &data[offset..]; let index_offsets_table = extension::index_entry_offset_table::find(extensions_data, object_hash); - let (entries_res, ext_res) = gix_features::parallel::threads(|scope| { + let (entries_res, ext_res) = gix_parallel::threads(|scope| { let extension_loading = (extensions_data.len() > min_extension_block_in_bytes_for_threading).then({ num_threads -= 1; || { - gix_features::parallel::build_thread() + gix_parallel::build_thread() .name("gix-index.from_bytes.load-extensions".into()) .spawn_scoped( scope, - gix_features::trace::in_thread(|| { + gix_trace::in_thread(|| { extension::decode::all(extensions_data, object_hash, alloc_limit_bytes) }), ) @@ -103,11 +103,11 @@ impl State { for (id, chunks) in entry_offsets_chunked.enumerate() { let chunks = chunks.to_vec(); threads.push( - gix_features::parallel::build_thread() + gix_parallel::build_thread() .name(format!("gix-index.from_bytes.read-entries.{id}")) .spawn_scoped( scope, - gix_features::trace::in_thread(move || { + gix_trace::in_thread(move || { let num_entries_for_chunks = chunks.iter().map(|c| c.num_entries).sum::<u32>() as usize; let mut entries = vec_with_capacity(num_entries_for_chunks)?; diff --git a/gix-index/src/extension/tree/verify.rs b/gix-index/src/extension/tree/verify.rs index e2acf9afee7..ecaab679228 100644 --- a/gix-index/src/extension/tree/verify.rs +++ b/gix-index/src/extension/tree/verify.rs @@ -81,7 +81,7 @@ impl Tree { } Ok(entries.into()) } - let _span = gix_features::trace::coarse!("gix_index::extension::Tree::verify()"); + let _span = gix_trace::coarse!("gix_index::extension::Tree::verify()"); if !self.name.is_empty() { bail!(corruption(format!( diff --git a/gix-index/src/file/init.rs b/gix-index/src/file/init.rs index d34a3a15b51..a83b85c6537 100644 --- a/gix-index/src/file/init.rs +++ b/gix-index/src/file/init.rs @@ -67,7 +67,7 @@ impl File { skip_hash: bool, options: decode::Options, ) -> Result<Self> { - let _span = gix_features::trace::detail!("gix_index::File::at()"); + let _span = gix_trace::detail!("gix_index::File::at()"); let path = path.into(); let (data, mtime) = { let mut file = std::fs::File::open(&path) @@ -90,7 +90,7 @@ impl File { let expected = gix_hash::ObjectId::from_bytes_or_panic(&data[data.len() - object_hash.len_in_bytes()..]); if !expected.is_null() { - let _span = gix_features::trace::detail!("gix::open_index::hash_index", path = ?path); + let _span = gix_trace::detail!("gix::open_index::hash_index", path = ?path); let meta = file .metadata() .or_raise(|| message("An IO error occurred while opening the index"))?; @@ -99,7 +99,7 @@ impl File { &mut file, num_bytes_to_hash, object_hash, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &Default::default(), )? .verify(&expected) diff --git a/gix-index/src/file/verify.rs b/gix-index/src/file/verify.rs index b92abc0ff00..905af6bc85e 100644 --- a/gix-index/src/file/verify.rs +++ b/gix-index/src/file/verify.rs @@ -8,7 +8,7 @@ impl File { pub fn verify_integrity(&self) -> Result { use gix_error::{ResultExt, message}; - let _span = gix_features::trace::coarse!("gix_index::File::verify_integrity()"); + let _span = gix_trace::coarse!("gix_index::File::verify_integrity()"); if let Some(checksum) = self.checksum { let num_bytes_to_hash = self .path @@ -21,7 +21,7 @@ impl File { &self.path, num_bytes_to_hash, checksum.kind(), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &should_interrupt, ) .or_raise(|| message("Could not read index file to generate hash"))? diff --git a/gix-index/src/file/write.rs b/gix-index/src/file/write.rs index 1a05d40d0b1..94e1109ad94 100644 --- a/gix-index/src/file/write.rs +++ b/gix-index/src/file/write.rs @@ -13,7 +13,7 @@ impl File { mut out: impl std::io::Write, options: write::Options, ) -> Result<(Version, gix_hash::ObjectId)> { - let _span = gix_features::trace::detail!("gix_index::File::write_to()", skip_hash = options.skip_hash); + let _span = gix_trace::detail!("gix_index::File::write_to()", skip_hash = options.skip_hash); let (version, hash) = if options.skip_hash { let out: &mut dyn std::io::Write = &mut out; let version = self.state.write_to(out, options)?; @@ -56,7 +56,7 @@ impl File { pub fn write(&mut self, options: write::Options) -> Result { use gix_error::{ErrorExt, ResultExt, bail, message}; - let _span = gix_features::trace::detail!("gix_index::File::write()", path = ?self.path); + let _span = gix_trace::detail!("gix_index::File::write()", path = ?self.path); let mut lock = std::io::BufWriter::with_capacity( 64 * 1024, gix_lock::File::acquire_to_update_resource(&self.path, gix_lock::acquire::Fail::Immediately, None, 0) diff --git a/gix-index/src/init.rs b/gix-index/src/init.rs index 54c8ba51792..c46ca41628d 100644 --- a/gix-index/src/init.rs +++ b/gix-index/src/init.rs @@ -57,7 +57,7 @@ pub mod from_tree { where Find: gix_object::Find, { - let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); + let _span = gix_trace::coarse!("gix_index::State::from_tree()"); let mut delegate = CollectEntries::new(validate); let traversal = depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate); diff --git a/gix-index/src/lib.rs b/gix-index/src/lib.rs index d29ddccca30..98455fc2f3c 100644 --- a/gix-index/src/lib.rs +++ b/gix-index/src/lib.rs @@ -244,7 +244,7 @@ mod impls { pub(crate) mod util { #[inline] pub fn var_int(data: &[u8]) -> Option<(u64, &[u8])> { - let (num, consumed) = gix_features::decode::leb64_from_read(data).ok()?; + let (num, consumed) = gix_utils::decode::leb64_from_read(data).ok()?; let data = &data[consumed..]; (num, data).into() } diff --git a/gix-index/src/verify.rs b/gix-index/src/verify.rs index a3cb238793a..d19a386cf52 100644 --- a/gix-index/src/verify.rs +++ b/gix-index/src/verify.rs @@ -6,7 +6,7 @@ use crate::State; impl State { /// Assure our entries are consistent. pub fn verify_entries(&self) -> Result { - let _span = gix_features::trace::coarse!("gix_index::File::verify_entries()"); + let _span = gix_trace::coarse!("gix_index::File::verify_entries()"); let mut previous = None::<&crate::Entry>; for (idx, entry) in self.entries.iter().enumerate() { if let Some(prev) = previous diff --git a/gix-index/src/write.rs b/gix-index/src/write.rs index aaf5defd30d..b10dcb2e2b6 100644 --- a/gix-index/src/write.rs +++ b/gix-index/src/write.rs @@ -73,7 +73,7 @@ impl State { skip_hash: _, }: Options, ) -> Result<Version> { - let _span = gix_features::trace::detail!("gix_index::State::write()"); + let _span = gix_trace::detail!("gix_index::State::write()"); let version = self.detect_required_version(); let mut write = CountBytes::new(out); diff --git a/gix-object/Cargo.toml b/gix-object/Cargo.toml index 6303a7cfea9..4b186329409 100644 --- a/gix-object/Cargo.toml +++ b/gix-object/Cargo.toml @@ -41,15 +41,12 @@ serde = [ "gix-actor/serde", ] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features", features = [ - "progress", -] } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-hashtable = { version = "^0.17.0", path = "../gix-hashtable" } gix-validate = { version = "^0.12.0", path = "../gix-validate" } gix-actor = { version = "^0.43.0", path = "../gix-actor" } gix-date = { version = "^0.17.0", path = "../gix-date" } -gix-utils = { version = "^0.4.0", path = "../gix-utils" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["progress"] } gix-command = { version = "^0.11.0", path = "../gix-command", optional = true } gix-path = { version = "^0.13.0", path = "../gix-path", optional = true } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } diff --git a/gix-object/src/find.rs b/gix-object/src/find.rs index 9f45e4c3b77..d5412fd1cb6 100644 --- a/gix-object/src/find.rs +++ b/gix-object/src/find.rs @@ -109,7 +109,7 @@ impl super::Write for Never { kind, from, size, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::new(false), ) } diff --git a/gix-object/src/lib.rs b/gix-object/src/lib.rs index 73323700ca2..12e381c72ef 100644 --- a/gix-object/src/lib.rs +++ b/gix-object/src/lib.rs @@ -367,7 +367,7 @@ pub fn compute_stream_hash( object_kind: Kind, stream: &mut dyn std::io::Read, stream_len: u64, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, should_interrupt: &std::sync::atomic::AtomicBool, ) -> Result<gix_hash::ObjectId> { let hasher = object_hasher(hash_kind, object_kind, stream_len); diff --git a/gix-object/tests/object/main.rs b/gix-object/tests/object/main.rs index 0ee517b569e..4d507f51450 100644 --- a/gix-object/tests/object/main.rs +++ b/gix-object/tests/object/main.rs @@ -34,7 +34,7 @@ fn compute_stream_hash() -> gix_testtools::TestResult { gix_object::Kind::Blob, &mut &[][..], 0, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::default() )?, gix_hash::ObjectId::empty_blob(*hk) @@ -45,7 +45,7 @@ fn compute_stream_hash() -> gix_testtools::TestResult { gix_object::Kind::Tree, &mut &[][..], 0, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::default() )?, gix_hash::ObjectId::empty_tree(*hk) diff --git a/gix-odb/Cargo.toml b/gix-odb/Cargo.toml index c3e145391b3..78e37712dbe 100644 --- a/gix-odb/Cargo.toml +++ b/gix-odb/Cargo.toml @@ -23,11 +23,14 @@ sha256 = ["gix-hash/sha256", "gix-pack/sha256"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-hash/serde", "gix-object/serde", "gix-pack/serde"] ## Enable support for multi-threaded usage of the object database. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] +crc32fast = "1.5.0" +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["progress"] } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-error = { version = "^0.4.0", path = "../gix-error" } -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["crc32"] } gix-zlib = { version = "^0.2.0", path = "../gix-zlib" } gix-hashtable = { version = "^0.17.0", path = "../gix-hashtable" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } diff --git a/gix-odb/src/lib.rs b/gix-odb/src/lib.rs index 77911cca530..a6e3f5b2adc 100644 --- a/gix-odb/src/lib.rs +++ b/gix-odb/src/lib.rs @@ -68,8 +68,8 @@ use std::{ }; use arc_swap::ArcSwap; -use gix_features::threading::OwnShared; pub use gix_pack as pack; +use gix_parallel::OwnShared; use gix_zlib::stream::deflate; mod store_impls; @@ -128,7 +128,7 @@ pub use traits::{Header, HeaderExt}; /// A thread-local handle to access any object. pub type Handle = Cache<store::Handle<OwnShared<Store>>>; -/// A thread-local handle to access any object, but thread-safe and independent of the actual type of `OwnShared` or feature toggles in `gix-features`. +/// A thread-local handle to access any object, but thread-safe and independent of the actual type of `OwnShared` or feature toggles in `gix-parallel`. pub type HandleArc = Cache<store::Handle<Arc<Store>>>; use store::types; diff --git a/gix-odb/src/store_impls/dynamic/handle.rs b/gix-odb/src/store_impls/dynamic/handle.rs index a7b66aa05f0..d0b3084741b 100644 --- a/gix-odb/src/store_impls/dynamic/handle.rs +++ b/gix-odb/src/store_impls/dynamic/handle.rs @@ -6,8 +6,8 @@ use std::{ }; use gix_error::Result; -use gix_features::threading::OwnShared; use gix_hash::oid; +use gix_parallel::OwnShared; use crate::store::{RefreshMode, handle, types}; @@ -247,7 +247,7 @@ impl super::Store { /// Create a new cache filled with a handle to this store, if this store is supporting shared ownership. /// - /// Note that the actual type of `OwnShared` depends on the `parallel` feature toggle of the `gix-features` crate. + /// Note that the actual type of `OwnShared` depends on the `parallel` feature toggle of the `gix-parallel` crate. pub fn to_cache(self: &OwnShared<Self>) -> crate::Cache<super::Handle<OwnShared<super::Store>>> { self.to_handle().into() } @@ -296,7 +296,7 @@ impl super::Store { /// Transform the only instance into an `Arc<Self>` or panic if this is not the only Rc handle /// to the contained store. /// - /// This is meant to be used when the `gix_features::threading::OwnShared` refers to an `Rc` as it was compiled without the + /// This is meant to be used when the `gix_parallel::OwnShared` refers to an `Rc` as it was compiled without the /// `parallel` feature toggle. pub fn into_shared_arc(self: OwnShared<Self>) -> Arc<Self> { match OwnShared::try_unwrap(self) { diff --git a/gix-odb/src/store_impls/dynamic/init.rs b/gix-odb/src/store_impls/dynamic/init.rs index 30edb6c728a..fee0479c201 100644 --- a/gix-odb/src/store_impls/dynamic/init.rs +++ b/gix-odb/src/store_impls/dynamic/init.rs @@ -90,7 +90,7 @@ impl Store { loose_compression, }: Options, ) -> std::io::Result<Self> { - let _span = gix_features::trace::detail!("gix_odb::Store::at()"); + let _span = gix_trace::detail!("gix_odb::Store::at()"); let current_dir = current_dir.map_or_else( || { // It's only used for real-pathing alternate paths and there it just needs to be consistent (enough). diff --git a/gix-odb/src/store_impls/dynamic/load_index.rs b/gix-odb/src/store_impls/dynamic/load_index.rs index fb99a156a1c..7d4f91d3767 100644 --- a/gix-odb/src/store_impls/dynamic/load_index.rs +++ b/gix-odb/src/store_impls/dynamic/load_index.rs @@ -124,7 +124,7 @@ impl super::Store { // TODO(odb-parallelism): consider to search fresh indices, // then retry all changed failed slots // before returning the first error. Persistent failures must not advance state_id. - gix_features::trace::error!(err=?_err, "Failed to load index file - some objects may seem to not exist"); + gix_trace::error!(err=?_err, "Failed to load index file - some objects may seem to not exist"); continue 'retry_with_next_slot_index; } } diff --git a/gix-odb/src/store_impls/dynamic/prefix.rs b/gix-odb/src/store_impls/dynamic/prefix.rs index 6b317ca4fc9..7a55015eb84 100644 --- a/gix-odb/src/store_impls/dynamic/prefix.rs +++ b/gix-odb/src/store_impls/dynamic/prefix.rs @@ -67,7 +67,7 @@ where match *count { Some(count) => Ok(count), None => { - let _span = gix_features::trace::detail!("gix_odb::Handle::packed_object_count()"); + let _span = gix_trace::detail!("gix_odb::Handle::packed_object_count()"); let mut snapshot = self.snapshot.borrow_mut(); *snapshot = self.store.load_all_indices()?; let mut obj_count = 0; diff --git a/gix-odb/src/store_impls/dynamic/structure.rs b/gix-odb/src/store_impls/dynamic/structure.rs index aa14a1734da..7907a6c61be 100644 --- a/gix-odb/src/store_impls/dynamic/structure.rs +++ b/gix-odb/src/store_impls/dynamic/structure.rs @@ -53,7 +53,7 @@ impl Store { /// implementation. The likelihood of actual changes is low though as these still depend on something /// changing on disk and somebody reading at the same time. pub fn structure(&self) -> Result<Vec<Record>> { - let _span = gix_features::trace::detail!("gix_odb::Store::structure()"); + let _span = gix_trace::detail!("gix_odb::Store::structure()"); let index = self.index.load(); if !index.is_initialized() { self.consolidate_with_disk_state(true, false /*load one new index*/, self.loose_compression)?; diff --git a/gix-odb/src/store_impls/dynamic/types.rs b/gix-odb/src/store_impls/dynamic/types.rs index 4aa804171cb..8e6e6650e50 100644 --- a/gix-odb/src/store_impls/dynamic/types.rs +++ b/gix-odb/src/store_impls/dynamic/types.rs @@ -8,7 +8,6 @@ use std::{ }; use arc_swap::ArcSwap; -use gix_features::hash; /// An id to refer to an index file or a multipack index file pub type IndexId = usize; @@ -111,8 +110,10 @@ impl SlotMapIndex { pub(crate) fn state_id(self: &Arc<SlotMapIndex>) -> StateId { // We let the loaded indices take part despite not being part of our own snapshot. // This is to account for indices being loaded in parallel without actually changing the snapshot itself. - let hash = hash::crc32(&(Arc::as_ptr(self) as usize).to_be_bytes()); - hash::crc32_update(hash, &self.loaded_indices.load(Ordering::SeqCst).to_be_bytes()) + let mut hash = crc32fast::Hasher::new(); + hash.update(&(Arc::as_ptr(self) as usize).to_be_bytes()); + hash.update(&self.loaded_indices.load(Ordering::SeqCst).to_be_bytes()); + hash.finalize() } pub(crate) fn marker(self: &Arc<SlotMapIndex>) -> SlotIndexMarker { diff --git a/gix-odb/src/store_impls/dynamic/verify.rs b/gix-odb/src/store_impls/dynamic/verify.rs index 2bade7f2131..630aa53c6dd 100644 --- a/gix-odb/src/store_impls/dynamic/verify.rs +++ b/gix-odb/src/store_impls/dynamic/verify.rs @@ -7,7 +7,7 @@ use std::{ use gix_error::{Message, ResultExt}; -use gix_features::progress::{DynNestedProgress, MessageLevel, Progress}; +use gix_utils::progress::{DynNestedProgress, MessageLevel, Progress}; use crate::{ pack, @@ -74,7 +74,7 @@ pub mod integrity { VerifyMultiIndex(PhantomData<gix_pack::multi_index::verify::integrity::ProgressId>), } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::VerifyLooseObjectDbPath => *b"VISP", @@ -103,7 +103,7 @@ impl super::Store { F: Fn() -> C + Send + Clone, { let changed = || gix_error::message("The object database changed during verification").retryable_error(); - let _span = gix_features::trace::coarse!("gix_odb:Store::verify_integrity()"); + let _span = gix_trace::coarse!("gix_odb:Store::verify_integrity()"); let mut index = self.index.load(); if !index.is_initialized() { self.consolidate_with_disk_state(true, false, self.loose_compression)?; @@ -116,7 +116,7 @@ impl super::Store { progress.init( Some(index.slot_indices.len()), - gix_features::progress::count("pack indices"), + gix_utils::progress::count("pack indices"), ); let mut statistics = Vec::new(); let index_check_message = |path: &std::path::Path| { @@ -126,7 +126,7 @@ impl super::Store { .map_or_else(Default::default, std::ffi::OsStr::to_string_lossy) ) }; - gix_features::trace::detail!("verify indices").into_scope(|| { + gix_trace::detail!("verify indices").into_scope(|| { for slot_index in &index.slot_indices { let slot = &self.files[*slot_index]; if slot.generation.load(Ordering::SeqCst) != index.generation { @@ -230,7 +230,7 @@ impl super::Store { child_progress.show_throughput_with( start, num_objects as usize, - gix_features::progress::count("objects").expect("set"), + gix_utils::progress::count("objects").expect("set"), MessageLevel::Success, ); progress.inc(); @@ -240,10 +240,10 @@ impl super::Store { progress.init( Some(index.loose_dbs.len()), - gix_features::progress::count("loose object stores"), + gix_utils::progress::count("loose object stores"), ); let mut loose_object_stores = Vec::new(); - gix_features::trace::detail!("verify loose ODBs").into_scope(|| -> Result<_> { + gix_trace::detail!("verify loose ODBs").into_scope(|| -> Result<_> { for loose_db in &*index.loose_dbs { let out = loose_db .verify_integrity( diff --git a/gix-odb/src/store_impls/loose/verify.rs b/gix-odb/src/store_impls/loose/verify.rs index cfb67e7ded0..d620f55107f 100644 --- a/gix-odb/src/store_impls/loose/verify.rs +++ b/gix-odb/src/store_impls/loose/verify.rs @@ -6,7 +6,7 @@ use std::{ use gix_error::{Class, ClassificationMarker, Message, OptionExt, ResultExt, bail, message, retryable}; -use gix_features::progress::{Count, DynNestedProgress, Progress}; +use gix_utils::progress::{Count, DynNestedProgress, Progress}; use crate::loose::Store; @@ -29,7 +29,7 @@ pub mod integrity { LooseObjects, } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::LooseObjects => *b"VILO", @@ -52,7 +52,7 @@ impl Store { let mut num_objects = 0; let start = Instant::now(); let mut progress = progress.add_child_with_id("Validating".into(), integrity::ProgressId::LooseObjects.into()); - progress.init(None, gix_features::progress::count("loose objects")); + progress.init(None, gix_utils::progress::count("loose objects")); for id in self.iter() { let id = id.or_raise(|| message("Could not enumerate loose objects"))?; let object = self diff --git a/gix-odb/tests/odb/error.rs b/gix-odb/tests/odb/error.rs index 7ab3e8e4aab..a1f120fa7a7 100644 --- a/gix-odb/tests/odb/error.rs +++ b/gix-odb/tests/odb/error.rs @@ -569,12 +569,8 @@ fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::Tes id: delta_id, entry_pack_location: output::count::PackLocation::NotLookedUp, }; - let mut valid_entries = output::entry::iter_from_counts( - vec![count.clone()], - db, - Box::new(gix_features::progress::Discard), - options, - )?; + let mut valid_entries = + output::entry::iter_from_counts(vec![count.clone()], db, Box::new(gix_utils::progress::Discard), options)?; let (_, generated) = valid_entries.next().expect("one input object produces one chunk")?; assert_eq!( generated.len(), @@ -614,7 +610,7 @@ fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::Tes entry_pack_location: output::count::PackLocation::NotLookedUp, }], db.clone(), - Box::new(gix_features::progress::Discard), + Box::new(gix_utils::progress::Discard), options, ) .err() @@ -625,7 +621,7 @@ fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::Tes let mut entries = output::entry::iter_from_counts( vec![count.clone()], db.clone(), - Box::new(gix_features::progress::Discard), + Box::new(gix_utils::progress::Discard), options, )?; let err = entries @@ -636,7 +632,7 @@ fn multi_index_pack_generation_is_fallible(large_offset: bool) -> gix_error::Tes assert!(!err.is_not_found(), "iteration corruption is not a missing delta base"); drop(entries); - let entries = output::entry::iter_from_counts(vec![count], db, Box::new(gix_features::progress::Discard), options)?; + let entries = output::entry::iter_from_counts(vec![count], db, Box::new(gix_utils::progress::Discard), options)?; let mut writer = output::bytes::FromEntriesIter::new( entries.map(|chunk| chunk.map(|(_, entries)| entries)), Vec::new(), @@ -932,7 +928,7 @@ fn multi_index_ref_delta_preserves_corrupt_base_lookup() -> gix_testtools::TestR gix_pack::multi_index::write_from_index_paths( vec![index_path], &mut multi_index, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::default(), gix_pack::multi_index::write::Options { object_hash }, )?; @@ -996,7 +992,7 @@ fn disappearing_loose_objects_keep_retryable_diagnostics() -> gix_testtools::Tes for (err, num_nodes) in [ ( loose - .verify_integrity(&mut gix_features::progress::Discard, &AtomicBool::new(false)) + .verify_integrity(&mut gix_utils::progress::Discard, &AtomicBool::new(false)) .expect_err("the enumerated object is missing"), 1, ), @@ -1004,7 +1000,7 @@ fn disappearing_loose_objects_keep_retryable_diagnostics() -> gix_testtools::Tes dynamic .store_ref() .verify_integrity( - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), Default::default(), ) diff --git a/gix-odb/tests/odb/store/dynamic.rs b/gix-odb/tests/odb/store/dynamic.rs index 287118cb37f..e91d6e97871 100644 --- a/gix-odb/tests/odb/store/dynamic.rs +++ b/gix-odb/tests/odb/store/dynamic.rs @@ -62,7 +62,7 @@ fn db_with_all_object_sources() -> Result<(gix_odb::Handle, gix_testtools::tempf fixture_path("objects/pack/pack-c0438c19fb16422b6bbcce24387b3264416d485b.idx"), ], &mut multi_pack_index, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::default(), gix_odb::pack::multi_index::write::Options { object_hash: gix_hash::Kind::Sha1, @@ -1100,8 +1100,8 @@ fn auto_refresh_with_and_without_id_stability() -> gix_testtools::TestResult { mod verify { use std::sync::atomic::AtomicBool; - use gix_features::progress; use gix_testtools::fixture_path; + use gix_utils::progress; use crate::store::dynamic::db; diff --git a/gix-odb/tests/odb/store/loose.rs b/gix-odb/tests/odb/store/loose.rs index 7596c6e1238..902b54ce8a8 100644 --- a/gix-odb/tests/odb/store/loose.rs +++ b/gix-odb/tests/odb/store/loose.rs @@ -1,8 +1,8 @@ use std::{path::PathBuf, sync::atomic::AtomicBool}; -use gix_features::progress; use gix_odb::loose::{Options, Store}; use gix_testtools::fixture_path; +use gix_utils::progress; use pretty_assertions::assert_eq; use crate::hex_to_id; @@ -228,7 +228,7 @@ mod write { } } - gix_features::parallel::threads(|scope| { + gix_parallel::threads(|scope| { scope.spawn(|| write_empty_trees(dir.path())); scope.spawn(|| write_empty_trees(dir.path())); }); @@ -370,7 +370,7 @@ mod find { assert!(db.try_header(&id).is_err(), "it must not panic"); let err = db .verify_integrity( - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::new(false), ) .expect_err("verification must report the invalid object"); @@ -709,7 +709,7 @@ cjHJZXWmV4CcRfmLsXzU8s2cR9A0DBvOxhPD1TlKC2JhBFXigjuL9U4Rbq9tdegB "conversion diagnostics retain the full requested object size in prose" ); insta::assert_debug_snapshot!(gix_testtools::redact_debug_snapshot(&err, &[(&path.to_string_lossy(), "<object-path>")]), "unrepresentable sizes retain the failed integer conversion", @r#" - Could not read loose object, "path"="<object-path>" + Could not read loose object, path="<object-path>" Caused by: 0: Cannot store loose object of 18446744073709551615 bytes in memory: the object size cannot be represented in memory diff --git a/gix-pack/Cargo.toml b/gix-pack/Cargo.toml index b4f9df36276..df8f0ea7ff5 100644 --- a/gix-pack/Cargo.toml +++ b/gix-pack/Cargo.toml @@ -38,14 +38,17 @@ object-cache-dynamic = ["dep:clru", "dep:gix-hashtable"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-object/serde", "gix-zlib/serde"] ## Enable parallel algorithms. -parallel = ["gix-features/parallel", "dep:crossbeam-deque"] +parallel = ["gix-parallel/parallel", "dep:crossbeam-deque"] ## Make it possible to compile to the `wasm32-unknown-unknown` target. wasm = ["gix-diff?/wasm"] ## Add test-utilities for tests and benchmarks. testing = [] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["crc32", "progress"] } +crc32fast = "1.5.0" +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["interrupt", "progress"] } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-zlib = { version = "^0.2.0", path = "../gix-zlib" } gix-path = { version = "^0.13.0", path = "../gix-path" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } diff --git a/gix-pack/benches/pack_create.rs b/gix-pack/benches/pack_create.rs index 35c85f95942..dddca0feb77 100644 --- a/gix-pack/benches/pack_create.rs +++ b/gix-pack/benches/pack_create.rs @@ -10,12 +10,13 @@ use std::{io, sync::atomic::AtomicBool}; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group}; -use gix_features::{parallel::InOrderIter, progress}; use gix_object::Write as _; use gix_pack::{ data::output::{self, bytes::FromEntriesIter, count, entry}, testing::Memory, }; +use gix_parallel::InOrderIter; +use gix_utils::progress; /// Object counts to exercise. Kept modest so the benchmark stays runnable while still showing how /// the phases scale; raise locally to probe larger, more degenerate repositories. diff --git a/gix-pack/fuzz/Cargo.toml b/gix-pack/fuzz/Cargo.toml index eaec384649a..76fae73818b 100644 --- a/gix-pack/fuzz/Cargo.toml +++ b/gix-pack/fuzz/Cargo.toml @@ -8,13 +8,10 @@ edition = "2024" cargo-fuzz = true [dependencies] +gix-utils = { version = "^0.4.0", path = "../../gix-utils", features = ["progress"] } gix-error = { version = "^0.4.0", path = "../../gix-error" } libfuzzer-sys = "0.4" -[dependencies.gix-features] -path = "../../gix-features" -features = ["progress"] - [dependencies.gix-zlib] path = "../../gix-zlib" diff --git a/gix-pack/fuzz/fuzz_targets/data_file.rs b/gix-pack/fuzz/fuzz_targets/data_file.rs index b416107a84d..4672dc91ded 100644 --- a/gix-pack/fuzz/fuzz_targets/data_file.rs +++ b/gix-pack/fuzz/fuzz_targets/data_file.rs @@ -1,7 +1,7 @@ #![no_main] use gix_error::Result; -use gix_features::progress; +use gix_utils::progress; use gix_pack::{cache, data}; use gix_pack_fuzz::{interrupt_flag, virtual_path}; use libfuzzer_sys::fuzz_target; diff --git a/gix-pack/fuzz/fuzz_targets/index_file.rs b/gix-pack/fuzz/fuzz_targets/index_file.rs index a9438f7e58d..a2b9f8130c7 100644 --- a/gix-pack/fuzz/fuzz_targets/index_file.rs +++ b/gix-pack/fuzz/fuzz_targets/index_file.rs @@ -1,7 +1,7 @@ #![no_main] use gix_error::Result; -use gix_features::progress; +use gix_utils::progress; use gix_hash::Prefix; use gix_pack::index; use gix_pack_fuzz::{empty_candidates, interrupt_flag, virtual_path}; diff --git a/gix-pack/fuzz/fuzz_targets/multi_index_file.rs b/gix-pack/fuzz/fuzz_targets/multi_index_file.rs index 33ed7c6899e..df313f8c55e 100644 --- a/gix-pack/fuzz/fuzz_targets/multi_index_file.rs +++ b/gix-pack/fuzz/fuzz_targets/multi_index_file.rs @@ -1,7 +1,7 @@ #![no_main] use gix_error::Result; -use gix_features::progress; +use gix_utils::progress; use gix_hash::Prefix; use gix_pack::multi_index; use gix_pack_fuzz::{empty_candidates, interrupt_flag, virtual_path}; diff --git a/gix-pack/src/bundle/mod.rs b/gix-pack/src/bundle/mod.rs index 01d3ca80dcd..f1128b27038 100644 --- a/gix-pack/src/bundle/mod.rs +++ b/gix-pack/src/bundle/mod.rs @@ -11,7 +11,7 @@ pub mod verify { use gix_error::Result; use std::sync::atomic::AtomicBool; - use gix_features::progress::DynNestedProgress; + use gix_utils::progress::DynNestedProgress; /// pub mod integrity { diff --git a/gix-pack/src/bundle/write/mod.rs b/gix-pack/src/bundle/write/mod.rs index e70207b3baf..33eaacf6652 100644 --- a/gix-pack/src/bundle/write/mod.rs +++ b/gix-pack/src/bundle/write/mod.rs @@ -8,12 +8,14 @@ use std::{ }; use gix_error::{ResultExt, message}; -use gix_features::{interrupt, progress, progress::Progress}; use gix_tempfile::{AutoRemove, ContainingDirectory}; +use gix_utils::interrupt; +use gix_utils::progress; +use gix_utils::progress::Progress; use crate::data; -use gix_features::progress::prodash::DynNestedProgress; +use gix_utils::progress::prodash::DynNestedProgress; mod types; use types::{LockWriter, PassThrough}; @@ -34,7 +36,7 @@ pub enum ProgressId { IndexingSteps(PhantomData<crate::index::write::ProgressId>), } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::ReadPackBytes => *b"BWRB", @@ -48,7 +50,7 @@ impl crate::Bundle { /// /// In the latter case, the functionality provided here is more a kind of pack data stream validation. /// - /// * `progress` provides detailed progress information which can be discarded with [`gix_features::progress::Discard`]. + /// * `progress` provides detailed progress information which can be discarded with [`gix_utils::progress::Discard`]. /// * `should_interrupt` is checked regularly and when true, the whole operation will stop. /// * `thin_pack_base_object_lookup` retrieves external bases to insert into the output pack, completing a thin pack. /// Ref-deltas whose bases are not found through this lookup are resolved in-pack during indexing, including forward @@ -70,7 +72,7 @@ impl crate::Bundle { object_hash: gix_hash::Kind, options: Options, ) -> Result<Outcome> { - let _span = gix_features::trace::coarse!("gix_pack::Bundle::write_to_directory()"); + let _span = gix_trace::coarse!("gix_pack::Bundle::write_to_directory()"); let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into()); read_progress.init(None, progress::bytes()); let pack = progress::Read { @@ -183,7 +185,7 @@ impl crate::Bundle { object_hash: gix_hash::Kind, options: Options, ) -> Result<Outcome> { - let _span = gix_features::trace::coarse!("gix_pack::Bundle::write_to_directory_eagerly()"); + let _span = gix_trace::coarse!("gix_pack::Bundle::write_to_directory_eagerly()"); let mut read_progress = progress.add_child_with_id("read pack".into(), ProgressId::ReadPackBytes.into()); /* Bundle Write Read pack Bytes*/ read_progress.init(pack_size.map(|s| s as usize), progress::bytes()); let pack = progress::Read { @@ -242,7 +244,7 @@ impl crate::Bundle { }; let num_objects = pack_entries_iter.size_hint().0; let pack_entries_iter = - gix_features::parallel::EagerIterIf::new(move || num_objects > 25_000, pack_entries_iter, 5_000, 5); + gix_parallel::EagerIterIf::new(move || num_objects > 25_000, pack_entries_iter, 5_000, 5); let WriteOutcome { outcome, @@ -345,7 +347,7 @@ impl crate::Bundle { index_file .persist(&index_path) .inspect_err(|_err| { - gix_features::trace::warn!("pack file at \"{}\" is retained despite failing to move the index file into place. You can use plumbing to make it usable.",data_path.display()); + gix_trace::warn!("pack file at \"{}\" is retained despite failing to move the index file into place. You can use plumbing to make it usable.",data_path.display()); }) .or_raise(|| message("Could not persist pack index"))?; } diff --git a/gix-pack/src/cache/delta/from_offsets.rs b/gix-pack/src/cache/delta/from_offsets.rs index d779fbdfe8b..4a7de5c3c3c 100644 --- a/gix-pack/src/cache/delta/from_offsets.rs +++ b/gix-pack/src/cache/delta/from_offsets.rs @@ -7,7 +7,7 @@ use std::{ }; use gix_error::{ErrorExt, OptionExt, ResultExt, bail, cancelled, message}; -use gix_features::progress::{self, Progress}; +use gix_utils::progress::{self, Progress}; use crate::{cache::delta::Tree, data}; diff --git a/gix-pack/src/cache/delta/traverse/mod.rs b/gix-pack/src/cache/delta/traverse/mod.rs index d071ef2066f..b435c1a7430 100644 --- a/gix-pack/src/cache/delta/traverse/mod.rs +++ b/gix-pack/src/cache/delta/traverse/mod.rs @@ -2,11 +2,9 @@ use gix_error::Result; use std::sync::atomic::{AtomicBool, Ordering}; use gix_error::bail; -use gix_features::{ - progress::{self, DynNestedProgress, Progress}, - threading, - threading::{Mutable, OwnShared}, -}; +use gix_parallel as threading; +use gix_parallel::{Mutable, OwnShared}; +use gix_utils::progress::{self, DynNestedProgress, Progress}; use crate::{ cache::delta::{Tree, traverse::util::ItemSliceSync, tree::Item}, diff --git a/gix-pack/src/cache/delta/traverse/resolve.rs b/gix-pack/src/cache/delta/traverse/resolve.rs index 142de6696f3..b9fc9a1f6a0 100644 --- a/gix-pack/src/cache/delta/traverse/resolve.rs +++ b/gix-pack/src/cache/delta/traverse/resolve.rs @@ -2,10 +2,8 @@ use gix_error::Result; use std::sync::atomic::{AtomicBool, Ordering}; use gix_error::{OptionExt, ResourceExhaustionKind, ResultExt, bail, message}; -use gix_features::{ - progress::Progress, - threading::{self, OwnShared}, -}; +use gix_parallel::{self as threading, OwnShared}; +use gix_utils::progress::Progress; use crate::{ cache::delta::{ @@ -136,8 +134,8 @@ pub(super) unsafe fn all<T, F, MBFN, R>( child_items: &ItemSliceSync<'_, Item<T>>, thread_limit: Option<usize>, num_objects: usize, - objects: gix_features::progress::StepShared, - size: gix_features::progress::StepShared, + objects: gix_utils::progress::StepShared, + size: gix_utils::progress::StepShared, progress: &dyn Progress, resolve: F, resolve_data: &R, @@ -170,7 +168,7 @@ where #[cfg(feature = "parallel")] { resolve_parallel( - gix_features::parallel::num_threads(thread_limit).min(num_objects), + gix_parallel::num_threads(thread_limit).min(num_objects), work, objects, size, @@ -214,8 +212,8 @@ where #[expect(clippy::too_many_arguments)] fn resolve_serial<T, F, MBFN, R>( mut work: Vec<WorkItem<'_, T>>, - objects: gix_features::progress::StepShared, - size: gix_features::progress::StepShared, + objects: gix_utils::progress::StepShared, + size: gix_utils::progress::StepShared, progress: &dyn Progress, resolve: F, resolve_data: &R, @@ -271,8 +269,8 @@ where fn resolve_parallel<T, F, MBFN, R>( num_threads: usize, work: Vec<WorkItem<'_, T>>, - objects: gix_features::progress::StepShared, - size: gix_features::progress::StepShared, + objects: gix_utils::progress::StepShared, + size: gix_utils::progress::StepShared, progress: &dyn Progress, resolve: F, resolve_data: &R, @@ -303,14 +301,14 @@ where let stealers: Vec<_> = workers.iter().map(crossbeam_deque::Worker::stealer).collect(); let abort = AtomicBool::new(false); - gix_features::parallel::threads(|scope| { + gix_parallel::threads(|scope| { let mut handles = Vec::with_capacity(num_threads); for (tid, worker) in workers.into_iter().enumerate() { - let result = gix_features::parallel::build_thread() + let result = gix_parallel::build_thread() .name(format!("gix-pack.traverse_deltas.{tid}")) .spawn_scoped( scope, - gix_features::trace::in_thread({ + gix_trace::in_thread({ let stealers = &stealers; let roots = &roots; let remaining = &remaining; @@ -463,8 +461,8 @@ fn resolve_task<'a, T, F, MBFN, R>( ref_delta_children: Option<&super::SharedRefDeltaChildren>, object_hash: gix_hash::Kind, alloc_limit_bytes: Option<usize>, - objects: &gix_features::progress::StepShared, - size: &gix_features::progress::StepShared, + objects: &gix_utils::progress::StepShared, + size: &gix_utils::progress::StepShared, mut push: impl FnMut(WorkItem<'a, T>), ) -> Result where @@ -578,8 +576,8 @@ fn inspect<T, MBFN>( resolved: &ResolvedBase, progress: &dyn Progress, modify_base: &mut MBFN, - objects: &gix_features::progress::StepShared, - size: &gix_features::progress::StepShared, + objects: &gix_utils::progress::StepShared, + size: &gix_utils::progress::StepShared, ) -> Result where T: Send, @@ -663,7 +661,7 @@ mod tests { use gix_error::Result; - use gix_features::progress; + use gix_utils::progress; use crate::{ cache::delta::{Tree, traverse}, diff --git a/gix-pack/src/cache/delta/tree.rs b/gix-pack/src/cache/delta/tree.rs index 4cb0f9fc791..719df602cac 100644 --- a/gix-pack/src/cache/delta/tree.rs +++ b/gix-pack/src/cache/delta/tree.rs @@ -331,7 +331,7 @@ mod tests { std::iter::once(()), &|_| first_entry_offset, &|_| Ok(None), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, ); @@ -354,7 +354,7 @@ mod tests { [first, second].into_iter(), &|offset| *offset, &|_| Ok(None), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, ) @@ -400,7 +400,7 @@ mod tests { .take() .expect("the failing resolver is called only once")) }, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, ) @@ -444,7 +444,7 @@ mod tests { resolutions.set(resolutions.get() + 1); Ok(None) }, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, ) @@ -484,7 +484,7 @@ mod tests { ); Ok(Some(offsets[0])) }, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, )?; @@ -527,7 +527,7 @@ mod tests { idx.sorted_offsets().into_iter(), &|ofs| *ofs, &|id| Ok(idx.lookup(id).map(|index| idx.pack_offset_at_index(index))), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(false), gix_hash::Kind::Sha1, )?; diff --git a/gix-pack/src/cache/lru.rs b/gix-pack/src/cache/lru.rs index ab223ba7d9b..ee9db1d43a4 100644 --- a/gix-pack/src/cache/lru.rs +++ b/gix-pack/src/cache/lru.rs @@ -28,7 +28,7 @@ mod memory { pub struct MemoryCappedHashmap { inner: clru::CLruCache<Key, Entry, std::collections::hash_map::RandomState, CustomScale>, free_list: Vec<Vec<u8>>, - debug: gix_features::cache::Debug, + debug: gix_utils::cache::Debug, } impl MemoryCappedHashmap { @@ -41,7 +41,7 @@ mod memory { .with_scale(CustomScale), ), free_list: Vec::new(), - debug: gix_features::cache::Debug::new(format!("MemoryCappedHashmap({memory_cap_in_bytes}B)")), + debug: gix_utils::cache::Debug::new(format!("MemoryCappedHashmap({memory_cap_in_bytes}B)")), } } } @@ -103,7 +103,7 @@ mod _static { pub struct StaticLinkedList<const SIZE: usize> { inner: uluru::LRUCache<Entry, SIZE>, last_evicted: Vec<u8>, - debug: gix_features::cache::Debug, + debug: gix_utils::cache::Debug, /// the amount of bytes we are currently holding, taking into account the capacities of all Vecs we keep. mem_used: usize, /// The total amount of memory we should be able to hold with all entries combined. @@ -116,7 +116,7 @@ mod _static { StaticLinkedList { inner: Default::default(), last_evicted: Vec::new(), - debug: gix_features::cache::Debug::new(format!("StaticLinkedList<{SIZE}>")), + debug: gix_utils::cache::Debug::new(format!("StaticLinkedList<{SIZE}>")), mem_used: 0, mem_limit: if mem_limit == 0 { usize::MAX } else { mem_limit }, } diff --git a/gix-pack/src/cache/mod.rs b/gix-pack/src/cache/mod.rs index 0e71613ad7b..fa54e780e59 100644 --- a/gix-pack/src/cache/mod.rs +++ b/gix-pack/src/cache/mod.rs @@ -74,7 +74,7 @@ pub mod object; /// offsets.into_iter(), /// &|offset| *offset, /// &|_id| Ok(None), // self-contained pack: no ref-delta lookups -/// &mut gix_features::progress::Discard, +/// &mut gix_utils::progress::Discard, /// &AtomicBool::default(), /// gix_hash::Kind::default(), /// )?; diff --git a/gix-pack/src/cache/object.rs b/gix-pack/src/cache/object.rs index 05e2230e242..135ae96a02f 100644 --- a/gix-pack/src/cache/object.rs +++ b/gix-pack/src/cache/object.rs @@ -29,7 +29,7 @@ mod memory { pub struct MemoryCappedHashmap { inner: clru::CLruCache<Key, Entry, gix_hashtable::hash::Builder, CustomScale>, free_list: Vec<Vec<u8>>, - debug: gix_features::cache::Debug, + debug: gix_utils::cache::Debug, } impl MemoryCappedHashmap { @@ -47,7 +47,7 @@ mod memory { .with_scale(CustomScale), ), free_list: Vec::new(), - debug: gix_features::cache::Debug::new(format!("MemoryCappedObjectHashmap({memory_cap_in_bytes}B)")), + debug: gix_utils::cache::Debug::new(format!("MemoryCappedObjectHashmap({memory_cap_in_bytes}B)")), } } } diff --git a/gix-pack/src/data/entry/decode.rs b/gix-pack/src/data/entry/decode.rs index adcad6a8809..a562b665c9c 100644 --- a/gix-pack/src/data/entry/decode.rs +++ b/gix-pack/src/data/entry/decode.rs @@ -1,7 +1,7 @@ use gix_error::{OptionExt, Result, ResultExt, bail, corruption}; use std::io; -use gix_features::decode::leb64_from_read; +use gix_utils::decode::leb64_from_read; use super::{BLOB, COMMIT, OFS_DELTA, REF_DELTA, TAG, TREE}; use crate::data; diff --git a/gix-pack/src/data/file/init.rs b/gix-pack/src/data/file/init.rs index f8a3d86a55f..df494fed24e 100644 --- a/gix-pack/src/data/file/init.rs +++ b/gix-pack/src/data/file/init.rs @@ -37,7 +37,7 @@ where pub fn from_data(data: T, path: PathBuf, object_hash: gix_hash::Kind) -> Result<Self> { let hash_len = object_hash.len_in_bytes(); let pack_len = data.len(); - let id = gix_features::hash::crc32(path.as_os_str().to_string_lossy().as_bytes()); + let id = crc32fast::hash(path.as_os_str().to_string_lossy().as_bytes()); if pack_len < data::header::SIZE + hash_len { bail!(gix_error::corruption(format!( "Pack data of size {pack_len} is too small for even an empty pack with shortest hash" diff --git a/gix-pack/src/data/file/verify.rs b/gix-pack/src/data/file/verify.rs index 9785c70058c..2ee59f4c49d 100644 --- a/gix-pack/src/data/file/verify.rs +++ b/gix-pack/src/data/file/verify.rs @@ -1,7 +1,7 @@ use gix_error::Result; use std::sync::atomic::AtomicBool; -use gix_features::progress::Progress; +use gix_utils::progress::Progress; use crate::data::File; @@ -19,7 +19,7 @@ where /// returning the actual checksum equivalent to the return value of [`checksum()`][File::checksum()] if there /// is no mismatch. /// - /// Note that if no `progress` is desired, one can pass [`gix_features::progress::Discard`]. + /// Note that if no `progress` is desired, one can pass [`gix_utils::progress::Discard`]. /// /// Have a look at [`index::File::verify_integrity(…)`][crate::index::File::verify_integrity()] for an /// even more thorough integrity check. diff --git a/gix-pack/src/data/input/bytes_to_entries.rs b/gix-pack/src/data/input/bytes_to_entries.rs index 8f2c3d287f5..45efd067989 100644 --- a/gix-pack/src/data/input/bytes_to_entries.rs +++ b/gix-pack/src/data/input/bytes_to_entries.rs @@ -142,8 +142,10 @@ where .header .write_to(bytes_copied, &mut header_buf.as_mut()) .map_err(io_error)?; - let state = gix_features::hash::crc32_update(0, &header_buf[..header_len]); - Some(gix_features::hash::crc32_update(state, &compressed)) + let mut crc32 = crc32fast::Hasher::new(); + crc32.update(&header_buf[..header_len]); + crc32.update(&compressed); + Some(crc32.finalize()) } else { None }; diff --git a/gix-pack/src/data/input/entries_to_bytes.rs b/gix-pack/src/data/input/entries_to_bytes.rs index 6779df82be0..4502634b738 100644 --- a/gix-pack/src/data/input/entries_to_bytes.rs +++ b/gix-pack/src/data/input/entries_to_bytes.rs @@ -98,7 +98,7 @@ where &mut self.output, num_bytes_written, self.object_hash, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &interrupt_never, )?; self.output diff --git a/gix-pack/src/data/input/entry.rs b/gix-pack/src/data/input/entry.rs index e6fbb35d849..a20aac5e482 100644 --- a/gix-pack/src/data/input/entry.rs +++ b/gix-pack/src/data/input/entry.rs @@ -43,8 +43,10 @@ impl input::Entry { .header .write_to(self.decompressed_size, &mut header_buf.as_mut()) .expect("write to memory will not fail"); - let state = gix_features::hash::crc32_update(0, &header_buf[..header_len]); - gix_features::hash::crc32_update(state, self.compressed.as_ref().expect("we always set it")) + let mut crc32 = crc32fast::Hasher::new(); + crc32.update(&header_buf[..header_len]); + crc32.update(self.compressed.as_ref().expect("we always set it")); + crc32.finalize() } } diff --git a/gix-pack/src/data/mod.rs b/gix-pack/src/data/mod.rs index 9268864da16..41001482d2e 100644 --- a/gix-pack/src/data/mod.rs +++ b/gix-pack/src/data/mod.rs @@ -148,7 +148,7 @@ where /// If `pack_offset` or `size` are pointing to a range outside of the mapped pack data. pub fn entry_crc32(&self, pack_offset: Offset, size: usize) -> u32 { let pack_offset: usize = pack_offset.try_into().expect("pack_size fits into usize"); - gix_features::hash::crc32(&self.data[pack_offset..pack_offset + size]) + crc32fast::hash(&self.data[pack_offset..pack_offset + size]) } } diff --git a/gix-pack/src/data/output/bytes.rs b/gix-pack/src/data/output/bytes.rs index 972ea7b1db5..918ca4d4d5a 100644 --- a/gix-pack/src/data/output/bytes.rs +++ b/gix-pack/src/data/output/bytes.rs @@ -38,7 +38,7 @@ where /// `output` writer, resembling a pack of `version` with exactly `num_entries` amount of objects contained in it. /// `object_hash` is the kind of hash to use for the pack checksum and maybe other places, depending on the version. /// - /// The input chunks are expected to be sorted already. You can use the [`InOrderIter`][gix_features::parallel::InOrderIter] to assure + /// The input chunks are expected to be sorted already. You can use the [`InOrderIter`][gix_parallel::InOrderIter] to assure /// this happens on the fly holding entire chunks in memory as long as needed for them to be dispensed in order. pub fn new( input: I, diff --git a/gix-pack/src/data/output/count/objects/mod.rs b/gix-pack/src/data/output/count/objects/mod.rs index daaf0ec201f..0b644a1bb46 100644 --- a/gix-pack/src/data/output/count/objects/mod.rs +++ b/gix-pack/src/data/output/count/objects/mod.rs @@ -1,8 +1,8 @@ use gix_error::Result; use std::{cell::RefCell, sync::atomic::AtomicBool}; -use gix_features::parallel; use gix_hash::ObjectId; +use gix_parallel as parallel; use crate::data::output; @@ -33,7 +33,7 @@ mod tree; pub fn objects<Find>( db: Find, objects_ids: Box<dyn Iterator<Item = Result<ObjectId>> + Send>, - objects: &dyn gix_features::progress::Count, + objects: &dyn gix_utils::progress::Count, should_interrupt: &AtomicBool, Options { thread_limit, @@ -51,7 +51,7 @@ where thread_limit, None, ); - let chunks = gix_features::iter::Chunks { + let chunks = gix_utils::iter::Chunks { inner: objects_ids, size: chunk_size, }; @@ -94,7 +94,7 @@ where pub fn objects_unthreaded( db: &dyn crate::Find, object_ids: &mut dyn Iterator<Item = Result<ObjectId>>, - objects: &dyn gix_features::progress::Count, + objects: &dyn gix_utils::progress::Count, should_interrupt: &AtomicBool, input_object_expansion: ObjectExpansion, ) -> Result<(Vec<output::Count>, Outcome)> { @@ -143,7 +143,7 @@ mod expand { oids: &mut dyn Iterator<Item = Result<ObjectId>>, buf1: &mut Vec<u8>, buf2: &mut Vec<u8>, - objects: &gix_features::progress::AtomicStep, + objects: &gix_utils::progress::AtomicStep, should_interrupt: &AtomicBool, allow_pack_lookups: bool, ) -> Result<(Vec<output::Count>, Outcome)> { @@ -332,7 +332,7 @@ mod expand { all_seen: &impl util::InsertImmutable, id: &oid, location: Option<crate::data::entry::Location>, - objects: &gix_features::progress::AtomicStep, + objects: &gix_utils::progress::AtomicStep, statistics: &mut Outcome, count_expanded: bool, ) { @@ -352,7 +352,7 @@ mod expand { db: &dyn crate::Find, buf: &mut Vec<u8>, id: &oid, - objects: &gix_features::progress::AtomicStep, + objects: &gix_utils::progress::AtomicStep, statistics: &mut Outcome, allow_pack_lookups: bool, ) -> Result<output::Count> { @@ -398,7 +398,7 @@ mod expand { decoded_objects: std::cell::RefCell<usize>, expanded_objects: std::cell::RefCell<usize>, out: std::cell::RefCell<Vec<output::Count>>, - objects_count: &'a gix_features::progress::AtomicStep, + objects_count: &'a gix_utils::progress::AtomicStep, objects: &'a dyn crate::Find, } @@ -406,7 +406,7 @@ mod expand { fn new( objects: &'a dyn crate::Find, out: Vec<output::Count>, - objects_count: &'a gix_features::progress::AtomicStep, + objects_count: &'a gix_utils::progress::AtomicStep, ) -> Self { Self { decoded_objects: Default::default(), diff --git a/gix-pack/src/data/output/count/objects/reduce.rs b/gix-pack/src/data/output/count/objects/reduce.rs index 03144b60fd5..732db8ee59d 100644 --- a/gix-pack/src/data/output/count/objects/reduce.rs +++ b/gix-pack/src/data/output/count/objects/reduce.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use gix_features::parallel; +use gix_parallel as parallel; use super::Outcome; use crate::data::output; diff --git a/gix-pack/src/data/output/entry/iter_from_counts.rs b/gix-pack/src/data/output/entry/iter_from_counts.rs index 43ce14e7898..176756d60ef 100644 --- a/gix-pack/src/data/output/entry/iter_from_counts.rs +++ b/gix-pack/src/data/output/entry/iter_from_counts.rs @@ -3,13 +3,11 @@ pub(crate) mod function { use std::{cmp::Ordering, sync::Arc}; use gix_error::{OptionExt, ResultExt, message, not_found}; - use gix_features::{ - parallel, - parallel::SequenceId, - progress::{ - Progress, - prodash::{Count, DynNestedProgress}, - }, + use gix_parallel as parallel; + use gix_parallel::SequenceId; + use gix_utils::progress::{ + Progress, + prodash::{Count, DynNestedProgress}, }; use super::{Mode, Options, Outcome, ProgressId, reduce, util}; @@ -58,7 +56,7 @@ pub(crate) mod function { }: Options, ) -> Result< impl Iterator<Item = Result<(SequenceId, Vec<output::Entry>)>> - + parallel::reduce::Finalize<Reduce = reduce::Statistics<gix_error::Error>>, + + parallel::Finalize<Reduce = reduce::Statistics<gix_error::Error>>, > where Find: crate::Find + Send + Clone + 'static, @@ -69,7 +67,7 @@ pub(crate) mod function { let progress = Arc::new(parking_lot::Mutex::new( progress.add_child_with_id("resolving".into(), ProgressId::ResolveCounts.into()), )); - progress.lock().init(None, gix_features::progress::count("counts")); + progress.lock().init(None, gix_utils::progress::count("counts")); let enough_counts_present = counts.len() > 4_000; let start = std::time::Instant::now(); parallel::in_parallel_if( @@ -95,14 +93,14 @@ pub(crate) mod function { Ok::<_, gix_error::Error>(()) } }, - parallel::reduce::IdentityWithResult::<(), gix_error::Error>::default(), + parallel::IdentityWithResult::<(), gix_error::Error>::default(), )?; progress.lock().show_throughput(start); } let counts_range_by_pack_id = match mode { Mode::PackCopyAndBaseObjects => { let mut progress = progress.add_child_with_id("sorting".into(), ProgressId::SortEntries.into()); - progress.init(Some(counts.len()), gix_features::progress::count("counts")); + progress.init(Some(counts.len()), gix_utils::progress::count("counts")); let start = std::time::Instant::now(); use crate::data::output::count::PackLocation::*; @@ -141,7 +139,7 @@ pub(crate) mod function { let progress = Arc::new(parking_lot::Mutex::new(progress)); let chunks = util::ChunkRanges::new(chunk_size, counts.len()); - Ok(parallel::reduce::Stepwise::new( + Ok(parallel::Stepwise::new( chunks.enumerate(), thread_limit, { @@ -151,7 +149,7 @@ pub(crate) mod function { Vec::new(), // object data buffer progress .lock() - .add_child_with_id(format!("thread {n}"), gix_features::progress::UNKNOWN), + .add_child_with_id(format!("thread {n}"), gix_utils::progress::UNKNOWN), ) } }, @@ -162,7 +160,7 @@ pub(crate) mod function { let chunk = &counts[chunk_range]; let mut stats = Outcome::default(); let mut pack_offsets_to_id = None; - progress.init(Some(chunk.len()), gix_features::progress::count("objects")); + progress.init(Some(chunk.len()), gix_utils::progress::count("objects")); for count in chunk.iter() { out.push(match count @@ -293,7 +291,8 @@ mod util { mod reduce { use std::marker::PhantomData; - use gix_features::{parallel, parallel::SequenceId}; + use gix_parallel as parallel; + use gix_parallel::SequenceId; use super::Outcome; use crate::data::output; @@ -428,7 +427,7 @@ mod types { SortEntries, } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::ResolveCounts => *b"ECRC", diff --git a/gix-pack/src/index/encode.rs b/gix-pack/src/index/encode.rs index 89be4f9b1a1..2735d240be3 100644 --- a/gix-pack/src/index/encode.rs +++ b/gix-pack/src/index/encode.rs @@ -38,7 +38,7 @@ mod function { use gix_error::Result; - use gix_features::progress::{self, DynNestedProgress}; + use gix_utils::progress::{self, DynNestedProgress}; use super::{HIGH_BIT, LARGE_OFFSET_THRESHOLD, fanout}; use crate::index::V2_SIGNATURE; @@ -95,7 +95,7 @@ mod function { progress.init(Some(4), progress::steps()); let start = std::time::Instant::now(); - let _info = progress.add_child_with_id("writing fan-out table".into(), gix_features::progress::UNKNOWN); + let _info = progress.add_child_with_id("writing fan-out table".into(), gix_utils::progress::UNKNOWN); let fan_out = fanout(&mut entries_sorted_by_oid.iter().map(|e| e.data.id.first_byte())); for value in fan_out.iter() { @@ -103,21 +103,21 @@ mod function { } progress.inc(); - let _info = progress.add_child_with_id("writing ids".into(), gix_features::progress::UNKNOWN); + let _info = progress.add_child_with_id("writing ids".into(), gix_utils::progress::UNKNOWN); for entry in &entries_sorted_by_oid { out.write_all(entry.data.id.as_slice()) .map_err(gix_hash::io::from_std_io)?; } progress.inc(); - let _info = progress.add_child_with_id("writing crc32".into(), gix_features::progress::UNKNOWN); + let _info = progress.add_child_with_id("writing crc32".into(), gix_utils::progress::UNKNOWN); for entry in &entries_sorted_by_oid { out.write_all(&entry.data.crc32.to_be_bytes()) .map_err(gix_hash::io::from_std_io)?; } progress.inc(); - let _info = progress.add_child_with_id("writing offsets".into(), gix_features::progress::UNKNOWN); + let _info = progress.add_child_with_id("writing offsets".into(), gix_utils::progress::UNKNOWN); { let mut offsets64 = Vec::<u64>::new(); for entry in &entries_sorted_by_oid { diff --git a/gix-pack/src/index/traverse/mod.rs b/gix-pack/src/index/traverse/mod.rs index fbdaec31c0c..80bd7c755e5 100644 --- a/gix-pack/src/index/traverse/mod.rs +++ b/gix-pack/src/index/traverse/mod.rs @@ -2,7 +2,8 @@ use gix_error::{Result, corruption}; use std::sync::atomic::AtomicBool; use gix_error::{ErrorExt, ResultExt, bail, message}; -use gix_features::{parallel, progress::Progress}; +use gix_parallel as parallel; +use gix_utils::progress::Progress; use crate::index; @@ -13,7 +14,7 @@ pub mod with_index; pub mod with_lookup; use reduce::Reducer; -use gix_features::progress::DynNestedProgress; +use gix_utils::progress::DynNestedProgress; mod types; pub use types::{Algorithm, ProgressId, SafetyCheck, Statistics}; @@ -63,7 +64,7 @@ where /// Iterate through all _decoded objects_ in the given `pack` and handle them with a `Processor`. /// The return value is (pack-checksum, [`Outcome`], `progress`), thus the pack traversal will always verify /// the whole packs checksum to assure it was correct. In case of bit-rod, the operation will abort early without - /// verifying all objects using the [interrupt mechanism][gix_features::interrupt] mechanism. + /// verifying all objects using the [interrupt mechanism][gix_utils::interrupt] mechanism. /// /// # Algorithms /// diff --git a/gix-pack/src/index/traverse/reduce.rs b/gix-pack/src/index/traverse/reduce.rs index fed6269455e..64f961ee730 100644 --- a/gix-pack/src/index/traverse/reduce.rs +++ b/gix-pack/src/index/traverse/reduce.rs @@ -4,11 +4,9 @@ use std::{ }; use gix_error::{Result, bail, cancelled}; -use gix_features::{ - parallel, - progress::Progress, - threading::{Mutable, OwnShared, lock}, -}; +use gix_parallel as parallel; +use gix_parallel::{Mutable, OwnShared, lock}; +use gix_utils::progress::Progress; use crate::{data, index::traverse}; @@ -111,7 +109,7 @@ where self.entries_seen, elapsed_s, objects_per_second, - gix_features::progress::bytesize::ByteSize(self.stats.average.object_size * u64::from(objects_per_second)) + gix_utils::progress::bytesize::ByteSize(self.stats.average.object_size * u64::from(objects_per_second)) )); Ok(self.stats) } diff --git a/gix-pack/src/index/traverse/with_index.rs b/gix-pack/src/index/traverse/with_index.rs index 6112f44e1f6..6fe6aa6557e 100644 --- a/gix-pack/src/index/traverse/with_index.rs +++ b/gix-pack/src/index/traverse/with_index.rs @@ -1,7 +1,8 @@ use gix_error::Result; use std::sync::atomic::{AtomicBool, Ordering}; -use gix_features::{parallel, progress::DynNestedProgress}; +use gix_parallel as parallel; +use gix_utils::progress::DynNestedProgress; use crate::{ cache::delta::traverse, @@ -40,7 +41,7 @@ pub enum ProgressId { DecodedBytes, } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::HashPackDataBytes => *b"PTHP", @@ -75,9 +76,8 @@ where }: Options, ) -> Result<Outcome> where - Processor: FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn gix_features::progress::Progress) -> Result - + Send - + Clone, + Processor: + FnMut(gix_object::Kind, &[u8], &index::Entry, &dyn gix_utils::progress::Progress) -> Result + Send + Clone, D: crate::FileData + Send + Sync, { let (verify_result, traversal_result) = parallel::join( @@ -143,7 +143,7 @@ where // TODO: Fix this - we overwrite the header of 'data' which also changes the computed entry size, // causing index and pack to seemingly mismatch. This is surprising, and should be done differently. // debug_assert_eq!(&data.index_entry.pack_offset, &pack_entry.pack_offset()); - gix_features::hash::crc32( + crc32fast::hash( pack.entry_slice(data.index_entry.pack_offset..entry_end) .expect("slice pointing into the pack (by now data is verified)"), ) diff --git a/gix-pack/src/index/traverse/with_lookup.rs b/gix-pack/src/index/traverse/with_lookup.rs index 7d1cc66bc7f..13765d8258b 100644 --- a/gix-pack/src/index/traverse/with_lookup.rs +++ b/gix-pack/src/index/traverse/with_lookup.rs @@ -1,11 +1,9 @@ use gix_error::Result; use std::sync::atomic::{AtomicBool, Ordering}; -use gix_features::{ - parallel::{self, in_parallel_if}, - progress::{self, Count, DynNestedProgress, Progress}, - threading::{Mutable, OwnShared, lock}, -}; +use gix_parallel::{self as parallel, in_parallel_if}; +use gix_parallel::{Mutable, OwnShared, lock}; +use gix_utils::progress::{self, Count, DynNestedProgress, Progress}; use super::Reducer; use crate::{ @@ -49,7 +47,7 @@ pub enum ProgressId { DecodedObjects, } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::HashPackDataBytes => *b"PTHP", @@ -132,7 +130,7 @@ where Vec::with_capacity(2048), // decode buffer gix_zlib::Inflate::default(), lock(&reduce_progress) - .add_child_with_id(format!("thread {index}"), gix_features::progress::UNKNOWN), // per thread progress + .add_child_with_id(format!("thread {index}"), gix_utils::progress::UNKNOWN), // per thread progress ) } }; @@ -147,7 +145,7 @@ where -> Result<Vec<data::decode::entry::Outcome>> { progress.init( Some(entries.len()), - gix_features::progress::count_with_decimals("objects", 2), + gix_utils::progress::count_with_decimals("objects", 2), ); let mut stats = exact_vec(entries.len()); progress.set(0); diff --git a/gix-pack/src/index/util.rs b/gix-pack/src/index/util.rs index 42eb3474c85..2e78b239eae 100644 --- a/gix-pack/src/index/util.rs +++ b/gix-pack/src/index/util.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use gix_features::progress::{self, Progress}; +use gix_utils::progress::{self, Progress}; use crate::exact_vec; diff --git a/gix-pack/src/index/verify.rs b/gix-pack/src/index/verify.rs index 4de2abe1a53..74d0d229ba6 100644 --- a/gix-pack/src/index/verify.rs +++ b/gix-pack/src/index/verify.rs @@ -2,9 +2,9 @@ use gix_error::{Result, corruption, message}; use std::sync::atomic::AtomicBool; use gix_error::{ResultExt, bail}; -use gix_features::progress::{DynNestedProgress, Progress}; use gix_object::WriteTo; use gix_object::bstr::ByteSlice; +use gix_utils::progress::{DynNestedProgress, Progress}; use crate::index; @@ -55,11 +55,11 @@ pub mod integrity { Traverse(PhantomData<crate::index::verify::index::traverse::ProgressId>), } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::ChecksumBytes => *b"PTHI", - ProgressId::Traverse(_) => gix_features::progress::UNKNOWN, + ProgressId::Traverse(_) => gix_utils::progress::UNKNOWN, } } } @@ -211,7 +211,7 @@ where object_kind: gix_object::Kind, buf: &[u8], index_entry: &index::Entry, - _progress: &dyn gix_features::progress::Progress, + _progress: &dyn gix_utils::progress::Progress, ) -> Result { if let Mode::HashCrc32Decode | Mode::HashCrc32DecodeEncode = verify_mode { use gix_object::Kind::*; diff --git a/gix-pack/src/index/write/mod.rs b/gix-pack/src/index/write/mod.rs index 3d51fc91b48..8c65146fd76 100644 --- a/gix-pack/src/index/write/mod.rs +++ b/gix-pack/src/index/write/mod.rs @@ -41,7 +41,7 @@ pub enum ProgressId { IndexBytesWritten, } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::IndexObjects => *b"IWIO", @@ -58,7 +58,7 @@ pub(super) mod function { use std::{io, sync::atomic::AtomicBool}; use gix_error::{OptionExt, ResultExt, bail}; - use gix_features::progress::{self, Count, Progress, prodash::DynNestedProgress}; + use gix_utils::progress::{self, Count, Progress, prodash::DynNestedProgress}; use crate::cache::delta::{Tree, traverse}; @@ -237,8 +237,7 @@ pub(super) mod function { let mut items = roots; items.extend(children); { - let _progress = - root_progress.add_child_with_id("sorting by id".into(), gix_features::progress::UNKNOWN); + let _progress = root_progress.add_child_with_id("sorting by id".into(), gix_utils::progress::UNKNOWN); items.sort_by_key(|e| e.data.id); } diff --git a/gix-pack/src/multi_index/verify.rs b/gix-pack/src/multi_index/verify.rs index 7e6685aba39..5e7f8e36cfe 100644 --- a/gix-pack/src/multi_index/verify.rs +++ b/gix-pack/src/multi_index/verify.rs @@ -2,7 +2,7 @@ use gix_error::{Result, corruption, message}; use std::{cmp::Ordering, sync::atomic::AtomicBool, time::Instant}; use gix_error::{OptionExt, bail, cancelled}; -use gix_features::progress::{Count, DynNestedProgress, Progress}; +use gix_utils::progress::{Count, DynNestedProgress, Progress}; use crate::{exact_vec, index, multi_index::File}; @@ -28,7 +28,7 @@ pub mod integrity { ObjectOffsets, } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::ChecksumBytes => *b"MVCK", @@ -135,11 +135,8 @@ where let mut pack_ids_and_offsets = exact_vec(self.num_objects as usize); { let order_start = Instant::now(); - let mut progress = progress.add_child_with_id("checking oid order".into(), gix_features::progress::UNKNOWN); - progress.init( - Some(self.num_objects as usize), - gix_features::progress::count("objects"), - ); + let mut progress = progress.add_child_with_id("checking oid order".into(), gix_utils::progress::UNKNOWN); + progress.init(Some(self.num_objects as usize), gix_utils::progress::count("objects")); for entry_index in 0..(self.num_objects - 1) { let lhs = self.oid_at_index(entry_index); @@ -164,10 +161,7 @@ where progress.show_throughput(order_start); }; - progress.init( - Some(self.num_indices as usize), - gix_features::progress::count("indices"), - ); + progress.init(Some(self.num_indices as usize), gix_utils::progress::count("indices")); let mut pack_ids_slice = pack_ids_and_offsets.as_slice(); @@ -196,10 +190,7 @@ where "verify object offsets".into(), integrity::ProgressId::ObjectOffsets.into(), ); - offsets_progress.init( - Some(pack_ids_and_offsets.len()), - gix_features::progress::count("objects"), - ); + offsets_progress.init(Some(pack_ids_and_offsets.len()), gix_utils::progress::count("objects")); pack_ids_slice = &pack_ids_slice[slice_end..]; for entry_id in multi_index_entries_to_check.iter().map(|e| e.1) { diff --git a/gix-pack/src/multi_index/write.rs b/gix-pack/src/multi_index/write.rs index af3508aebac..268efd328ef 100644 --- a/gix-pack/src/multi_index/write.rs +++ b/gix-pack/src/multi_index/write.rs @@ -34,7 +34,7 @@ pub enum ProgressId { BytesWritten, } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::FromPathsCollectingEntries => *b"MPCE", @@ -62,7 +62,7 @@ pub(super) mod function { }; use gix_error::{ResourceExhaustionKind, ResultExt, cancelled, ensure}; - use gix_features::progress::{Count, DynNestedProgress, Progress}; + use gix_utils::progress::{Count, DynNestedProgress, Progress}; use crate::{MMap, multi_index}; @@ -95,7 +95,7 @@ pub(super) mod function { "Collecting entries".into(), ProgressId::FromPathsCollectingEntries.into(), ); - progress.init(Some(index_paths_sorted.len()), gix_features::progress::count("indices")); + progress.init(Some(index_paths_sorted.len()), gix_utils::progress::count("indices")); // This could be parallelized… but it's probably not worth it unless you have 500mio objects. for (index_id, index) in index_paths_sorted.iter().enumerate() { @@ -124,7 +124,7 @@ pub(super) mod function { let start = Instant::now(); progress.set_name("Deduplicate".into()); - progress.init(Some(entries.len()), gix_features::progress::count("entries")); + progress.init(Some(entries.len()), gix_utils::progress::count("entries")); entries.sort_by(|l, r| { l.id.cmp(&r.id) .then_with(|| l.index_mtime.cmp(&r.index_mtime).reverse()) @@ -165,9 +165,9 @@ pub(super) mod function { let write_start = Instant::now(); write_progress.init( Some(cf.planned_storage_size() as usize + multi_index::File::<MMap>::HEADER_LEN), - gix_features::progress::bytes(), + gix_utils::progress::bytes(), ); - let mut out = gix_features::progress::Write { + let mut out = gix_utils::progress::Write { inner: out, progress: write_progress, }; @@ -182,7 +182,7 @@ pub(super) mod function { { progress.set_name("Writing chunks".into()); - progress.init(Some(cf.num_chunks()), gix_features::progress::count("chunks")); + progress.init(Some(cf.num_chunks()), gix_utils::progress::count("chunks")); let mut chunk_write = cf .into_write(&mut out, bytes_written) diff --git a/gix-pack/src/verify.rs b/gix-pack/src/verify.rs index 0d51d1d2cd5..f20167d16e8 100644 --- a/gix-pack/src/verify.rs +++ b/gix-pack/src/verify.rs @@ -2,7 +2,7 @@ use gix_error::Result; use std::{path::Path, sync::atomic::AtomicBool}; use gix_error::{Class, ClassificationMarker, ResultExt, bail}; -use gix_features::progress::Progress; +use gix_utils::progress::Progress; /// Returns the `index` at which the following `index + 1` value is not an increment over the value at `index`. pub fn fan(data: &[u32]) -> Option<usize> { diff --git a/gix-pack/tests/pack/bundle.rs b/gix-pack/tests/pack/bundle.rs index c01628db903..714fb588245 100644 --- a/gix-pack/tests/pack/bundle.rs +++ b/gix-pack/tests/pack/bundle.rs @@ -95,9 +95,9 @@ mod write_to_directory { sync::atomic::AtomicBool, }; - use gix_features::progress; use gix_odb::pack; use gix_testtools::tempfile::TempDir; + use gix_utils::progress; use crate::{SMALL_PACK, SMALL_PACK_INDEX, fixture_path}; diff --git a/gix-pack/tests/pack/data/file.rs b/gix-pack/tests/pack/data/file.rs index f8e7c24365c..3ad3412633b 100644 --- a/gix-pack/tests/pack/data/file.rs +++ b/gix-pack/tests/pack/data/file.rs @@ -35,7 +35,7 @@ fn unresolved_delta_base_is_not_found() { mod method { use std::sync::atomic::AtomicBool; - use gix_features::progress; + use gix_utils::progress; use crate::{SMALL_PACK, data::file::pack_at, pack_from_memory_at}; diff --git a/gix-pack/tests/pack/data/output/count_and_entries.rs b/gix-pack/tests/pack/data/output/count_and_entries.rs index d6c4034e483..5244e1a6eef 100644 --- a/gix-pack/tests/pack/data/output/count_and_entries.rs +++ b/gix-pack/tests/pack/data/output/count_and_entries.rs @@ -1,12 +1,10 @@ use crate::Result; use std::sync::atomic::AtomicBool; -use gix_features::{ - parallel::{InOrderIter, reduce::Finalize}, - progress, -}; use gix_odb::{pack, pack::FindExt}; use gix_pack::data::{output, output::count}; +use gix_parallel::{Finalize, InOrderIter}; +use gix_utils::progress; use crate::{ data::output::{DbKind, db}, diff --git a/gix-pack/tests/pack/index.rs b/gix-pack/tests/pack/index.rs index 69d15526e04..87d59f6f95b 100644 --- a/gix-pack/tests/pack/index.rs +++ b/gix-pack/tests/pack/index.rs @@ -134,9 +134,9 @@ mod version { use crate::Result; use std::{fs, io, sync::atomic::AtomicBool}; - use gix_features::progress; use gix_odb::pack; use gix_pack::{data::input, index}; + use gix_utils::progress; use crate::{INDEX_V2, SMALL_PACK, V2_PACKS_AND_INDICES, fixture_path}; @@ -342,7 +342,7 @@ fn traverse_with_index_respects_alloc_limit_bytes() -> gix_testtools::TestResult #[test] fn from_memory_backing_supports_verification_and_traversal() { - use gix_features::progress; + use gix_utils::progress; let index = memory_backed_index(SMALL_PACK_INDEX); let data = pack_from_memory_at(SMALL_PACK); @@ -374,8 +374,8 @@ fn from_memory_backing_supports_verification_and_traversal() { assert_eq!(count.load(Ordering::SeqCst), index.num_objects() as usize); } -use gix_features::progress; use gix_pack::{cache, data::decode::entry::Outcome, index}; +use gix_utils::progress; use maplit::btreemap; use crate::{INDEX_V2, PACK_FOR_INDEX_V2}; diff --git a/gix-pack/tests/pack/multi_index/fuzzed.rs b/gix-pack/tests/pack/multi_index/fuzzed.rs index f13261164cb..edb803b7303 100644 --- a/gix-pack/tests/pack/multi_index/fuzzed.rs +++ b/gix-pack/tests/pack/multi_index/fuzzed.rs @@ -199,7 +199,7 @@ fn integrity_verification_rejects_corrupt_offset_references() -> gix_testtools:: let index = gix_pack::multi_index::File::from_data(data, "invalid-reference.midx".into(), None)?; let err = index .verify_integrity_fast( - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::new(false), ) .expect_err("verification must inspect references even though loading does not"); diff --git a/gix-pack/tests/pack/multi_index/verify.rs b/gix-pack/tests/pack/multi_index/verify.rs index 62567188ff4..5ba1e0a0485 100644 --- a/gix-pack/tests/pack/multi_index/verify.rs +++ b/gix-pack/tests/pack/multi_index/verify.rs @@ -1,6 +1,6 @@ use std::sync::atomic::AtomicBool; -use gix_features::progress; +use gix_utils::progress; use maplit::btreemap; use crate::{multi_index::multi_index, object_hash}; diff --git a/gix-pack/tests/pack/multi_index/write.rs b/gix-pack/tests/pack/multi_index/write.rs index 71b7d56b257..966b88ed399 100644 --- a/gix-pack/tests/pack/multi_index/write.rs +++ b/gix-pack/tests/pack/multi_index/write.rs @@ -4,8 +4,8 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; -use gix_features::progress; use gix_testtools::fixture_path; +use gix_utils::progress; /// Writes a multi-index from the static SHA-1 pack indices, with pinned SHA-1 expectations. /// The SHA-256 counterpart lives in [`from_a_hash_parameterized_pack`] below. diff --git a/gix-features/CHANGELOG.md b/gix-parallel/CHANGELOG.md similarity index 100% rename from gix-features/CHANGELOG.md rename to gix-parallel/CHANGELOG.md diff --git a/gix-parallel/Cargo.toml b/gix-parallel/Cargo.toml new file mode 100644 index 00000000000..5c7b6d5b673 --- /dev/null +++ b/gix-parallel/Cargo.toml @@ -0,0 +1,56 @@ +lints.workspace = true + +[package] +name = "gix-parallel" +description = "Serial and parallel computation helpers and shared ownership primitives for gitoxide" +repository = "https://github.com/GitoxideLabs/gitoxide" +version = "0.50.0" +authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] +license = "MIT OR Apache-2.0" +edition = "2024" +rust-version = "1.88" +include = ["/src/**/*", "/LICENSE-*"] + +[lib] +doctest = false +test = false + +[features] +default = [] +## Use scoped threads and channels to parallelize computations, and thread-safe primitives for shared ownership and mutation. +## Without this feature, computation helpers run serially and shared ownership uses `Rc` and `RefCell`. +parallel = ["dep:crossbeam-channel", "dep:parking_lot"] +## Provide `OnceCell` in synchronized or unsynchronized form, selected by `parallel`. +## The external crate is needed for `get_or_try_init()` at the minimum supported Rust version. +once_cell = ["dep:once_cell"] + +[[test]] +name = "parallel" +path = "tests/parallel_threaded.rs" +required-features = ["parallel"] + +[[test]] +name = "multi-threaded" +path = "tests/parallel_shared_threaded.rs" +required-features = ["parallel"] + +[[test]] +name = "single-threaded" +path = "tests/parallel_shared.rs" + +[dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +crossbeam-channel = { version = "0.5.15", optional = true } +parking_lot = { version = "0.12.4", default-features = false, optional = true } +once_cell = { version = "1.21.3", optional = true } +document-features = { version = "0.2.0", optional = true } + +[package.metadata.docs.rs] +all-features = true +features = ["document-features"] + +[dev-dependencies] +gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } +gix-trace = { path = "../gix-trace", features = ["tracing"] } +insta = "1.46.3" +tracing-subscriber = { version = "0.3.22", default-features = false, features = ["registry"] } diff --git a/gix-features/LICENSE-APACHE b/gix-parallel/LICENSE-APACHE similarity index 100% rename from gix-features/LICENSE-APACHE rename to gix-parallel/LICENSE-APACHE diff --git a/gix-features/LICENSE-MIT b/gix-parallel/LICENSE-MIT similarity index 100% rename from gix-features/LICENSE-MIT rename to gix-parallel/LICENSE-MIT diff --git a/gix-features/src/parallel/eager_iter.rs b/gix-parallel/src/eager_iter.rs similarity index 98% rename from gix-features/src/parallel/eager_iter.rs rename to gix-parallel/src/eager_iter.rs index 6aadbf459a6..434e837e48b 100644 --- a/gix-features/src/parallel/eager_iter.rs +++ b/gix-parallel/src/eager_iter.rs @@ -27,7 +27,7 @@ where let size_hint = iter.size_hint(); assert!(chunk_size > 0, "non-zero chunk size is needed"); - std::thread::spawn(crate::trace::in_thread(move || { + std::thread::spawn(gix_trace::in_thread(move || { let mut out = Vec::with_capacity(chunk_size); for item in iter { out.push(item); diff --git a/gix-features/src/parallel/in_order.rs b/gix-parallel/src/in_order.rs similarity index 100% rename from gix-features/src/parallel/in_order.rs rename to gix-parallel/src/in_order.rs diff --git a/gix-features/src/parallel/in_parallel.rs b/gix-parallel/src/in_parallel.rs similarity index 97% rename from gix-features/src/parallel/in_parallel.rs rename to gix-parallel/src/in_parallel.rs index 2e6809ccf8b..e4968af8e29 100644 --- a/gix-features/src/parallel/in_parallel.rs +++ b/gix-parallel/src/in_parallel.rs @@ -1,6 +1,6 @@ use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering}; -use crate::parallel::{Reduce, num_threads}; +use crate::{Reduce, num_threads}; /// A scope to start threads within. pub type Scope<'scope, 'env> = std::thread::Scope<'scope, 'env>; @@ -10,11 +10,11 @@ pub fn join<O1: Send, O2: Send>(left: impl FnOnce() -> O1 + Send, right: impl Fn std::thread::scope(|s| { let left = std::thread::Builder::new() .name("gitoxide.join.left".into()) - .spawn_scoped(s, crate::trace::in_thread(left)) + .spawn_scoped(s, gix_trace::in_thread(left)) .expect("valid name"); let right = std::thread::Builder::new() .name("gitoxide.join.right".into()) - .spawn_scoped(s, crate::trace::in_thread(right)) + .spawn_scoped(s, gix_trace::in_thread(right)) .expect("valid name"); (left.join().unwrap(), right.join().unwrap()) }) @@ -71,7 +71,7 @@ where let receive_input = receive_input.clone(); let new_thread_state = new_thread_state.clone(); let mut consume = consume.clone(); - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { let mut state = new_thread_state(thread_id); for item in receive_input { if send_result.send(consume(item, &mut state)).is_err() { @@ -86,7 +86,7 @@ where .name("gitoxide.in_parallel.feed".into()) .spawn_scoped( s, - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { for item in input { if send_input.send(item).is_err() { break; @@ -143,7 +143,7 @@ where let new_thread_state = new_thread_state.clone(); let mut consume = consume.clone(); let finalize = finalize.clone(); - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { let mut state = new_thread_state(thread_id); let mut can_send = true; for item in receive_input { @@ -163,7 +163,7 @@ where .name("gitoxide.in_parallel.feed".into()) .spawn_scoped( s, - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { for item in input { if send_input.send(item).is_err() { break; @@ -224,7 +224,7 @@ where let watcher = std::thread::Builder::new() .name("gitoxide.in_parallel_with_slice.watch-interrupts".into()) .spawn_scoped(s, { - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { loop { if stop_everything.load(Ordering::Relaxed) { break; @@ -283,7 +283,7 @@ where let state_to_rval = state_to_rval.clone(); let mut consume = consume.clone(); let input = Input(input.as_mut_ptr()); - crate::trace::in_thread(move || { + gix_trace::in_thread(move || { let _ = &input; threads_left.fetch_sub(1, Ordering::SeqCst); let mut state = new_thread_state(thread_id); diff --git a/gix-features/src/parallel/mod.rs b/gix-parallel/src/lib.rs similarity index 88% rename from gix-features/src/parallel/mod.rs rename to gix-parallel/src/lib.rs index a0cab7d427a..f86cdce341f 100644 --- a/gix-features/src/parallel/mod.rs +++ b/gix-parallel/src/lib.rs @@ -1,8 +1,12 @@ -//! Run computations in parallel, or not based the `parallel` feature toggle. +//! Computation helpers and shared ownership primitives for serial and threaded execution. +//! +//! Enable `parallel` to use threaded computation helpers and synchronized shared ownership. +//! Without it, these helpers run serially and shared ownership uses `Rc` and `RefCell`. +//! Applications using `gix` should select its `parallel` feature to enable all parallel code paths. //! //! Worker threads started by the functions and iterators here inherit the current tracing //! span and subscriber. When spawning threads directly with [`threads()`] or [`build_thread()`], -//! wrap their closures with [`crate::trace::in_thread()`] to propagate that context. +//! wrap their closures with [`gix_trace::in_thread()`] to propagate that context. //! //! ### `in_parallel`(…) //! @@ -19,13 +23,13 @@ //! operation which runs as fast as possible, which is cancellable only by merit of stopping the input or stopping the output //! aggregation. //! -//! ### `reduce::Stepwise` +//! ### `Stepwise` //! -//! The [`Stepwise`][reduce::Stepwise] iterator works exactly as [`in_parallel()`] except that the processing of the output produced by +//! The [`Stepwise`] iterator works exactly as [`in_parallel()`] except that the processing of the output produced by //! `consume(I, &mut State) -> O` is made accessible by the `Iterator` trait's `next()` method. As produced work is not //! buffered, the owner of the iterator controls the progress made. //! -//! Getting the final output of the [`Reduce`] is achieved through the consuming [`Stepwise::finalize()`][reduce::Stepwise::finalize()] method, which +//! Getting the final output of the [`Reduce`] is achieved through the consuming [`Stepwise::finalize()`] method, which //! is functionally equivalent to calling [`in_parallel()`]. //! //! In an `async` context this means that progress is only made each time `next()` is called on the iterator, while merely dropping @@ -36,6 +40,14 @@ //! In order to assure that threads don't outlive the data they borrow because their handles are leaked, we enforce //! the `'static` lifetime for its inputs, making it less intuitive to use. It is, however, possible to produce //! suitable input iterators as long as they can hold something on the heap. +//! ## Feature Flags +#![cfg_attr(all(doc, feature = "document-features"), doc = ::document_features::document_features!())] +#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] +#![deny(missing_docs)] + +mod threading; +pub use threading::*; + #[cfg(feature = "parallel")] mod in_parallel; #[cfg(feature = "parallel")] @@ -178,5 +190,5 @@ where } /// -pub mod reduce; -pub use reduce::Reduce; +mod reduce; +pub use reduce::{Finalize, IdentityWithResult, Reduce, Stepwise}; diff --git a/gix-features/src/parallel/reduce.rs b/gix-parallel/src/reduce.rs similarity index 93% rename from gix-features/src/parallel/reduce.rs rename to gix-parallel/src/reduce.rs index fca8d732e03..c8cc9d68430 100644 --- a/gix-features/src/parallel/reduce.rs +++ b/gix-parallel/src/reduce.rs @@ -1,8 +1,8 @@ #[cfg(feature = "parallel")] mod stepped { - use crate::parallel::num_threads; + use crate::num_threads; - /// An iterator adaptor to allow running computations using [`in_parallel()`][crate::parallel::in_parallel()] in a step-wise manner, see the [module docs][crate::parallel] + /// An iterator adaptor to allow running computations using [`in_parallel()`][crate::in_parallel()] in a step-wise manner, see the [module docs][crate] /// for details. pub struct Stepwise<Reduce: super::Reduce> { /// This field is first to assure it's dropped first and cause threads that are dropped next to stop their loops @@ -34,7 +34,7 @@ mod stepped { impl<Reduce: super::Reduce> Stepwise<Reduce> { /// Instantiate a new iterator and start working in threads. - /// For a description of parameters, see [`in_parallel()`][crate::parallel::in_parallel()]. + /// For a description of parameters, see [`in_parallel()`][crate::in_parallel()]. pub fn new<InputIter, ThreadStateFn, ConsumeFn, I, O, S>( input: InputIter, thread_limit: Option<usize>, @@ -56,7 +56,7 @@ mod stepped { let (send_input, receive_input) = crossbeam_channel::bounded::<I>(num_threads); let (send_result, receive_result) = std::sync::mpsc::sync_channel::<O>(num_threads); for thread_id in 0..num_threads { - let handle = std::thread::spawn(crate::trace::in_thread({ + let handle = std::thread::spawn(gix_trace::in_thread({ let send_result = send_result.clone(); let receive_input = receive_input.clone(); let new_thread_state = new_thread_state.clone(); @@ -72,7 +72,7 @@ mod stepped { })); threads.push(handle); } - threads.push(std::thread::spawn(crate::trace::in_thread(move || { + threads.push(std::thread::spawn(gix_trace::in_thread(move || { for item in input { if send_input.send(item).is_err() { break; @@ -88,7 +88,7 @@ mod stepped { } } - /// Consume the iterator by finishing its iteration and calling [`Reduce::finalize()`][crate::parallel::Reduce::finalize()]. + /// Consume the iterator by finishing its iteration and calling [`Reduce::finalize()`][crate::Reduce::finalize()]. pub fn finalize(mut self) -> Result<Reduce::Output, Reduce::Error> { for value in self.by_ref() { drop(value?); @@ -127,7 +127,7 @@ mod stepped { #[cfg(not(feature = "parallel"))] mod stepped { - /// An iterator adaptor to allow running computations using [`in_parallel()`][crate::parallel::in_parallel()] in a step-wise manner, see the [module docs][crate::parallel] + /// An iterator adaptor to allow running computations using [`in_parallel()`][crate::in_parallel()] in a step-wise manner, see the [module docs][crate] /// for details. pub struct Stepwise<InputIter, ConsumeFn, ThreadState, Reduce> { input: InputIter, @@ -143,7 +143,7 @@ mod stepped { Reduce: super::Reduce<Input = O>, { /// Instantiate a new iterator. - /// For a description of parameters, see [`in_parallel()`][crate::parallel::in_parallel()]. + /// For a description of parameters, see [`in_parallel()`][crate::in_parallel()]. pub fn new<ThreadStateFn>( input: InputIter, _thread_limit: Option<usize>, @@ -162,7 +162,7 @@ mod stepped { } } - /// Consume the iterator by finishing its iteration and calling [`Reduce::finalize()`][crate::parallel::Reduce::finalize()]. + /// Consume the iterator by finishing its iteration and calling [`Reduce::finalize()`][crate::Reduce::finalize()]. pub fn finalize(mut self) -> Result<Reduce::Output, Reduce::Error> { for value in self.by_ref() { drop(value?); @@ -236,7 +236,7 @@ pub trait Reduce { fn finalize(self) -> Result<Self::Output, Self::Error>; } -/// An identity reducer for those who want to use [`Stepwise`] or [`in_parallel()`][crate::parallel::in_parallel()] +/// An identity reducer for those who want to use [`Stepwise`] or [`in_parallel()`][crate::in_parallel()] /// without the use of non-threaded reduction of products created in threads. pub struct IdentityWithResult<Input, Error> { _input: PhantomData<Input>, diff --git a/gix-features/src/parallel/serial.rs b/gix-parallel/src/serial.rs similarity index 99% rename from gix-features/src/parallel/serial.rs rename to gix-parallel/src/serial.rs index fdff786cf34..ec53a2bdfd3 100644 --- a/gix-features/src/parallel/serial.rs +++ b/gix-parallel/src/serial.rs @@ -1,4 +1,4 @@ -use crate::parallel::Reduce; +use crate::Reduce; #[cfg(not(feature = "parallel"))] mod not_parallel { diff --git a/gix-features/src/threading.rs b/gix-parallel/src/threading.rs similarity index 95% rename from gix-features/src/threading.rs rename to gix-parallel/src/threading.rs index e5d47ca3623..ed10fd813c9 100644 --- a/gix-features/src/threading.rs +++ b/gix-parallel/src/threading.rs @@ -1,6 +1,6 @@ -//! Type definitions for putting shared ownership and synchronized mutation behind the `threading` feature toggle. +//! Type definitions for putting shared ownership and synchronized mutation behind the `parallel` feature toggle. //! -//! That way, single-threaded applications will not have to use thread-safe primitives, and simply do not specify the 'threading' feature. +//! That way, single-threaded applications will not have to use thread-safe primitives, and simply do not specify the `parallel` feature. #[cfg(feature = "parallel")] mod _impl { @@ -101,7 +101,7 @@ mod _impl { v.borrow() } - /// Downgrade a handle previously obtained with [`upgrade_ref_to_mut()`] to drop mutation support. + /// Downgrade a handle previously obtained with [`get_mut()`] to drop mutation support. pub fn downgrade_mut_to_ref<'a, T>(v: RefMut<'a, T>, orig: &'a RefCell<T>) -> RefGuard<'a, T> { drop(v); orig.borrow() diff --git a/gix-features/tests/parallel/in_order_iter.rs b/gix-parallel/tests/parallel/in_order_iter.rs similarity index 97% rename from gix-features/tests/parallel/in_order_iter.rs rename to gix-parallel/tests/parallel/in_order_iter.rs index 6378d59bc30..ff2066896b0 100644 --- a/gix-features/tests/parallel/in_order_iter.rs +++ b/gix-parallel/tests/parallel/in_order_iter.rs @@ -1,6 +1,6 @@ use std::convert::Infallible; -use gix_features::parallel::InOrderIter; +use gix_parallel::InOrderIter; #[test] fn in_order_stays_in_order() -> gix_testtools::TestResult { diff --git a/gix-features/tests/parallel/in_parallel_with_slice.rs b/gix-parallel/tests/parallel/in_parallel_with_slice.rs similarity index 99% rename from gix-features/tests/parallel/in_parallel_with_slice.rs rename to gix-parallel/tests/parallel/in_parallel_with_slice.rs index f930655373c..bb0f1406fdf 100644 --- a/gix-features/tests/parallel/in_parallel_with_slice.rs +++ b/gix-parallel/tests/parallel/in_parallel_with_slice.rs @@ -1,4 +1,4 @@ -use gix_features::parallel; +use gix_parallel as parallel; #[test] fn in_parallel_with_mut_slice_in_chunks() { diff --git a/gix-features/tests/parallel/mod.rs b/gix-parallel/tests/parallel/mod.rs similarity index 92% rename from gix-features/tests/parallel/mod.rs rename to gix-parallel/tests/parallel/mod.rs index 009bf31708f..4b82364c884 100644 --- a/gix-features/tests/parallel/mod.rs +++ b/gix-parallel/tests/parallel/mod.rs @@ -1,5 +1,5 @@ //! Tests that are working similarly in parallel and serial mode -use gix_features::parallel; +use gix_parallel as parallel; mod in_order_iter; mod in_parallel_with_slice; @@ -40,7 +40,7 @@ fn in_parallel() { #[test] fn stepped_reduce_next() { - let mut iter = parallel::reduce::Stepwise::new( + let mut iter = parallel::Stepwise::new( std::iter::from_fn(|| Some(1)).take(100), None, |_n| (), @@ -69,7 +69,7 @@ fn stepped_reduce_ref_input_and_consume() { } } - let mut iter = parallel::reduce::Stepwise::new( + let mut iter = parallel::Stepwise::new( ArcIter(seq.clone(), 0).enumerate(), None, { @@ -92,7 +92,7 @@ fn stepped_reduce_ref_input_and_consume() { #[test] fn stepped_reduce_finalize() { - let iter = parallel::reduce::Stepwise::new( + let iter = parallel::Stepwise::new( std::iter::from_fn(|| Some(1)).take(100), None, |_n| (), diff --git a/gix-features/tests/parallel_shared.rs b/gix-parallel/tests/parallel_shared.rs similarity index 100% rename from gix-features/tests/parallel_shared.rs rename to gix-parallel/tests/parallel_shared.rs diff --git a/gix-features/tests/parallel_shared_threaded.rs b/gix-parallel/tests/parallel_shared_threaded.rs similarity index 100% rename from gix-features/tests/parallel_shared_threaded.rs rename to gix-parallel/tests/parallel_shared_threaded.rs diff --git a/gix-features/tests/parallel_threaded.rs b/gix-parallel/tests/parallel_threaded.rs similarity index 94% rename from gix-features/tests/parallel_threaded.rs rename to gix-parallel/tests/parallel_threaded.rs index 05f49d19550..75f4657ff60 100644 --- a/gix-features/tests/parallel_threaded.rs +++ b/gix-parallel/tests/parallel_threaded.rs @@ -1,5 +1,5 @@ mod optimize_chunk_size_and_thread_limit { - use gix_features::parallel::optimize_chunk_size_and_thread_limit; + use gix_parallel::optimize_chunk_size_and_thread_limit; #[test] fn not_enough_chunks_for_threads() { @@ -54,7 +54,7 @@ mod optimize_chunk_size_and_thread_limit { } mod unknown_chunk_count { - use gix_features::parallel::optimize_chunk_size_and_thread_limit; + use gix_parallel::optimize_chunk_size_and_thread_limit; #[test] fn medium_chunk_size_many_threads() { @@ -94,7 +94,7 @@ mod optimize_chunk_size_and_thread_limit { } mod real_values { - use gix_features::parallel::optimize_chunk_size_and_thread_limit; + use gix_parallel::optimize_chunk_size_and_thread_limit; #[test] fn linux_kernel_pack_my_machine_lookup() { diff --git a/gix-features/tests/trace/parallel.rs b/gix-parallel/tests/tracing.rs similarity index 89% rename from gix-features/tests/trace/parallel.rs rename to gix-parallel/tests/tracing.rs index d90ca12dc02..dff214bced0 100644 --- a/gix-features/tests/trace/parallel.rs +++ b/gix-parallel/tests/tracing.rs @@ -1,4 +1,5 @@ -use gix_features::{parallel, trace}; +use gix_parallel as parallel; +use gix_trace as trace; fn assert_parent() { assert_eq!( @@ -23,7 +24,7 @@ fn workers_inherit_the_current_span_and_subscriber() { assert_parent(); Ok(item) }, - parallel::reduce::IdentityWithResult::<_, ()>::default(), + parallel::IdentityWithResult::<_, ()>::default(), ) .expect("workers complete successfully"); parallel::in_parallel_with_finalize( @@ -38,7 +39,7 @@ fn workers_inherit_the_current_span_and_subscriber() { assert_parent(); Ok(0) }, - parallel::reduce::IdentityWithResult::<_, ()>::default(), + parallel::IdentityWithResult::<_, ()>::default(), ) .expect("workers complete successfully"); let periodic_ran = std::sync::atomic::AtomicBool::new(false); @@ -67,7 +68,7 @@ fn workers_inherit_the_current_span_and_subscriber() { 2, "the eager worker visits every input" ); - parallel::reduce::Stepwise::new( + parallel::Stepwise::new( (0..2).inspect(|_| assert_parent()), Some(2), |_| assert_parent(), @@ -75,7 +76,7 @@ fn workers_inherit_the_current_span_and_subscriber() { assert_parent(); Ok(item) }, - parallel::reduce::IdentityWithResult::<_, ()>::default(), + parallel::IdentityWithResult::<_, ()>::default(), ) .finalize() .expect("workers complete successfully"); diff --git a/gix-protocol/Cargo.toml b/gix-protocol/Cargo.toml index cde15571c37..4dcc9cb3478 100644 --- a/gix-protocol/Cargo.toml +++ b/gix-protocol/Cargo.toml @@ -73,14 +73,11 @@ required-features = ["async-client"] [dependencies] gix-error = { version = "^0.4.0", path = "../gix-error" } -gix-features = { version = "^0.50.0", path = "../gix-features", features = [ - "progress", -] } gix-transport = { version = "^0.60.0", path = "../gix-transport" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-shallow = { version = "^0.14.0", path = "../gix-shallow" } gix-date = { version = "^0.17.0", path = "../gix-date" } -gix-utils = { version = "^0.4.0", path = "../gix-utils" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["progress"] } gix-ref = { version = "^0.68.0", path = "../gix-ref" } gix-macros = { version = "^0.2.0", path = "../gix-macros" } diff --git a/gix-protocol/src/fetch/function.rs b/gix-protocol/src/fetch/function.rs index f18ba95d5ad..806a95ff871 100644 --- a/gix-protocol/src/fetch/function.rs +++ b/gix-protocol/src/fetch/function.rs @@ -4,7 +4,7 @@ use std::{ }; use gix_error::{Result, ResultExt, bail, message, validation}; -use gix_features::progress::DynNestedProgress; +use gix_utils::progress::DynNestedProgress; use crate::fetch::{ Arguments, Context, Negotiate, NegotiateOutcome, Options, Outcome, ProgressId, Shallow, Tags, negotiate, @@ -56,7 +56,7 @@ pub async fn fetch<P, T>( }: Options<'_>, ) -> Result<Option<Outcome>> where - P: gix_features::progress::NestedProgress, + P: gix_utils::progress::NestedProgress, P::SubProgress: 'static, T: Transport, { @@ -279,7 +279,7 @@ fn add_shallow_args( } fn setup_remote_progress<'a>( - progress: &mut dyn gix_features::progress::DynNestedProgress, + progress: &mut dyn gix_utils::progress::DynNestedProgress, reader: &mut Box<dyn ExtendedBufRead<'a> + Unpin + 'a>, should_interrupt: &'a AtomicBool, ) { diff --git a/gix-protocol/src/fetch/types.rs b/gix-protocol/src/fetch/types.rs index 7d9c768405b..671ce22b77a 100644 --- a/gix-protocol/src/fetch/types.rs +++ b/gix-protocol/src/fetch/types.rs @@ -267,7 +267,7 @@ pub enum ProgressId { RemoteProgress, } -impl From<ProgressId> for gix_features::progress::Id { +impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::RemoteProgress => *b"FERP", diff --git a/gix-protocol/src/handshake/function.rs b/gix-protocol/src/handshake/function.rs index 9342a4983c3..9c93094241f 100644 --- a/gix-protocol/src/handshake/function.rs +++ b/gix-protocol/src/handshake/function.rs @@ -1,8 +1,9 @@ use crate::bisync::bisync; use gix_error::Result; use gix_error::{ErrorExt, OptionExt, ResultExt, bail, message}; -use gix_features::{progress, progress::Progress}; use gix_transport::{Service, client}; +use gix_utils::progress; +use gix_utils::progress::Progress; use crate::Handshake; #[crate::bisync::only_async] @@ -39,7 +40,8 @@ where AuthFn: FnMut(credentials::helper::Action) -> Result<Option<credentials::protocol::Outcome>>, T: Transport, { - let _span = gix_features::trace::detail!("gix_protocol::handshake()", service = ?service, extra_parameters = ?extra_parameters); + let _span = + gix_trace::detail!("gix_protocol::handshake()", service = ?service, extra_parameters = ?extra_parameters); let (server_protocol_version, refs, capabilities) = { progress.init(None, progress::steps()); progress.set_name("handshake".into()); diff --git a/gix-protocol/src/handshake/mod.rs b/gix-protocol/src/handshake/mod.rs index f31968988dd..8e767d877a3 100644 --- a/gix-protocol/src/handshake/mod.rs +++ b/gix-protocol/src/handshake/mod.rs @@ -75,7 +75,7 @@ pub(crate) mod hero { use gix_error::Result; use gix_error::{ResultExt, message}; - use gix_features::progress::Progress; + use gix_utils::progress::Progress; /// Intermediate state while potentially fetching a refmap after the handshake. pub enum ObtainRefMap<'a> { diff --git a/gix-protocol/src/ls_refs.rs b/gix-protocol/src/ls_refs.rs index 9cbb8a0a914..b2d8e1d43ea 100644 --- a/gix-protocol/src/ls_refs.rs +++ b/gix-protocol/src/ls_refs.rs @@ -7,8 +7,8 @@ pub(crate) mod function { use bstr::{BString, ByteVec}; use gix_error::{ResultExt, message}; - use gix_features::progress::Progress; use gix_transport::client::Capabilities; + use gix_utils::progress::Progress; #[cfg(feature = "async-client")] use crate::transport::client::async_io::TransportV2Ext as _; @@ -111,7 +111,7 @@ pub(crate) mod function { progress: &mut impl Progress, trace: bool, ) -> gix_error::Result<Vec<Ref>> { - let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode); + let _span = gix_trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode); Command::LsRefs .validate_argument_prefixes( gix_transport::Protocol::V2, @@ -232,7 +232,7 @@ pub(crate) mod function { false, ); let err = super::LsRefsCommand::new(None, &capabilities, ("agent", Some("test".into()))) - .invoke_blocking(transport, &mut gix_features::progress::Discard, false) + .invoke_blocking(transport, &mut gix_utils::progress::Discard, false) .expect_err("the transport write fails"); error_snapshots.push(gix_testtools::redact_debug_snapshot(&(err), &[])); assert_eq!(err.can_retry_lenient(), retryable, "preserve retry policy for {kind:?}"); diff --git a/gix-protocol/src/remote_progress.rs b/gix-protocol/src/remote_progress.rs index 7fd7dbf6063..11e3fed2739 100644 --- a/gix-protocol/src/remote_progress.rs +++ b/gix-protocol/src/remote_progress.rs @@ -26,7 +26,7 @@ impl RemoteProgress<'_> { /// Parse `text`, which is interpreted as error if `is_error` is true, as [`RemoteProgress`] and call the respective /// methods on the given `progress` instance. - pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_features::progress::Progress) { + pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_utils::progress::Progress) { fn progress_name(current: Option<String>, action: &[u8]) -> String { match current { Some(current) => format!( @@ -51,7 +51,7 @@ impl RemoteProgress<'_> { max, }) => { progress.set_name(progress_name(progress.name(), action)); - progress.init(max, gix_features::progress::count("objects")); + progress.init(max, gix_utils::progress::count("objects")); if let Some(step) = step { progress.set(step); } diff --git a/gix-protocol/tests/protocol/fetch/_impl.rs b/gix-protocol/tests/protocol/fetch/_impl.rs index 7710ec7153d..ee9cb020427 100644 --- a/gix-protocol/tests/protocol/fetch/_impl.rs +++ b/gix-protocol/tests/protocol/fetch/_impl.rs @@ -13,7 +13,6 @@ pub enum RefsAction { mod fetch_fn { use crate::bisync::bisync; use gix_error::{ErrorExt, Result, ResultExt, message}; - use gix_features::progress::NestedProgress; use gix_protocol::{ Command, LsRefsCommand, credentials, fetch::{Arguments, Response}, @@ -23,6 +22,7 @@ mod fetch_fn { use gix_transport::client::async_io::{ExtendedBufRead, HandleProgress, Transport}; #[cfg(feature = "blocking-client")] use gix_transport::client::blocking_io::{ExtendedBufRead, HandleProgress, Transport}; + use gix_utils::progress::NestedProgress; use std::ops::ControlFlow; use super::{Action, Delegate, RefsAction}; @@ -398,8 +398,8 @@ mod delegate { ops::DerefMut, }; - use gix_features::progress::NestedProgress; use gix_protocol::{fetch::Response, handshake::Ref}; + use gix_utils::progress::NestedProgress; use super::DelegateBlocking; @@ -459,8 +459,8 @@ mod delegate { use async_trait::async_trait; use futures_io::AsyncBufRead; - use gix_features::progress::NestedProgress; use gix_protocol::{fetch::Response, handshake::Ref}; + use gix_utils::progress::NestedProgress; use super::DelegateBlocking; diff --git a/gix-protocol/tests/protocol/fetch/mod.rs b/gix-protocol/tests/protocol/fetch/mod.rs index 106b796b972..64047e5937e 100644 --- a/gix-protocol/tests/protocol/fetch/mod.rs +++ b/gix-protocol/tests/protocol/fetch/mod.rs @@ -150,8 +150,8 @@ impl DelegateBlocking for LsRemoteDelegate { mod blocking_io { use std::io; - use gix_features::progress::NestedProgress; use gix_protocol::{fetch::Response, handshake, handshake::Ref}; + use gix_utils::progress::NestedProgress; use super::_impl::Delegate; use crate::fetch::{CloneDelegate, CloneRefInWantDelegate, LsRemoteDelegate}; @@ -207,8 +207,8 @@ mod async_io { use async_trait::async_trait; use futures_io::AsyncBufRead; - use gix_features::progress::NestedProgress; use gix_protocol::{fetch::Response, handshake, handshake::Ref}; + use gix_utils::progress::NestedProgress; use super::_impl::Delegate; use crate::fetch::{CloneDelegate, CloneRefInWantDelegate, LsRemoteDelegate}; diff --git a/gix-protocol/tests/protocol/fetch/response.rs b/gix-protocol/tests/protocol/fetch/response.rs index e4f47771434..2460ab06e89 100644 --- a/gix-protocol/tests/protocol/fetch/response.rs +++ b/gix-protocol/tests/protocol/fetch/response.rs @@ -432,7 +432,7 @@ mod v2 { assert!(r.has_pack()); let mut buf = Vec::new(); reader.set_progress_handler(Some(Box::new(|a: bool, b: &[u8]| { - gix_protocol::RemoteProgress::translate_to_progress(a, b, &mut gix_features::progress::Discard); + gix_protocol::RemoteProgress::translate_to_progress(a, b, &mut gix_utils::progress::Discard); std::ops::ControlFlow::Continue(()) }) as HandleProgress)); let bytes_read = reader.read_to_end(&mut buf).await?; diff --git a/gix-protocol/tests/protocol/fetch/v1.rs b/gix-protocol/tests/protocol/fetch/v1.rs index 9709426a2ef..26d82338a77 100644 --- a/gix-protocol/tests/protocol/fetch/v1.rs +++ b/gix-protocol/tests/protocol/fetch/v1.rs @@ -1,8 +1,8 @@ use crate::TestResult; use bstr::ByteSlice; -use gix_features::progress; use gix_protocol::handshake; use gix_transport::Protocol; +use gix_utils::progress; use crate::fetch::{_impl::FetchConnection, CloneDelegate, LsRemoteDelegate, helper_unused, oid, transport}; diff --git a/gix-protocol/tests/protocol/fetch/v2.rs b/gix-protocol/tests/protocol/fetch/v2.rs index d774c8f6e43..71320f1ef63 100644 --- a/gix-protocol/tests/protocol/fetch/v2.rs +++ b/gix-protocol/tests/protocol/fetch/v2.rs @@ -1,8 +1,8 @@ use crate::TestResult; use bstr::ByteSlice; -use gix_features::progress; use gix_protocol::handshake; use gix_transport::Protocol; +use gix_utils::progress; use crate::fetch::{ _impl::FetchConnection, CloneDelegate, CloneRefInWantDelegate, LsRemoteDelegate, helper_unused, oid, transport, diff --git a/gix-ref/Cargo.toml b/gix-ref/Cargo.toml index 4d1b3a2f962..0683061e4f9 100644 --- a/gix-ref/Cargo.toml +++ b/gix-ref/Cargo.toml @@ -23,11 +23,11 @@ sha256 = ["gix-hash/sha256"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "gix-hash/serde", "gix-actor/serde", "gix-object/serde"] ## Enable support for thread-safety. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } -gix-features = { version = "^0.50.0", path = "../gix-features" } gix-fs = { version = "^0.23.0", path = "../gix-fs", features = ["walkdir"] } gix-path = { version = "^0.13.0", path = "../gix-path" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } diff --git a/gix-ref/src/store/file/packed.rs b/gix-ref/src/store/file/packed.rs index c2ea1495122..e57754c64b8 100644 --- a/gix-ref/src/store/file/packed.rs +++ b/gix-ref/src/store/file/packed.rs @@ -63,7 +63,7 @@ pub type SharedBufferSnapshot = gix_fs::SharedFileSnapshot<packed::Buffer>; pub(crate) mod modifiable { use gix_error::{Message, Result, ResultExt}; - use gix_features::threading::OwnShared; + use gix_parallel::OwnShared; use crate::{file, packed}; diff --git a/gix-status/Cargo.toml b/gix-status/Cargo.toml index b244d146844..7b56bbfe5b8 100644 --- a/gix-status/Cargo.toml +++ b/gix-status/Cargo.toml @@ -22,16 +22,18 @@ sha256 = ["gix-hash/sha256"] ## Add support for tracking rewrites along with checking for worktree modifications. worktree-rewrites = ["dep:gix-dir", "dep:gix-diff"] ## Enable parallel algorithms. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["interrupt", "progress"] } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-index = { version = "^0.56.0", path = "../gix-index" } gix-fs = { version = "^0.23.0", path = "../gix-fs" } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-error = { version = "^0.4.0", path = "../gix-error" } gix-object = { version = "^0.65.0", path = "../gix-object" } gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["progress"] } gix-filter = { version = "^0.35.0", path = "../gix-filter" } gix-worktree = { version = "^0.57.0", path = "../gix-worktree", default-features = false, features = ["attributes"] } gix-pathspec = { version = "^0.21.0", path = "../gix-pathspec" } diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs index 3274678ae43..a03a7214aa3 100644 --- a/gix-status/src/index_as_worktree/function.rs +++ b/gix-status/src/index_as_worktree/function.rs @@ -8,9 +8,9 @@ use std::{ use bstr::BStr; use filetime::FileTime; use gix_error::{ErrorExt, ResultExt, message}; -use gix_features::parallel::{Reduce, in_parallel_if}; use gix_filter::pipeline::convert::ToGitOutcome; use gix_object::FindExt; +use gix_parallel::{Reduce, in_parallel_if}; #[cfg(windows)] use crate::fscache::{FsCache, Metadata as FsCacheMetadata}; @@ -60,7 +60,7 @@ pub fn index_as_worktree<'index, T, U, Find>( compare: impl CompareBlobs<Output = T> + Send + Clone, submodule: impl SubmoduleStatus<Output = U> + Send + Clone, objects: Find, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, Context { pathspec, stack, @@ -78,7 +78,7 @@ where // (modified at or after the last index update) during the index update we then set those // entries size to 0 (see below) to ensure they keep showing up as racy and reset the timestamp. let timestamp = index.timestamp(); - let (chunk_size, thread_limit, _) = gix_features::parallel::optimize_chunk_size_and_thread_limit( + let (chunk_size, thread_limit, _) = gix_parallel::optimize_chunk_size_and_thread_limit( 500, // just like git index.entries().len().into(), options.thread_limit, @@ -94,7 +94,7 @@ where let entry_index_offset = range.start; let entries = &entries[range]; - let _span = gix_features::trace::detail!("gix_status::index_as_worktree", + let _span = gix_trace::detail!("gix_status::index_as_worktree", num_entries = entries.len(), chunk_size = chunk_size, thread_limit = ?thread_limit); @@ -104,7 +104,7 @@ where let (worktree_bytes, worktree_reads, odb_bytes, odb_reads, racy_clean) = Default::default(); num_entries = entries.len(); - progress.init(entries.len().into(), gix_features::progress::count("files")); + progress.init(entries.len().into(), gix_utils::progress::count("files")); let count = progress.counter(); let new_state = { @@ -146,7 +146,7 @@ where }; in_parallel_if( || true, // TODO: heuristic: when is parallelization not worth it? Git says 500 items per thread, but to 20 threads, we can be more fine-grained though. - gix_features::interrupt::Iter::new( + gix_utils::interrupt::Iter::new( OffsetIter { inner: entries.chunks(chunk_size), offset: entry_index_offset, diff --git a/gix-status/src/index_as_worktree/traits.rs b/gix-status/src/index_as_worktree/traits.rs index 6d055d9b264..06b80e09877 100644 --- a/gix-status/src/index_as_worktree/traits.rs +++ b/gix-status/src/index_as_worktree/traits.rs @@ -153,7 +153,7 @@ impl CompareBlobs for HashEq { gix_object::Kind::Blob, &mut stream, len, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::default(), )?, }; diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs index 299e63ee507..fe2823b7a27 100644 --- a/gix-status/src/index_as_worktree_with_renames/mod.rs +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -53,7 +53,7 @@ pub(super) mod function { compare: impl CompareBlobs<Output = T> + Send + Clone, submodule: impl SubmoduleStatus<Output = U> + Send + Clone, objects: Find, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, mut ctx: Context<'_>, options: Options<'_>, ) -> Result<Outcome> @@ -64,16 +64,16 @@ pub(super) mod function { { let mut tracked_file_modifications = options.tracked_file_modifications; tracked_file_modifications.fscache = options.fscache; - gix_features::parallel::threads(|scope| -> Result<Outcome> { + gix_parallel::threads(|scope| -> Result<Outcome> { let (tx, rx) = std::sync::mpsc::channel(); let walk_outcome = options .dirwalk .map(|options| { - gix_features::parallel::build_thread() + gix_parallel::build_thread() .name("gix_status::dirwalk".into()) .spawn_scoped( scope, - gix_features::trace::in_thread({ + gix_trace::in_thread({ let tx = tx.clone(); let mut collect = dirwalk::Delegate { tx, @@ -136,11 +136,11 @@ pub(super) mod function { ctx.resource_cache.attr_stack.clone(), ) }); - let tracked_modifications_outcome = gix_features::parallel::build_thread() + let tracked_modifications_outcome = gix_parallel::build_thread() .name("gix_status::index_as_worktree".into()) .spawn_scoped( scope, - gix_features::trace::in_thread({ + gix_trace::in_thread({ let mut collect = tracked_modifications::Delegate { tx }; let objects = objects.clone(); let stack = ctx.resource_cache.attr_stack.clone(); @@ -289,7 +289,7 @@ pub(super) mod function { // NOTE: to make this work, we'd want to wait the index modification check to complete. // Then it's possible to efficiently emit the tracked files along with what we already sent, // i.e. untracked and ignored files. - gix_features::trace::debug!("full-tree copy tracking isn't currently supported"); + gix_trace::debug!("full-tree copy tracking isn't currently supported"); Ok::<_, std::io::Error>(()) }, ) @@ -546,7 +546,7 @@ pub(super) mod function { ErrorKind::NotFound | ErrorKind::PermissionDenied | ErrorKind::Interrupted ) => { - gix_features::trace::debug!( + gix_trace::debug!( ?file_path, ?err, "ignoring worktree file as it can't be read for hashing" @@ -581,7 +581,7 @@ pub(super) mod function { message!("Could not read metadata for worktree file \"{}\"", file_path.display()) })? .len(), - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, should_interrupt, ) .or_raise(|| message!("Could not hash worktree file \"{}\"", file_path.display()))?, diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index 1ae26dd15c2..ea65ac155e5 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -205,7 +205,7 @@ fn fixture_filtered_detailed( FastEq, SubmoduleStatusMock { dirty: submodule_dirty }, odb, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, ctx, options, ) @@ -218,7 +218,7 @@ fn fixture_filtered_detailed( FastEq, SubmoduleStatusMock { dirty: submodule_dirty }, &odb, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, ctx, options, ) @@ -299,7 +299,7 @@ fn hash_errors_preserve_io_kinds() { gix_object::Kind::Blob, &mut &b"x"[..], stream_len, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, &AtomicBool::new(interrupted), ) .expect_err("a short stream or requested interruption prevents hashing"); @@ -1279,7 +1279,7 @@ fn racy_git() -> gix_testtools::TestResult { counter.clone(), SubmoduleStatusMock { dirty: false }, gix_object::find::Never, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, ctx.clone(), Options { fs, @@ -1315,7 +1315,7 @@ fn racy_git() -> gix_testtools::TestResult { counter, SubmoduleStatusMock { dirty: false }, gix_object::find::Never, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, ctx, Options { fs, diff --git a/gix-status/tests/status/index_as_worktree_with_renames.rs b/gix-status/tests/status/index_as_worktree_with_renames.rs index 7cb6d8b7e5f..019c67f9296 100644 --- a/gix-status/tests/status/index_as_worktree_with_renames.rs +++ b/gix-status/tests/status/index_as_worktree_with_renames.rs @@ -362,7 +362,7 @@ fn fixture_filtered_detailed( FastEq, crate::index_as_worktree::SubmoduleStatusMock { dirty: false }, objects, - &mut gix_features::progress::Discard, + &mut gix_utils::progress::Discard, context, options, ) diff --git a/gix-tix/src/command.rs b/gix-tix/src/command.rs index c8c81013f08..80675513e40 100644 --- a/gix-tix/src/command.rs +++ b/gix-tix/src/command.rs @@ -577,9 +577,7 @@ fn write_history( hide: &[OsString], mut out: impl Write, ) -> Result<()> { - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( - crate::history::Authors::default(), - )); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(crate::history::Authors::default())); let refs = crate::history::snapshot(repository, revisions, hide, false)?; let mut app = crate::app::App::new(usize::MAX); app.id_mode = crate::app::IdMode::Commit; diff --git a/gix-tix/src/command/rebase.rs b/gix-tix/src/command/rebase.rs index 2dc7a8a67b8..edbc9e0e644 100644 --- a/gix-tix/src/command/rebase.rs +++ b/gix-tix/src/command/rebase.rs @@ -155,7 +155,7 @@ fn prepare(repo: &gix::Repository, args: &Todo) -> Result<todo::Prepared> { } let refs = history::snapshot(repo, &args.tips, &hide, false)?; - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut app = App::new(usize::MAX); let mut decorations = Decorations::default(); let mut graph = None; diff --git a/gix-tix/src/edit/mod.rs b/gix-tix/src/edit/mod.rs index f5847de3040..f7084449fd2 100644 --- a/gix-tix/src/edit/mod.rs +++ b/gix-tix/src/edit/mod.rs @@ -63,9 +63,7 @@ fn load_graph( ) -> Result<crate::history::HistoryGraph> { use std::sync::atomic::AtomicBool; - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( - crate::history::Authors::default(), - )); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(crate::history::Authors::default())); let mut graph = None; crate::history::load( repo, diff --git a/gix-tix/src/edit/time_travel.rs b/gix-tix/src/edit/time_travel.rs index c6866977d2e..b0e9d977899 100644 --- a/gix-tix/src/edit/time_travel.rs +++ b/gix-tix/src/edit/time_travel.rs @@ -1630,9 +1630,7 @@ mod tests { } fn loaded_graph(repository: &gix::Repository, revisions: &[OsString]) -> TestResult<history::HistoryGraph> { - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( - history::Authors::default(), - )); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(history::Authors::default())); let mut graph = None; history::load( repository, diff --git a/gix-tix/src/history.rs b/gix-tix/src/history.rs index 7fc0afc1f64..9b14e761f31 100644 --- a/gix-tix/src/history.rs +++ b/gix-tix/src/history.rs @@ -14,7 +14,7 @@ use gix::{ use crate::app::{Attribution, AttributionKind, Author, Commit, LoadedCommits, Metadata, SignatureState}; -pub(crate) type SharedAuthors = gix::features::threading::OwnShared<gix::features::threading::Mutable<Authors>>; +pub(crate) type SharedAuthors = gix::parallel::OwnShared<gix::parallel::Mutable<Authors>>; static EMPTY_AUTHOR: std::sync::LazyLock<Author> = std::sync::LazyLock::new(|| Author { name: BStr::new(b""), email: BStr::new(b""), @@ -910,7 +910,7 @@ impl HistoryGraph { None } else { let object = repo.find_commit(id)?; - let mut authors = gix::features::threading::lock(authors); + let mut authors = gix::parallel::lock(authors); Some(decode_metadata(object.iter(), &mut authors, &mut attributions)?) }; let metadata_loaded = metadata.is_some(); @@ -1245,7 +1245,7 @@ pub(crate) fn load( if tips.is_empty() { let mut rows = Vec::with_capacity(hidden_tips.len()); let mut attributions = Vec::new(); - let mut authors = gix::features::threading::lock(authors); + let mut authors = gix::parallel::lock(authors); let mut buf = Vec::new(); for &id in &hidden_tips { if cancelled.load(Ordering::Relaxed) { @@ -1365,7 +1365,7 @@ pub(crate) fn load( None } else { let object = repo.find_commit(id)?; - let mut authors = gix::features::threading::lock(authors); + let mut authors = gix::parallel::lock(authors); Some(decode_metadata(object.iter(), &mut authors, &mut attributions)?) }; if should_emit { @@ -1466,7 +1466,7 @@ pub(crate) fn load( connected.retain(|id| graph.index(*id).is_none_or(|index| !states[index.as_usize()].emitted)); let mut rows = Vec::with_capacity(connected.len()); let mut attributions = Vec::new(); - let mut authors = gix::features::threading::lock(authors); + let mut authors = gix::parallel::lock(authors); for id in connected { if cancelled.load(Ordering::Relaxed) { emit(Event::Cancelled); @@ -1917,7 +1917,7 @@ pub(crate) fn load_metadata( ) -> Result<(Metadata<BString>, Vec<Attribution>)> { let object = repo.find_commit(id)?; let mut attributions = Vec::new(); - let mut authors = gix::features::threading::lock(authors); + let mut authors = gix::parallel::lock(authors); let metadata = decode_metadata(object.iter(), &mut authors, &mut attributions)?; Ok((metadata, attributions)) } @@ -2448,8 +2448,7 @@ mod tests { fn loaded(path: &std::path::Path, revisions: &[&str], hidden_revisions: &[&str]) -> TestResult<Vec<Event>> { let mut events = Vec::new(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let repo = crate::test_repository::open(path)?; load( &repo, @@ -3067,8 +3066,7 @@ mod tests { .expect("older graph commits defer metadata"); let repo = crate::test_repository::open(fixture_path)?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let (metadata, _) = load_metadata(&repo, deferred.id, &authors)?; assert!( !metadata.title.is_empty(), @@ -3237,8 +3235,7 @@ mod tests { "hidden-only descendants are excluded" ); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); graph.refresh( &repo, &[OsString::from("main")], @@ -3322,8 +3319,7 @@ mod tests { }) .expect("history loading returns the persistent graph"); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); graph.refresh(&repo, &["topic".into()], &[], false, &HashSet::new(), &authors)?; assert_eq!( @@ -3369,8 +3365,7 @@ mod tests { } let repo = crate::test_repository::open(path)?; let new_tip = repo.rev_parse_single("main")?.detach(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let refresh = graph.refresh(&repo, &[], &["main".into()], false, &HashSet::new(), &authors)?; assert_eq!( @@ -3407,8 +3402,7 @@ mod tests { "graph-only traversal does not claim rows were sent to the UI" ); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let refresh = graph.refresh(&repo, &revisions, &[], false, &HashSet::new(), &authors)?; assert!( refresh.commits.rows.iter().any(|row| row.id == tip), @@ -3445,8 +3439,7 @@ mod tests { std::fs::write(&broken_tag, format!("{}\n", "f".repeat(40)))?; let repo = crate::test_repository::open(fixture.path())?; let new_tip = repo.rev_parse_single("main")?.detach(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); graph .refresh(&repo, &["main".into()], &[], false, &HashSet::new(), &authors) @@ -3503,8 +3496,7 @@ mod tests { assert!(status.success(), "git prepares one new commit"); } let repo = crate::test_repository::open(fixture.path())?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let first = graph.refresh(&repo, &["main".into()], &[], false, &HashSet::new(), &authors)?; assert_eq!(first.commits.rows.len(), 1, "only the new descendant is loaded"); let second = graph.refresh(&repo, &["main".into()], &[], false, &HashSet::new(), &authors)?; @@ -3597,8 +3589,7 @@ mod tests { let repo = crate::test_repository::open(fixture.path())?; let replacement = repo.rev_parse_single("main")?.detach(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); graph.refresh(&repo, &["main".into()], &[], false, &HashSet::new(), &authors)?; let descendants = graph .descendants_in_parent_order(parent) @@ -3647,8 +3638,7 @@ mod tests { assert!(cached.state & NODE_COMPLETE != 0 && cached.state & NODE_STORED == 0); cached.parents = start..end; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let refresh = graph.refresh(&repo, &["topic".into()], &[], false, &HashSet::new(), &authors)?; assert!( refresh.commits.rows.is_empty(), @@ -3705,8 +3695,7 @@ mod tests { main, "test symbolic ref-tree pin", )?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let refresh = graph.refresh(&repo, &["topic".into()], &[], false, &HashSet::new(), &authors)?; let refreshed: HashSet<_> = refresh.commits.rows.iter().map(|row| row.id).collect(); assert!( @@ -3807,8 +3796,7 @@ mod tests { ); let repo = crate::test_repository::open(path)?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let refresh = graph.refresh(&repo, &["local".into()], &[], false, &boundary, &authors)?; visible.extend(refresh.commits.rows.into_iter().map(|row| row.id)); let expected: HashSet<_> = repo @@ -3891,8 +3879,7 @@ mod tests { ); let mut cancelled = Vec::new(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let repo = crate::test_repository::open(&fixture)?; load(&repo, &[], &[], false, &authors, &AtomicBool::new(true), |event| { cancelled.push(event); diff --git a/gix-tix/src/lib.rs b/gix-tix/src/lib.rs index e410fca4a26..5f867d8ea35 100644 --- a/gix-tix/src/lib.rs +++ b/gix-tix/src/lib.rs @@ -1426,7 +1426,7 @@ fn event_loop( } else { drop(view_repository); } - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut watcher_retry_deadline = None; let mut ref_watcher = if preview_mode { None @@ -1448,7 +1448,7 @@ fn event_loop( &revisions, if show_hidden { &[] } else { &hide }, false, - gix::features::threading::OwnShared::clone(&authors), + gix::parallel::OwnShared::clone(&authors), ); let mut app = App::new(1); @@ -2518,7 +2518,7 @@ fn event_loop( hide.clone(), false, Default::default(), - gix::features::threading::OwnShared::clone(&authors), + gix::parallel::OwnShared::clone(&authors), history_graph .take() .expect("worktree preview starts only with a cached history graph"), @@ -2544,7 +2544,7 @@ fn event_loop( if app.show_hidden { Vec::new() } else { hide.clone() }, true, Default::default(), - gix::features::threading::OwnShared::clone(&authors), + gix::parallel::OwnShared::clone(&authors), history_graph .take() .expect("ref-tree refresh starts only with a cached history graph"), @@ -2620,7 +2620,7 @@ fn event_loop( hidden, ref_tree.is_active(), expand, - gix::features::threading::OwnShared::clone(&authors), + gix::parallel::OwnShared::clone(&authors), history_graph .take() .expect("refresh starts only with a cached history graph"), @@ -11733,8 +11733,7 @@ mod tests { let repository = test_repository::open(&fixture)?; let topic = repository.rev_parse_single("topic")?.detach(); let main = repository.rev_parse_single("main")?.detach(); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut graph = None; history::load( &repository, @@ -11807,8 +11806,7 @@ mod tests { .status()?; assert!(status.success(), "the configured tracking ref exists"); let repository = test_repository::open(path)?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut graph = None; history::load( &repository, @@ -12380,8 +12378,7 @@ mod tests { gix::refs::transaction::PreviousValue::MustNotExist, "test optional non-commit remote reference", )?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let hidden = vec![OsString::from("merged")]; let mut graph = HistoryGraph::default(); let history = graph.refresh(&repository, &[], &hidden, false, &HashSet::new(), &authors)?; @@ -12574,8 +12571,7 @@ mod tests { let repository = test_repository::open(&worktree)?; let mut repository_path = repository.git_dir().to_owned(); std::env::set_current_dir(&worktree)?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut graph = HistoryGraph::default(); let expected = graph .refresh(&repository, &[], &[], false, &Default::default(), &authors)? @@ -13757,8 +13753,7 @@ mod tests { fn tree_selection_keys_require_deliberate_confirmations_and_restore_normal_navigation() -> gix_testtools::Result { let fixture = gix_testtools::scripted_fixture_read_only("rebase_edit.sh")?; let repository = test_repository::open(&fixture)?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut graph = HistoryGraph::default(); let history = graph.refresh(&repository, &[], &[], false, &HashSet::new(), &authors)?; let mut app = App::new(10); @@ -15087,8 +15082,7 @@ mod tests { .collect::<gix_testtools::Result<Vec<_>>>()?; let mut app = App::new(1); app.extend_commits(rows); - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let commits = load_rebase_todo_commits(&repository, &mut app, &authors, &scope)?; @@ -15117,8 +15111,7 @@ mod tests { ["user.name=preview author", "user.email=preview@example.com"], )?; let before = gix_testtools::repository::snapshot(fixture.path())?; - let authors = - gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new(Authors::default())); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(Authors::default())); let mut graph = HistoryGraph::default(); let history = graph.refresh(&repo, &[], &["HEAD~2".into()], false, &HashSet::new(), &authors)?; let mut app = App::new(usize::MAX); diff --git a/gix-tix/src/logging.rs b/gix-tix/src/logging.rs index a40a366e767..ad3bdc8ae1b 100644 --- a/gix-tix/src/logging.rs +++ b/gix-tix/src/logging.rs @@ -6,7 +6,7 @@ use std::{ time::{Duration, SystemTime}, }; -use gix::features::threading::{Mutable, OwnShared, lock}; +use gix::parallel::{Mutable, OwnShared, lock}; use gix::{ Result, error::{OptionExt, ResultExt, message}, diff --git a/gix-tix/src/ref_tree.rs b/gix-tix/src/ref_tree.rs index 5963df8b1fb..9ba3f665680 100644 --- a/gix-tix/src/ref_tree.rs +++ b/gix-tix/src/ref_tree.rs @@ -1560,9 +1560,7 @@ pub(crate) fn render_full( } } let mut refs = crate::history::snapshot(repository, &visible_revisions, hidden, true)?; - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( - crate::history::Authors::default(), - )); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(crate::history::Authors::default())); let mut graph = None; crate::history::load( repository, diff --git a/gix-tix/src/worktrunk/mod.rs b/gix-tix/src/worktrunk/mod.rs index 97ecc2edab3..c51e930905d 100644 --- a/gix-tix/src/worktrunk/mod.rs +++ b/gix-tix/src/worktrunk/mod.rs @@ -1854,9 +1854,7 @@ mod tests { ); assert_eq!(head.commit_id, Some(topic_id)); let hidden = crate::history::available_hidden_revisions(&worktree, &[], true)?.0; - let authors = gix::features::threading::OwnShared::new(gix::features::threading::Mutable::new( - crate::history::Authors::default(), - )); + let authors = gix::parallel::OwnShared::new(gix::parallel::Mutable::new(crate::history::Authors::default())); let mut graph = crate::history::HistoryGraph::default(); let refresh = graph.refresh(&worktree, &[], &hidden, false, &Default::default(), &authors)?; let loaded = graph_metadata(&worktree, &graph, &refresh.refs)?; diff --git a/gix-trace/Cargo.toml b/gix-trace/Cargo.toml index b838eef9f41..9b0972d7920 100644 --- a/gix-trace/Cargo.toml +++ b/gix-trace/Cargo.toml @@ -13,7 +13,7 @@ include = ["/src/**/*", "/LICENSE-*"] [features] -## Note that there is no default feature, as the application is supposed to opt-in via `gix-features` or using this crate directly. +## There is no default feature: applications opt in through `gix` or this crate directly. default = [] ## Implement `tracing` with the `tracing` crate, which provides applications with valuable performance details if they opt-in to it. diff --git a/gix-trace/src/lib.rs b/gix-trace/src/lib.rs index 22404266d3f..69170928828 100644 --- a/gix-trace/src/lib.rs +++ b/gix-trace/src/lib.rs @@ -1,8 +1,8 @@ //! A crate providing macros for creating spans in various detail levels. `coarse!` should be used for top-level operations, whereas //! `detail!` should be used in plumbing crates unless their operations are likely to cost a lot of time. //! -//! The application is supposed to explicitly turn on tracing via `gix-features`. -//! Crates that use `gix-features` should use `gix_features::trace`, and those who don't can use `gix_trace` directly. +//! Applications opt in through this crate's `tracing` feature or the corresponding feature on `gix`. +//! Plumbing crates use `gix_trace` directly, and applications using `gix` can use its `trace` reexport. //! Enable `forest` to collect and print trace trees; its module documentation explains connecting worker spans to their parent. //! ## Feature Flags #![cfg_attr( diff --git a/gix-transport/Cargo.toml b/gix-transport/Cargo.toml index a837ec5e209..d19e9838253 100644 --- a/gix-transport/Cargo.toml +++ b/gix-transport/Cargo.toml @@ -23,7 +23,7 @@ blocking-client = ["gix-packetline/blocking-io"] http-client = [ "base64", "parking_lot", - "gix-features/io-pipe", + "gix-utils/io-pipe", "blocking-client", "gix-credentials", ] @@ -91,10 +91,11 @@ path = "tests/async-transport.rs" required-features = ["async-client"] [dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", optional = true } gix-command = { version = "^0.11.0", path = "../gix-command" } gix-error = { version = "^0.4.0", path = "../gix-error" } gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features" } gix-url = { version = "^0.39.0", path = "../gix-url" } gix-sec = { version = "^0.15.0", path = "../gix-sec" } gix-packetline = { version = "^0.23.0", path = "../gix-packetline" } diff --git a/gix-transport/src/client/async_io/request.rs b/gix-transport/src/client/async_io/request.rs index 062de3e1352..ebe3404f6da 100644 --- a/gix-transport/src/client/async_io/request.rs +++ b/gix-transport/src/client/async_io/request.rs @@ -69,26 +69,26 @@ impl<'a> RequestWriter<'a> { match message { MessageKind::Flush => { if self.trace { - gix_features::trace::trace!(">> FLUSH"); + gix_trace::trace!(">> FLUSH"); } encode::write_packet_line(&gix_packetline::PacketLineRef::Flush, self.writer.inner_mut()).await } MessageKind::Delimiter => { if self.trace { - gix_features::trace::trace!(">> DELIM"); + gix_trace::trace!(">> DELIM"); } encode::write_packet_line(&gix_packetline::PacketLineRef::Delimiter, self.writer.inner_mut()).await } MessageKind::ResponseEnd => { if self.trace { - gix_features::trace::trace!(">> RESPONSE_END"); + gix_trace::trace!(">> RESPONSE_END"); } encode::write_packet_line(&gix_packetline::PacketLineRef::ResponseEnd, self.writer.inner_mut()).await } MessageKind::Text(t) => { if self.trace { use bstr::ByteSlice; - gix_features::trace::trace!(">> {}", t.as_bstr()); + gix_trace::trace!(">> {}", t.as_bstr()); } encode::write_text(&gix_packetline::TextRef::from(t), self.writer.inner_mut()).await } diff --git a/gix-transport/src/client/blocking_io/file.rs b/gix-transport/src/client/blocking_io/file.rs index 256ba3bc67c..99408ca949b 100644 --- a/gix-transport/src/client/blocking_io/file.rs +++ b/gix-transport/src/client/blocking_io/file.rs @@ -243,7 +243,7 @@ fn supervise_stderr( std::thread::Builder::new() .name("supervise ssh stderr".into()) .stack_size(128 * 1024) - .spawn(gix_features::trace::in_thread(move || -> std::io::Result<()> { + .spawn(gix_trace::in_thread(move || -> std::io::Result<()> { let mut process_stderr = std::io::stderr(); for line in std::io::BufReader::new(stderr).byte_lines() { let line = line?; @@ -287,7 +287,7 @@ impl client::blocking_io::Transport for SpawnProcessOnDemand { }; let mut cmd = into_std_command(cmd, &cmd_name)?; - gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand"); + gix_trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand"); let mut child = match cmd.spawn() { Ok(child) => child, Err(err) if ssh_kind.is_none() && err.kind() == std::io::ErrorKind::NotFound => { @@ -295,7 +295,7 @@ impl client::blocking_io::Transport for SpawnProcessOnDemand { // the service can still be run through it (#2313). let (cmd, cmd_name) = self.prepare_fallback_command(service); let mut cmd = into_std_command(cmd, &cmd_name)?; - gix_features::trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand (fallback)"); + gix_trace::debug!(command = ?cmd, "gix_transport::SpawnProcessOnDemand (fallback)"); cmd.spawn().map_err(|err| client::Error::InvokeProgram { source: err, command: cmd_name, diff --git a/gix-transport/src/client/blocking_io/http/curl/mod.rs b/gix-transport/src/client/blocking_io/http/curl/mod.rs index 40f1d2ce640..cd3202dace6 100644 --- a/gix-transport/src/client/blocking_io/http/curl/mod.rs +++ b/gix-transport/src/client/blocking_io/http/curl/mod.rs @@ -7,7 +7,7 @@ use std::{ }; use gix_error::{ClassificationMarker, Error, Result, ResultExt, message}; -use gix_features::io; +use gix_utils::io; use parking_lot::Mutex; use crate::client::blocking_io::http::{self, traits::PostBodyDataKind}; diff --git a/gix-transport/src/client/blocking_io/http/curl/remote.rs b/gix-transport/src/client/blocking_io/http/curl/remote.rs index e551c26e33d..1726efbf313 100644 --- a/gix-transport/src/client/blocking_io/http/curl/remote.rs +++ b/gix-transport/src/client/blocking_io/http/curl/remote.rs @@ -12,7 +12,7 @@ use std::{ use bstr::ByteSlice; use curl::easy::{Auth, Easy2}; use gix_error::{ClassificationMarker, Error, ErrorExt, OptionExt, Result, ResultExt, message}; -use gix_features::io::pipe; +use gix_utils::io::pipe; use parking_lot::Mutex; use crate::client::blocking_io::http::{ @@ -414,7 +414,7 @@ pub fn new() -> Worker { let redirected_base_url_shared_out = redirected_base_url_shared.clone(); let (req_send, req_recv) = sync_channel(0); let (res_send, res_recv) = sync_channel(0); - let handle = std::thread::spawn(gix_features::trace::in_thread(move || -> Result { + let handle = std::thread::spawn(gix_trace::in_thread(move || -> Result { let mut handle = Easy2::new(Handler::default()); // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway. curl!(handle.pipewait(false)); @@ -703,7 +703,7 @@ mod authentication_tests { #[test] fn only_current_challenges_are_collected_and_continuations_are_unfolded() { - let (writer, mut reader) = gix_features::io::pipe::unidirectional(16); + let (writer, mut reader) = gix_utils::io::pipe::unidirectional(16); let mut handler = super::Handler { send_header: Some(writer), ..Default::default() diff --git a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs index 1112afbb249..8cbff635fc8 100644 --- a/gix-transport/src/client/blocking_io/http/reqwest/remote.rs +++ b/gix-transport/src/client/blocking_io/http/reqwest/remote.rs @@ -6,7 +6,7 @@ use std::{ }; use gix_error::{Class, Error, ErrorExt, Result, ResultExt, message}; -use gix_features::io::pipe; +use gix_utils::io::pipe; use parking_lot::Mutex; use crate::client::blocking_io::http::{ @@ -45,7 +45,7 @@ impl Default for Remote { let (res_send, res_recv) = std::sync::mpsc::sync_channel(0); let redirected_base_url_shared = Arc::new(Mutex::new(None)); let redirected_base_url_shared_for_field = redirected_base_url_shared.clone(); - let handle = std::thread::spawn(gix_features::trace::in_thread(move || -> Result { + let handle = std::thread::spawn(gix_trace::in_thread(move || -> Result { let mut follow = None; let redirect_action = Arc::new(Mutex::new(RedirectAction::Stop)); let redirect_tail = Arc::new(Mutex::new(String::new())); diff --git a/gix-transport/src/client/blocking_io/request.rs b/gix-transport/src/client/blocking_io/request.rs index 41f80575012..afa8845af84 100644 --- a/gix-transport/src/client/blocking_io/request.rs +++ b/gix-transport/src/client/blocking_io/request.rs @@ -18,7 +18,7 @@ pub struct RequestWriter<'a> { impl io::Write for RequestWriter<'_> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { if self.trace { - gix_features::trace::trace!(">> {}", bstr::BStr::new(buf)); + gix_trace::trace!(">> {}", bstr::BStr::new(buf)); } self.writer.write(buf) } @@ -59,25 +59,25 @@ impl<'a> RequestWriter<'a> { match message { MessageKind::Flush => { if self.trace { - gix_features::trace::trace!(">> FLUSH"); + gix_trace::trace!(">> FLUSH"); } encode::write_packet_line(&gix_packetline::PacketLineRef::Flush, self.writer.inner_mut()) } MessageKind::Delimiter => { if self.trace { - gix_features::trace::trace!(">> DELIM"); + gix_trace::trace!(">> DELIM"); } encode::write_packet_line(&gix_packetline::PacketLineRef::Delimiter, self.writer.inner_mut()) } MessageKind::ResponseEnd => { if self.trace { - gix_features::trace::trace!(">> RESPONSE_END"); + gix_trace::trace!(">> RESPONSE_END"); } encode::write_packet_line(&gix_packetline::PacketLineRef::ResponseEnd, self.writer.inner_mut()) } MessageKind::Text(t) => { if self.trace { - gix_features::trace::trace!(">> {}", bstr::BStr::new(t)); + gix_trace::trace!(">> {}", bstr::BStr::new(t)); } encode::write_text(&gix_packetline::TextRef::from(t), self.writer.inner_mut()) } diff --git a/gix-transport/src/client/blocking_io/ssh/mod.rs b/gix-transport/src/client/blocking_io/ssh/mod.rs index 675f2d79fbf..37f79f91ab4 100644 --- a/gix-transport/src/client/blocking_io/ssh/mod.rs +++ b/gix-transport/src/client/blocking_io/ssh/mod.rs @@ -192,7 +192,7 @@ fn determine_client_kind( let mut kind = known_kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd)); if known_kind.is_none() && kind == ProgramKind::Simple { let mut cmd = build_client_feature_check_command(ssh_cmd, url, disallow_shell)?; - gix_features::trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check"); + gix_trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check"); kind = if cmd.status().ok().is_some_and(|status| status.success()) { ProgramKind::Ssh } else { diff --git a/gix-utils/Cargo.toml b/gix-utils/Cargo.toml index 866279e45c0..d7c6d4fe278 100644 --- a/gix-utils/Cargo.toml +++ b/gix-utils/Cargo.toml @@ -5,7 +5,7 @@ name = "gix-utils" version = "0.4.0" repository = "https://github.com/GitoxideLabs/gitoxide" license = "MIT OR Apache-2.0" -description = "A crate with `gitoxide` utilities that don't need feature toggles" +description = "Utilities shared by the gitoxide crates" authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] edition = "2024" rust-version = "1.88" @@ -19,9 +19,28 @@ name = "precompose" harness = false [features] +## Byte-string conversion utilities. bstr = ["dep:bstr"] +## Progress traits, types, and I/O wrappers backed by `prodash`. +progress = ["dep:prodash"] +## Human-readable numbers in progress units. +progress-unit-human-numbers = ["prodash?/unit-human"] +## Human-readable byte sizes and progress units. +progress-unit-bytes = ["dep:bytesize", "prodash?/unit-bytes"] +## I/O and iterator wrappers that preserve typed cancellation errors. +interrupt = ["dep:gix-error"] +## A bounded in-memory byte pipe. +io-pipe = ["dep:bytes"] +## Count cache hits, misses, and insertions and print diagnostics on drop. +cache-efficiency-debug = [] [dependencies] +gix-error = { version = "^0.4.0", path = "../gix-error", optional = true } +prodash = { version = "31.0.1", optional = true } +bytesize = { version = "2.3.1", optional = true } +bytes = { version = "1.11.1", optional = true } +document-features = { version = "0.2.0", optional = true } + # We seed our own thread-local `Rng` (see `src/rng.rs`), so `fastrand`'s global generator and its # `std` default feature aren't needed. fastrand = { version = "2.4.1", default-features = false } @@ -35,5 +54,16 @@ unicode-normalization = { version = "0.1.19", default-features = false } getrandom = "0.4" [dev-dependencies] +gix-testtools = { path = "../tests/tools", default-features = false, features = ["sha1"] } +insta = "1.46.3" gix-utils = { path = ".", features = ["bstr" ]} criterion = "0.8.2" + +[[test]] +name = "pipe" +path = "tests/pipe.rs" +required-features = ["io-pipe"] + +[package.metadata.docs.rs] +all-features = true +features = ["document-features"] diff --git a/gix-features/src/cache.rs b/gix-utils/src/cache.rs similarity index 100% rename from gix-features/src/cache.rs rename to gix-utils/src/cache.rs diff --git a/gix-features/src/decode.rs b/gix-utils/src/decode.rs similarity index 100% rename from gix-features/src/decode.rs rename to gix-utils/src/decode.rs diff --git a/gix-features/src/interrupt.rs b/gix-utils/src/interrupt.rs similarity index 100% rename from gix-features/src/interrupt.rs rename to gix-utils/src/interrupt.rs diff --git a/gix-features/src/io.rs b/gix-utils/src/io.rs similarity index 100% rename from gix-features/src/io.rs rename to gix-utils/src/io.rs diff --git a/gix-utils/src/iter.rs b/gix-utils/src/iter.rs new file mode 100644 index 00000000000..d49ed07a468 --- /dev/null +++ b/gix-utils/src/iter.rs @@ -0,0 +1,29 @@ +//! Iterator utilities. +/// An iterator over chunks of input, producing `Vec<Item>` with a size of `size`, with the last chunk being the remainder and thus +/// potentially smaller than `size`. +pub struct Chunks<I> { + /// The inner iterator to ask for items. + pub inner: I, + /// The size of chunks to produce + pub size: usize, +} + +impl<I, Item> Iterator for Chunks<I> +where + I: Iterator<Item = Item>, +{ + type Item = Vec<Item>; + + fn next(&mut self) -> Option<Self::Item> { + let mut res = Vec::with_capacity(self.size); + let mut items_left = self.size; + for item in &mut self.inner { + res.push(item); + items_left -= 1; + if items_left == 0 { + break; + } + } + (!res.is_empty()).then_some(res) + } +} diff --git a/gix-utils/src/lib.rs b/gix-utils/src/lib.rs index 4bb5151742a..7d91394dbb8 100644 --- a/gix-utils/src/lib.rs +++ b/gix-utils/src/lib.rs @@ -1,6 +1,6 @@ -//! A crate with utilities that don't need feature toggles. +//! Utilities shared by the `gitoxide` crates. //! -//! If they would need feature toggles, they should be in `gix-features` instead. +//! Utilities requiring additional dependencies are available through feature toggles. //! //! ## Examples //! @@ -16,9 +16,24 @@ //! Duration::from_millis(9), //! ]); //! ``` +//! ## Feature Flags +#![cfg_attr(all(doc, feature = "document-features"), doc = ::document_features::document_features!())] +#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))] #![deny(missing_docs)] #![forbid(unsafe_code)] +/// Cache efficiency diagnostics, enabled with `cache-efficiency-debug`. +pub mod cache; +/// Variable-length integer decoding. +pub mod decode; +#[cfg(feature = "interrupt")] +pub mod interrupt; +#[cfg(feature = "io-pipe")] +pub mod io; +pub mod iter; +#[cfg(feature = "progress")] +pub mod progress; + /// pub mod backoff; diff --git a/gix-features/src/progress.rs b/gix-utils/src/progress.rs similarity index 100% rename from gix-features/src/progress.rs rename to gix-utils/src/progress.rs diff --git a/gix-features/tests/pipe.rs b/gix-utils/tests/pipe.rs similarity index 95% rename from gix-features/tests/pipe.rs rename to gix-utils/tests/pipe.rs index 7c08fe8d001..80c44a300d0 100644 --- a/gix-features/tests/pipe.rs +++ b/gix-utils/tests/pipe.rs @@ -1,11 +1,11 @@ mod io { use std::io::{BufRead, ErrorKind, Read, Write}; - use gix_features::io; + use gix_utils::io; #[test] fn threaded_read_to_end() -> gix_testtools::TestResult { - let (mut writer, mut reader) = gix_features::io::pipe::unidirectional(0); + let (mut writer, mut reader) = gix_utils::io::pipe::unidirectional(0); let message = "Hello, world!"; std::thread::spawn(move || { @@ -100,7 +100,7 @@ mod io { std::thread::spawn(move || { for _ in 0..block_count { let data = &[0; BLOCK_SIZE]; - writer.write_all(data).unwrap(); + writer.write_all(data).expect("reader remains connected"); } }); diff --git a/gix-worktree-state/Cargo.toml b/gix-worktree-state/Cargo.toml index a20718f2ff5..3dd5d7f4bb7 100644 --- a/gix-worktree-state/Cargo.toml +++ b/gix-worktree-state/Cargo.toml @@ -20,16 +20,18 @@ sha1 = ["gix-index/sha1"] ## Enable support for the SHA-256 hash by forwarding the feature to dependencies. sha256 = ["gix-index/sha256"] ## Enable support for thread-safety. -parallel = ["gix-features/parallel"] +parallel = ["gix-parallel/parallel"] [dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["interrupt", "progress"] } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel" } gix-worktree = { version = "^0.57.0", path = "../gix-worktree", default-features = false, features = ["attributes"] } gix-index = { version = "^0.56.0", path = "../gix-index" } gix-fs = { version = "^0.23.0", path = "../gix-fs" } gix-object = { version = "^0.65.0", path = "../gix-object" } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } gix-path = { version = "^0.13.0", path = "../gix-path" } -gix-features = { version = "^0.50.0", path = "../gix-features" } gix-filter = { version = "^0.35.0", path = "../gix-filter" } io-close = "0.3.7" diff --git a/gix-worktree-state/src/checkout/chunk.rs b/gix-worktree-state/src/checkout/chunk.rs index 3ead8fc2e36..9c04ca5f20e 100644 --- a/gix-worktree-state/src/checkout/chunk.rs +++ b/gix-worktree-state/src/checkout/chunk.rs @@ -16,7 +16,7 @@ mod reduce { pub aggregate: super::Outcome<'entry>, } - impl<'entry> gix_features::parallel::Reduce for Reduce<'entry> { + impl<'entry> gix_parallel::Reduce for Reduce<'entry> { type Input = Result<super::Outcome<'entry>>; type FeedProduce = (); type Output = super::Outcome<'entry>; @@ -283,7 +283,7 @@ where fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { let written = self.inner.write(buf)?; self.progress - .fetch_add(written as gix_features::progress::Step, Ordering::SeqCst); + .fetch_add(written as gix_utils::progress::Step, Ordering::SeqCst); Ok(written) } @@ -372,7 +372,7 @@ fn is_collision( } // We are here because a file existed or was blocked by a directory which shouldn't be possible unless // we are on a file insensitive file system. - gix_features::trace::error!("{entry_path}: collided ({:?})", err.kind()); + gix_trace::error!("{entry_path}: collided ({:?})", err.kind()); collisions.push(checkout::Collision { path: entry_path.into(), error_kind: err.kind(), diff --git a/gix-worktree-state/src/checkout/entry.rs b/gix-worktree-state/src/checkout/entry.rs index fe432e91c5b..e4a51bfeac7 100644 --- a/gix-worktree-state/src/checkout/entry.rs +++ b/gix-worktree-state/src/checkout/entry.rs @@ -181,14 +181,14 @@ where obj.data.len() } gix_index::entry::Mode::DIR => { - gix_features::trace::warn!( + gix_trace::warn!( "Skipped sparse directory at \"{entry_path}\" ({id}) as it cannot yet be handled", id = entry.id ); 0 } gix_index::entry::Mode::COMMIT => { - gix_features::trace::warn!( + gix_trace::warn!( "Skipped submodule at \"{entry_path}\" ({id}) as it cannot yet be handled", id = entry.id ); diff --git a/gix-worktree-state/src/checkout/function.rs b/gix-worktree-state/src/checkout/function.rs index 9bf8f350fa1..8abde5e4bbb 100644 --- a/gix-worktree-state/src/checkout/function.rs +++ b/gix-worktree-state/src/checkout/function.rs @@ -2,7 +2,8 @@ use gix_error::Result; use std::sync::atomic::AtomicBool; use gix_error::{ResultExt, message}; -use gix_features::{interrupt, parallel::in_parallel_with_finalize}; +use gix_parallel::in_parallel_with_finalize; +use gix_utils::interrupt; use gix_worktree::{Stack, stack}; use crate::checkout::chunk; @@ -21,8 +22,8 @@ pub fn checkout<Find>( index: &mut gix_index::State, dir: impl Into<std::path::PathBuf>, objects: Find, - files: &dyn gix_features::progress::Count, - bytes: &dyn gix_features::progress::Count, + files: &dyn gix_utils::progress::Count, + bytes: &dyn gix_utils::progress::Count, should_interrupt: &AtomicBool, options: crate::checkout::Options, ) -> Result<crate::checkout::Outcome> @@ -41,8 +42,8 @@ fn checkout_inner<Find>( paths: &gix_index::PathStorage, dir: impl Into<std::path::PathBuf>, objects: Find, - files: &dyn gix_features::progress::Count, - bytes: &dyn gix_features::progress::Count, + files: &dyn gix_utils::progress::Count, + bytes: &dyn gix_utils::progress::Count, should_interrupt: &AtomicBool, mut options: crate::checkout::Options, ) -> Result<crate::checkout::Outcome> @@ -52,7 +53,7 @@ where let num_files = files.counter(); let num_bytes = bytes.counter(); let dir = dir.into(); - let (chunk_size, thread_limit, num_threads) = gix_features::parallel::optimize_chunk_size_and_thread_limit( + let (chunk_size, thread_limit, num_threads) = gix_parallel::optimize_chunk_size_and_thread_limit( 100, index.entries().len().into(), options.thread_limit, @@ -104,7 +105,7 @@ where } else { let entries_with_paths = interrupt::Iter::new(index.entries_mut_with_paths_in(paths), should_interrupt); in_parallel_with_finalize( - gix_features::iter::Chunks { + gix_utils::iter::Chunks { inner: entries_with_paths, size: chunk_size, }, diff --git a/gix-worktree-state/tests/worktree-state/checkout.rs b/gix-worktree-state/tests/worktree-state/checkout.rs index 3c26a50ec95..32522f240db 100644 --- a/gix-worktree-state/tests/worktree-state/checkout.rs +++ b/gix-worktree-state/tests/worktree-state/checkout.rs @@ -9,9 +9,9 @@ use std::{ sync::atomic::{AtomicBool, AtomicUsize, Ordering}, }; -use gix_features::progress; use gix_object::{Data, bstr::ByteSlice}; use gix_testtools::tempfile::TempDir; +use gix_utils::progress; use gix_worktree_state::checkout::Collision; use std::sync::LazyLock; @@ -987,7 +987,7 @@ fn collisions_are_detected_on_a_case_insensitive_filesystem_even_with_delayed_fi } fn multi_threaded() -> bool { - gix_features::parallel::num_threads(None) > 1 + gix_parallel::num_threads(None) > 1 } fn assert_equality(source_tree: &Path, destination: &TempDir, allow_symlinks: bool) -> Result<usize> { @@ -1117,7 +1117,7 @@ fn opts_from_probe() -> gix_worktree_state::checkout::Options { gix_worktree_state::checkout::Options { fs: *CAPABILITIES, destination_is_initially_empty: true, - thread_limit: gix_features::parallel::num_threads(None).into(), + thread_limit: gix_parallel::num_threads(None).into(), ..gix_worktree_state::checkout::Options::new(gix_filter::Pipeline::new( Default::default(), gix_testtools::object_hash(), diff --git a/gix-worktree-stream/Cargo.toml b/gix-worktree-stream/Cargo.toml index 459c2d750da..97ae2d01bf5 100644 --- a/gix-worktree-stream/Cargo.toml +++ b/gix-worktree-stream/Cargo.toml @@ -21,7 +21,8 @@ sha1 = ["gix-hash/sha1"] sha256 = ["gix-hash/sha256"] [dependencies] -gix-features = { version = "^0.50.0", path = "../gix-features", features = ["progress", "io-pipe", "parallel"] } +gix-trace = { version = "^0.2.0", path = "../gix-trace" } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["io-pipe"] } gix-hash = { version = "^0.27.0", path = "../gix-hash" } gix-object = { version = "^0.65.0", path = "../gix-object" } gix-attributes = { version = "^0.36.0", path = "../gix-attributes", features = ["parallel"] } diff --git a/gix-worktree-stream/src/from_tree/mod.rs b/gix-worktree-stream/src/from_tree/mod.rs index 88a9d352b2f..5f2675bc554 100644 --- a/gix-worktree-stream/src/from_tree/mod.rs +++ b/gix-worktree-stream/src/from_tree/mod.rs @@ -51,7 +51,7 @@ where E: std::error::Error + Send + Sync + 'static, { let (stream, mut write, additional_entries) = Stream::new(); - std::thread::spawn(gix_features::trace::in_thread({ + std::thread::spawn(gix_trace::in_thread({ let slot = stream.err.clone(); move || { if let Err(err) = run( @@ -89,7 +89,7 @@ fn run<Find, E>( ) -> std::result::Result<(), E> + Send + 'static, - out: &mut gix_features::io::pipe::Writer, + out: &mut gix_utils::io::pipe::Writer, err: SharedErrorSlot, additional_entries: std::sync::mpsc::Receiver<AdditionalEntry>, ) -> Result diff --git a/gix-worktree-stream/src/from_tree/traverse.rs b/gix-worktree-stream/src/from_tree/traverse.rs index 534a8b68760..5a537179ce8 100644 --- a/gix-worktree-stream/src/from_tree/traverse.rs +++ b/gix-worktree-stream/src/from_tree/traverse.rs @@ -19,7 +19,7 @@ pub struct Delegate<'a, AttributesFn, Find> where Find: gix_object::Find, { - pub(crate) out: &'a mut gix_features::io::pipe::Writer, + pub(crate) out: &'a mut gix_utils::io::pipe::Writer, pub(crate) err: SharedErrorSlot, pub(crate) path_deque: VecDeque<BString>, pub(crate) path: BString, diff --git a/gix-worktree-stream/src/lib.rs b/gix-worktree-stream/src/lib.rs index 5a8cacd60e4..93017c4f18b 100644 --- a/gix-worktree-stream/src/lib.rs +++ b/gix-worktree-stream/src/lib.rs @@ -178,14 +178,14 @@ impl Stream { impl Stream { pub(crate) fn new() -> ( Stream, - gix_features::io::pipe::Writer, + gix_utils::io::pipe::Writer, std::sync::mpsc::Receiver<AdditionalEntry>, ) { // 1 write for entry header and 1 for hash, 1 for entry path, + 1 for a buffer, then 32 of these. // Giving some buffer, at the expense of memory, is important to allow consumers to take off bytes more quickly, // otherwise, both threads effectively run in lock-step and nullify the benefit. let in_flight_writes = (2 + 1) * 32; - let (write, read) = gix_features::io::pipe::unidirectional(in_flight_writes); + let (write, read) = gix_utils::io::pipe::unidirectional(in_flight_writes); let (tx_entries, rx_entries) = std::sync::mpsc::channel(); ( Stream { @@ -205,7 +205,7 @@ impl Stream { pub(crate) mod utils { pub enum Read { - Known(gix_features::io::pipe::Reader), + Known(gix_utils::io::pipe::Reader), Unknown(Box<dyn std::io::Read>), } diff --git a/gix-worktree-stream/src/protocol.rs b/gix-worktree-stream/src/protocol.rs index 409271c88b2..b15085ad6ff 100644 --- a/gix-worktree-stream/src/protocol.rs +++ b/gix-worktree-stream/src/protocol.rs @@ -35,7 +35,7 @@ pub(crate) fn write_entry_header_and_path( oid: &gix_hash::oid, mode: gix_object::tree::EntryMode, stream_len: Option<usize>, - out: &mut gix_features::io::pipe::Writer, + out: &mut gix_utils::io::pipe::Writer, ) -> std::io::Result<()> { const HEADER_LEN: usize = std::mem::size_of::<usize>() * 2 + 2; let mut buf = [0u8; HEADER_LEN + gix_hash::Kind::longest().len_in_bytes()]; @@ -64,7 +64,7 @@ pub(crate) fn write_entry_header_and_path( pub(crate) fn write_stream( buf: &mut Vec<u8>, mut input: impl std::io::Read, - out: &mut gix_features::io::pipe::Writer, + out: &mut gix_utils::io::pipe::Writer, ) -> std::io::Result<()> { const BUF_LEN: usize = u16::MAX as usize; clear_and_set_len(buf, BUF_LEN)?; diff --git a/gix-worktree/Cargo.toml b/gix-worktree/Cargo.toml index 2cd22a7c59c..b78e78c60a6 100644 --- a/gix-worktree/Cargo.toml +++ b/gix-worktree/Cargo.toml @@ -23,15 +23,18 @@ sha256 = ["gix-hash/sha256"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "bstr/serde", "gix-index/serde", "gix-hash/serde", "gix-object/serde", "gix-attributes?/serde", "gix-ignore/serde"] ## Enable thread-safety. -parallel = ["gix-features/parallel", "gix-attributes?/parallel"] +parallel = ["gix-parallel/parallel", "gix-attributes?/parallel"] ## Instantiate stacks that can access `.gitattributes` information. attributes = ["dep:gix-attributes", "dep:gix-validate"] ## Add linked worktrees by preparing their administrative files. add = ["dep:gix-validate"] ## Remove linked worktrees recursively, with parallel traversal and deletion. -remove = ["dep:dua-core", "gix-features/progress"] +remove = ["dep:dua-core", "dep:gix-trace", "gix-utils/progress"] [dependencies] +gix-trace = { version = "^0.2.0", path = "../gix-trace", optional = true } +gix-utils = { version = "^0.4.0", path = "../gix-utils", optional = true } +gix-parallel = { version = "^0.50.0", path = "../gix-parallel", optional = true } gix-error = { version = "^0.4.0", path = "../gix-error" } gix-index = { version = "^0.56.0", path = "../gix-index" } gix-fs = { version = "^0.23.0", path = "../gix-fs" } @@ -42,7 +45,6 @@ gix-path = { version = "^0.13.0", path = "../gix-path" } gix-attributes = { version = "^0.36.0", path = "../gix-attributes", optional = true } gix-validate = { version = "^0.12.0", path = "../gix-validate", optional = true } gix-ignore = { version = "^0.23.0", path = "../gix-ignore" } -gix-features = { version = "^0.50.0", path = "../gix-features", optional = true } dua-core = { version = "4.1.0", optional = true } @@ -65,5 +67,5 @@ features = ["sha1", "document-features", "serde"] [package.metadata.cargo-machete] ignored = [ # Forwarding-only dependency for test feature parity after dissolving gix-worktree-tests. - "gix-features", + "gix-parallel", ] diff --git a/gix-worktree/src/remove.rs b/gix-worktree/src/remove.rs index 31dc7c83321..209d8d60628 100644 --- a/gix-worktree/src/remove.rs +++ b/gix-worktree/src/remove.rs @@ -6,7 +6,7 @@ use std::{ sync::atomic::Ordering, }; -use gix_features::progress::Progress; +use gix_utils::progress::Progress; /// Options for removing a linked worktree and its administrative directory. #[derive(Debug, Clone, Copy)] @@ -114,7 +114,7 @@ pub(super) mod _impl { use std::{fs, path::Path}; - use gix_features::progress::NestedProgress; + use gix_utils::progress::NestedProgress; /// Recursively remove `work_dir` and its private `git_dir` without following symbolic links. /// @@ -196,7 +196,7 @@ fn remove_root_with_after_scan( ) -> Result<(), DirectoryError> { let root = normalize_root(root)?; let root = root.as_path(); - scan.init(None, gix_features::progress::count("entries")); + scan.init(None, gix_utils::progress::count("entries")); #[cfg(unix)] let may_descend = { let containing_device = root @@ -263,7 +263,7 @@ fn remove_root_with_after_scan( remove.init( Some(leaves.len() + directories.len()), - gix_features::progress::count("entries"), + gix_utils::progress::count("entries"), ); let counter = remove.counter(); let mut first_error = remove_leaves(&leaves, num_threads, |path, is_symlink| { @@ -398,7 +398,7 @@ fn remove_leaves( let remove_chunk = &remove_chunk; let handles: Vec<_> = leaves .chunks(leaves.len().div_ceil(num_threads)) - .map(|chunk| scope.spawn(gix_features::trace::in_thread(move || remove_chunk(chunk)))) + .map(|chunk| scope.spawn(gix_trace::in_thread(move || remove_chunk(chunk)))) .collect(); handles.into_iter().fold(None, |first_error, handle| { let error = handle.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic)); @@ -591,8 +591,8 @@ mod tests { super::remove_root_with_after_scan( &root, - gix_features::progress::Discard, - gix_features::progress::Discard, + gix_utils::progress::Discard, + gix_utils::progress::Discard, options, || { scans += 1; @@ -624,8 +624,8 @@ mod tests { let err = super::remove_root_with_after_scan( &root, - gix_features::progress::Discard, - gix_features::progress::Discard, + gix_utils::progress::Discard, + gix_utils::progress::Discard, Options { max_retries: 5, ..Options::default() @@ -661,8 +661,8 @@ mod tests { let err = super::remove_root_with_after_scan( &root, - gix_features::progress::Discard, - gix_features::progress::Discard, + gix_utils::progress::Discard, + gix_utils::progress::Discard, Options { max_retries, ..Options::default() diff --git a/gix-worktree/tests/worktree/remove.rs b/gix-worktree/tests/worktree/remove.rs index 533d2999a3e..9da8386b1b3 100644 --- a/gix-worktree/tests/worktree/remove.rs +++ b/gix-worktree/tests/worktree/remove.rs @@ -30,7 +30,7 @@ fn relative_roots_are_rejected_before_either_root_is_removed() -> gix_testtools: let err = gix_worktree::remove( work_dir_input, git_dir_input, - gix_features::progress::Discard, + gix_utils::progress::Discard, Options::default(), ) .expect_err("both roots must be absolute before deletion can begin"); @@ -74,7 +74,7 @@ fn removes_both_roots_with_configured_thread_limits_without_retries() -> gix_tes gix_worktree::remove( &work_dir, &git_dir, - gix_features::progress::Discard, + gix_utils::progress::Discard, Options { thread_limit, max_retries: 0, @@ -111,7 +111,7 @@ fn removes_both_roots_and_does_not_follow_symlinks() -> gix_testtools::TestResul return Err(err.into()); } - gix_worktree::remove(&work_dir, &git_dir, gix_features::progress::Discard, Options::default())?; + gix_worktree::remove(&work_dir, &git_dir, gix_utils::progress::Discard, Options::default())?; assert!(!work_dir.exists(), "the checkout was removed"); assert!(!git_dir.exists(), "the private Git directory was removed"); @@ -142,7 +142,7 @@ fn symlink_roots_with_or_without_a_trailing_separator_are_unlinked() -> gix_test work_dir.clone() }; - gix_worktree::remove(&root, &git_dir, gix_features::progress::Discard, Options::default())?; + gix_worktree::remove(&root, &git_dir, gix_utils::progress::Discard, Options::default())?; assert!(!work_dir.exists(), "the symlink itself was removed"); assert!(!git_dir.exists(), "the private Git directory was removed"); @@ -161,7 +161,7 @@ fn missing_roots_are_already_removed() -> gix_testtools::TestResult { gix_worktree::remove( tmp.path().join("missing-worktree"), tmp.path().join("repo.git/worktrees/missing"), - gix_features::progress::Discard, + gix_utils::progress::Discard, Options::default(), )?; let non_directory = tmp.path().join("file"); @@ -169,7 +169,7 @@ fn missing_roots_are_already_removed() -> gix_testtools::TestResult { gix_worktree::remove( non_directory.join("missing-worktree"), tmp.path().join("repo.git/worktrees/missing"), - gix_features::progress::Discard, + gix_utils::progress::Discard, Options::default(), )?; Ok(()) @@ -185,7 +185,7 @@ fn only_the_conventional_empty_worktrees_parent_is_removed() -> gix_testtools::T gix_worktree::remove( tmp.path().join("missing-worktree"), &git_dir, - gix_features::progress::Discard, + gix_utils::progress::Discard, Options::default(), )?; @@ -223,7 +223,7 @@ fn unreadable_directories_match_git() -> gix_testtools::TestResult { gix_worktree::remove( &work_dir, &git_dir, - gix_features::progress::Discard, + gix_utils::progress::Discard, Options { max_retries: 0, ..Options::default() @@ -291,7 +291,7 @@ fn administrative_data_is_removed_after_checkout_removal_fails() -> gix_testtool fs::create_dir_all(&git_dir)?; fs::write(git_dir.join("HEAD"), b"ref: refs/heads/topic\n")?; - let result = gix_worktree::remove(&work_dir, &git_dir, gix_features::progress::Discard, Options::default()); + let result = gix_worktree::remove(&work_dir, &git_dir, gix_utils::progress::Discard, Options::default()); if work_dir.exists() { fs::set_permissions(&work_dir, fs::Permissions::from_mode(0o700))?; } @@ -322,7 +322,7 @@ fn readonly_files_do_not_prevent_removal() -> gix_testtools::TestResult { permissions.set_readonly(true); fs::set_permissions(&readonly, permissions)?; - gix_worktree::remove(&work_dir, &git_dir, gix_features::progress::Discard, Options::default())?; + gix_worktree::remove(&work_dir, &git_dir, gix_utils::progress::Discard, Options::default())?; assert!(!work_dir.exists(), "the checkout was removed"); assert!(!git_dir.exists(), "the private Git directory was removed"); diff --git a/gix/Cargo.toml b/gix/Cargo.toml index ec804fd22f1..20a10a6dee8 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -78,10 +78,11 @@ extras = [ ] ## Various progress-related features that improve the look of progress message units. -comfort = [ - "gix-features/progress-unit-bytes", - "gix-features/progress-unit-human-numbers", -] +comfort = ["progress-unit-bytes", "progress-unit-human-numbers"] +## Display progress counts with human-readable number units. +progress-unit-human-numbers = ["gix-utils/progress-unit-human-numbers"] +## Display byte sizes and progress with human-readable byte units. +progress-unit-bytes = ["gix-utils/progress-unit-bytes"] #! #### Hashes #! @@ -271,7 +272,7 @@ hp-tempfile-registry = ["gix-tempfile/hp-hashmap"] ## ## If unset, most of `gix` can only be used in a single thread as data structures won't be `Send` anymore. parallel = [ - "gix-features/parallel", + "gix-parallel/parallel", "gix-pack/parallel", "gix-attributes?/parallel", "gix-filter?/parallel", @@ -307,10 +308,10 @@ anyhow = ["gix-error/anyhow"] tree-error = ["gix-error/tree-error"] ## Enable tracing using the `tracing` crate for coarse tracing. -tracing = ["gix-features/tracing"] +tracing = ["gix-trace/tracing"] ## Enable tracing using the `tracing` crate for detailed tracing. Also enables coarse tracing. -tracing-detail = ["gix-features/tracing-detail", "tracing"] +tracing-detail = ["gix-trace/tracing-detail", "tracing"] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = [ @@ -337,11 +338,12 @@ serde = [ progress-tree = ["prodash/progress-tree"] ## Print debugging information about usage of object database caches, useful for tuning cache sizes. -cache-efficiency-debug = ["gix-features/cache-efficiency-debug"] +cache-efficiency-debug = ["gix-utils/cache-efficiency-debug"] [dependencies] +gix-parallel = { version = "^0.50.0", path = "../gix-parallel", features = ["once_cell"] } gix-error = { version = "^0.4.0", path = "../gix-error", features = ["bstr"] } -gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["bstr"] } +gix-utils = { version = "^0.4.0", path = "../gix-utils", features = ["bstr", "interrupt", "progress"] } gix-fs = { version = "^0.23.0", path = "../gix-fs" } gix-ref = { version = "^0.68.0", path = "../gix-ref" } gix-discover = { version = "^0.56.0", path = "../gix-discover" } @@ -376,10 +378,6 @@ gix-diff = { version = "^0.68.0", path = "../gix-diff", default-features = false gix-merge = { version = "^0.21.0", path = "../gix-merge", default-features = false, optional = true } gix-mailmap = { version = "^0.35.0", path = "../gix-mailmap", optional = true } gix-note = { version = "^0.2.0", path = "../gix-note", optional = true } -gix-features = { version = "^0.50.0", path = "../gix-features", features = [ - "progress", - "once_cell", -] } gix-trace = { version = "^0.2.0", path = "../gix-trace" } gix-glob = { version = "^0.28.0", path = "../gix-glob" } diff --git a/gix/src/clone/checkout.rs b/gix/src/clone/checkout.rs index 95f437a98d7..f896d060a3f 100644 --- a/gix/src/clone/checkout.rs +++ b/gix/src/clone/checkout.rs @@ -19,7 +19,7 @@ pub mod main_worktree { BytesWritten, } - impl From<ProgressId> for gix_features::progress::Id { + impl From<ProgressId> for gix_utils::progress::Id { fn from(v: ProgressId) -> Self { match v { ProgressId::CheckoutFiles => *b"CLCF", @@ -47,15 +47,15 @@ pub mod main_worktree { should_interrupt: &AtomicBool, ) -> Result<(Repository, gix_worktree_state::checkout::Outcome)> where - P: gix_features::progress::NestedProgress, - P::SubProgress: gix_features::progress::NestedProgress + 'static, + P: gix_utils::progress::NestedProgress, + P::SubProgress: gix_utils::progress::NestedProgress + 'static, { self.main_worktree_inner(&mut progress, should_interrupt) } fn main_worktree_inner( &mut self, - progress: &mut dyn gix_features::progress::DynNestedProgress, + progress: &mut dyn gix_utils::progress::DynNestedProgress, should_interrupt: &AtomicBool, ) -> Result<(Repository, gix_worktree_state::checkout::Outcome)> { let _span = gix_trace::coarse!("gix::clone::PrepareCheckout::main_worktree()"); diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index 5fd49199ea5..37212505b62 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -116,7 +116,7 @@ fn write_to_local_config(config: &gix_config::File, mode: WriteMode) -> std::io: /// as the `repo` handle was opened before that write and won't observe it until /// it is either updated in memory or reopened. pub fn append_config_to_repo_config(repo: &mut Repository, config: gix_config::File) -> Result { - let repo_config = gix_features::threading::OwnShared::make_mut(&mut repo.config.resolved); + let repo_config = gix_parallel::OwnShared::make_mut(&mut repo.config.resolved); repo_config .append(config) .or_raise(|| message("Failed to append repository configuration")) diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 76b4fc600fe..d75bf48fc82 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -1,5 +1,5 @@ pub use gix_config::*; -use gix_features::threading::OnceCell; +use gix_parallel::OnceCell; use crate::{Repository, repository::identity}; diff --git a/gix/src/config/snapshot/access.rs b/gix/src/config/snapshot/access.rs index 2884572286d..0b505c66976 100644 --- a/gix/src/config/snapshot/access.rs +++ b/gix/src/config/snapshot/access.rs @@ -1,7 +1,7 @@ use std::ffi::OsString; use crate::error::{bail, validation}; -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{ Result, diff --git a/gix/src/interrupt.rs b/gix/src/interrupt.rs index 148bcbdcbd5..469213e581c 100644 --- a/gix/src/interrupt.rs +++ b/gix/src/interrupt.rs @@ -158,7 +158,7 @@ pub use init::{Deregister, init_handler}; /// A wrapper for an inner iterator which will check for interruptions on each iteration. pub struct Iter<I, EFN> { /// The actual iterator to yield elements from. - inner: gix_features::interrupt::IterWithErr<'static, I, EFN>, + inner: gix_utils::interrupt::IterWithErr<'static, I, EFN>, } impl<I, EFN, E> Iter<I, EFN> @@ -174,7 +174,7 @@ where /// [`crate::Error::from_error()`]. pub fn new(inner: I, make_err: EFN) -> Self { Iter { - inner: gix_features::interrupt::IterWithErr::new(inner, make_err, &IS_INTERRUPTED), + inner: gix_utils::interrupt::IterWithErr::new(inner, make_err, &IS_INTERRUPTED), } } @@ -207,7 +207,7 @@ where /// It fails a [read][`std::io::Read::read`] while an interrupt was requested. pub struct Read<R> { /// The actual implementor of [`std::io::Read`] to which interrupt support will be added. - inner: gix_features::interrupt::Read<'static, R>, + inner: gix_utils::interrupt::Read<'static, R>, } impl<R> Read<R> @@ -217,7 +217,7 @@ where /// Create a new interruptible reader from `read`. pub fn new(read: R) -> Self { Read { - inner: gix_features::interrupt::Read { + inner: gix_utils::interrupt::Read { inner: read, should_interrupt: &IS_INTERRUPTED, }, diff --git a/gix/src/lib.rs b/gix/src/lib.rs index cf43680e9ec..9160d8b507c 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -139,13 +139,6 @@ pub use gix_date as date; #[cfg(feature = "dirwalk")] pub use gix_dir as dir; pub use gix_error as error; -pub use gix_features as features; -use gix_features::threading::OwnShared; -pub use gix_features::{ - parallel, - progress::{Count, DynNestedProgress, NestedProgress, Progress}, - threading, -}; pub use gix_fs as fs; pub use gix_glob as glob; pub use gix_hash as hash; @@ -161,6 +154,8 @@ pub use gix_negotiate as negotiate; pub use gix_object as objs; pub use gix_object::bstr; pub use gix_odb as odb; +pub use gix_parallel as parallel; +use gix_parallel::OwnShared; #[cfg(feature = "credentials")] pub use gix_prompt as prompt; pub use gix_protocol as protocol; @@ -176,6 +171,7 @@ pub use gix_url as url; #[doc(inline)] pub use gix_url::Url; pub use gix_utils as utils; +pub use gix_utils::progress::{Count, DynNestedProgress, NestedProgress, Progress}; pub use gix_validate as validate; pub use gix_zlib as zlib; pub use hash::{ObjectId, oid}; diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 93f32e0186f..49164859a7d 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -1,7 +1,7 @@ use gix_config::file::Metadata; use gix_error::{ErrorExt, ResultExt, bail, message, not_found, validation}; -use gix_features::threading::OwnShared; use gix_object::bstr::ByteSlice; +use gix_parallel::OwnShared; use gix_path::RelativePath; use std::path::Path; use std::{ @@ -389,7 +389,7 @@ impl ThreadSafeRepository { *git_dir_trust = gix_sec::Trust::Reduced; } - let Ok(mut resolved) = gix_features::threading::OwnShared::try_unwrap(config.resolved) else { + let Ok(mut resolved) = gix_parallel::OwnShared::try_unwrap(config.resolved) else { unreachable!("Shared ownership was just established, with one reference") }; let section_ids: Vec<_> = resolved.section_ids().collect(); diff --git a/gix/src/prelude.rs b/gix/src/prelude.rs index 03954574a36..e2685270b48 100644 --- a/gix/src/prelude.rs +++ b/gix/src/prelude.rs @@ -1,5 +1,5 @@ -pub use gix_features::parallel::reduce::Finalize; pub use gix_object::{Find, FindExt, Write}; pub use gix_odb::{Header, HeaderExt}; +pub use gix_parallel::Finalize; pub use crate::ext::*; diff --git a/gix/src/progress.rs b/gix/src/progress.rs index 0a88aa0441a..1f97c02e0ba 100644 --- a/gix/src/progress.rs +++ b/gix/src/progress.rs @@ -1,3 +1,3 @@ #[cfg(feature = "progress-tree")] -pub use gix_features::progress::prodash::tree; -pub use gix_features::progress::*; +pub use gix_utils::progress::prodash::tree; +pub use gix_utils::progress::*; diff --git a/gix/src/remote/connection/fetch/receive_pack.rs b/gix/src/remote/connection/fetch/receive_pack.rs index 9d446fbcdfb..296686ce09c 100644 --- a/gix/src/remote/connection/fetch/receive_pack.rs +++ b/gix/src/remote/connection/fetch/receive_pack.rs @@ -74,7 +74,7 @@ where #[gix_protocol::bisync::bisync] pub async fn receive<P>(self, progress: P, should_interrupt: &AtomicBool) -> Result<Outcome> where - P: gix_features::progress::NestedProgress, + P: gix_utils::progress::NestedProgress, P::SubProgress: 'static, { let Prepare { inner, repo } = self; @@ -94,7 +94,7 @@ where should_interrupt: &AtomicBool, ) -> Result<Outcome> where - P: gix_features::progress::NestedProgress, + P: gix_utils::progress::NestedProgress, P::SubProgress: 'static, { let ref_map = &self.ref_map; diff --git a/gix/src/remote/connection/ref_map.rs b/gix/src/remote/connection/ref_map.rs index 8d3fe709b7f..cd5d704a521 100644 --- a/gix/src/remote/connection/ref_map.rs +++ b/gix/src/remote/connection/ref_map.rs @@ -1,9 +1,9 @@ use crate::error::{ResultExt, message}; -use gix_features::progress::Progress; #[cfg(feature = "async-network-client")] use gix_transport::client::async_io::Transport; #[cfg(feature = "blocking-network-client")] use gix_transport::client::blocking_io::Transport; +use gix_utils::progress::Progress; use crate::{ Result, diff --git a/gix/src/remote/url/rewrite.rs b/gix/src/remote/url/rewrite.rs index ec13531acaa..b2c70e174af 100644 --- a/gix/src/remote/url/rewrite.rs +++ b/gix/src/remote/url/rewrite.rs @@ -1,4 +1,4 @@ -use gix_features::threading::OwnShared; +use gix_parallel::OwnShared; use crate::{ bstr::{BStr, BString, ByteVec}, diff --git a/gix/src/repository/branch.rs b/gix/src/repository/branch.rs index a558b999191..f0c0682dfd2 100644 --- a/gix/src/repository/branch.rs +++ b/gix/src/repository/branch.rs @@ -175,7 +175,7 @@ impl crate::Repository { deleted: deleted.clone(), })?; remove_branch_config( - gix_features::threading::OwnShared::make_mut(&mut self.config.resolved), + gix_parallel::OwnShared::make_mut(&mut self.config.resolved), &names, |meta| { meta.source == gix_config::Source::Local diff --git a/gix/src/repository/submodule.rs b/gix/src/repository/submodule.rs index ad0b30c821b..486c334c3ca 100644 --- a/gix/src/repository/submodule.rs +++ b/gix/src/repository/submodule.rs @@ -74,7 +74,7 @@ impl Repository { None => return Ok(None), }, }; - Ok(Some(gix_features::threading::OwnShared::new( + Ok(Some(gix_parallel::OwnShared::new( self.modules_from_bytes(&self.find_object(id)?.data, None)?.into(), ))) } diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index ab3ad68a181..cc52a527f54 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -193,11 +193,11 @@ impl crate::Repository { &self, mut stream: gix_worktree_stream::Stream, out: impl std::io::Write + std::io::Seek, - blobs: impl gix_features::progress::Count, + blobs: impl gix_utils::progress::Count, should_interrupt: &std::sync::atomic::AtomicBool, options: gix_archive::Options, ) -> Result<()> { - let mut out = gix_features::interrupt::Write { + let mut out = gix_utils::interrupt::Write { inner: out, should_interrupt, }; diff --git a/gix/src/shallow.rs b/gix/src/shallow.rs index 34413d54bec..3144bdd3369 100644 --- a/gix/src/shallow.rs +++ b/gix/src/shallow.rs @@ -1,5 +1,5 @@ pub(crate) type CommitsStorage = - gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<nonempty::NonEmpty<gix_hash::ObjectId>>>; + gix_parallel::OwnShared<gix_fs::SharedFileSnapshotMut<nonempty::NonEmpty<gix_hash::ObjectId>>>; /// A lazily loaded and auto-updated list of commits which are at the shallow boundary (behind which there are no commits available), /// sorted to allow bisecting. pub type Commits = gix_fs::SharedFileSnapshot<nonempty::NonEmpty<gix_hash::ObjectId>>; diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 096196c4756..6a37d6885d2 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -77,7 +77,7 @@ impl Repository { >, compare: impl CompareBlobs<Output = T> + Send + Clone, submodule: impl SubmoduleStatus<Output = U> + Send + Clone, - progress: &mut dyn gix_features::progress::Progress, + progress: &mut dyn gix_utils::progress::Progress, should_interrupt: &AtomicBool, options: Options, ) -> Result<gix_status::index_as_worktree_with_renames::Outcome> @@ -539,7 +539,7 @@ pub mod iter { /// Lifecycle impl<Progress> Platform<'_, Progress> where - Progress: gix_features::progress::Progress, + Progress: gix_utils::progress::Progress, { /// Turn the platform into an iterator for changes between the index and the working tree. /// diff --git a/gix/src/status/iter/mod.rs b/gix/src/status/iter/mod.rs index 7339681611d..879fba3d222 100644 --- a/gix/src/status/iter/mod.rs +++ b/gix/src/status/iter/mod.rs @@ -17,7 +17,7 @@ use types::{ApplyChange, Item, Iter, Outcome}; /// Lifecycle impl<Progress> Platform<'_, Progress> where - Progress: gix_features::progress::Progress, + Progress: gix_utils::progress::Progress, { /// Turn the platform into an iterator for changes between the head-tree and the index, and the index and the working tree, /// while optionally listing untracked and/or ignored files. diff --git a/gix/src/status/iter/types.rs b/gix/src/status/iter/types.rs index a4222d7ae9c..730b1f4da63 100644 --- a/gix/src/status/iter/types.rs +++ b/gix/src/status/iter/types.rs @@ -86,7 +86,7 @@ impl Outcome { /// Write the changes if there are any back to the index file. /// This can only be done once as the changes are consumed in the process, if there were any. pub fn write_changes(&mut self) -> Option<Result> { - let _span = gix_features::trace::coarse!("gix::status::index_worktree::Outcome::write_changes()"); + let _span = gix_trace::coarse!("gix::status::index_worktree::Outcome::write_changes()"); let changes = self.changes.take()?; let mut index = match &self.worktree_index { IndexPersistedOrInMemory::Persisted(persisted) => (***persisted).clone(), diff --git a/gix/src/status/mod.rs b/gix/src/status/mod.rs index 6af4ba18a2a..32e6efa5c3f 100644 --- a/gix/src/status/mod.rs +++ b/gix/src/status/mod.rs @@ -5,7 +5,7 @@ use crate::{Repository, Result, config, config::cache::util::ApplyLeniencyDefaul /// A structure to hold options configuring the status request, which can then be turned into an iterator. pub struct Platform<'repo, Progress> where - Progress: gix_features::progress::Progress + 'static, + Progress: gix_utils::progress::Progress + 'static, { repo: &'repo Repository, progress: Progress, @@ -88,7 +88,7 @@ impl Repository { /// ultimately is much faster. pub fn status<P>(&self, progress: P) -> Result<Platform<'_, P>> where - P: gix_features::progress::Progress + 'static, + P: gix_utils::progress::Progress + 'static, { let platform = Platform { repo: self, @@ -168,7 +168,7 @@ pub mod is_dirty { } } let is_dirty = self - .status(gix_features::progress::Discard)? + .status(gix_utils::progress::Discard)? .index_worktree_rewrites(None) .index_worktree_submodules(crate::status::Submodule::AsConfigured { check_dirty: true }) .index_worktree_options_mut(|opts| { diff --git a/gix/src/status/platform.rs b/gix/src/status/platform.rs index 4faa69e48df..fd567b37512 100644 --- a/gix/src/status/platform.rs +++ b/gix/src/status/platform.rs @@ -5,7 +5,7 @@ use crate::status::{OwnedOrStaticAtomicBool, Platform, Submodule, UntrackedFiles /// Builder impl<Progress> Platform<'_, Progress> where - Progress: gix_features::progress::Progress, + Progress: gix_utils::progress::Progress, { /// Call `cb` on dirwalk options if these are set (which is the default when created through [`Repository::status()`](crate::Repository::status())). /// The directory walk is used to find untracked files or ignored files. diff --git a/gix/src/submodule/mod.rs b/gix/src/submodule/mod.rs index 5ead9b33b0a..54b6638ac39 100644 --- a/gix/src/submodule/mod.rs +++ b/gix/src/submodule/mod.rs @@ -15,7 +15,7 @@ use crate::{ worktree::IndexPersistedOrInMemory, }; -pub(crate) type ModulesFileStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<File>>; +pub(crate) type ModulesFileStorage = gix_parallel::OwnShared<gix_fs::SharedFileSnapshotMut<File>>; /// A lazily loaded and auto-updated worktree index. pub type ModulesSnapshot = gix_fs::SharedFileSnapshot<File>; @@ -428,9 +428,9 @@ pub mod status { ignore: config::Ignore, check_dirty: bool, adjust_options: &mut dyn for<'a> FnMut( - crate::status::Platform<'a, gix_features::progress::Discard>, + crate::status::Platform<'a, gix_utils::progress::Discard>, ) - -> crate::status::Platform<'a, gix_features::progress::Discard>, + -> crate::status::Platform<'a, gix_utils::progress::Discard>, ) -> Result<Status> { let mut state = self.state_inner(ignore != config::Ignore::All)?; if ignore == config::Ignore::All { @@ -474,7 +474,7 @@ pub mod status { if !state.worktree_checkout { return Ok(status); } - let statuses = adjust_options(sm_repo.status(gix_features::progress::Discard)?) + let statuses = adjust_options(sm_repo.status(gix_utils::progress::Discard)?) .index_worktree_options_mut(|opts| { if ignore == config::Ignore::Untracked { opts.dirwalk_options = None; diff --git a/gix/src/types.rs b/gix/src/types.rs index 4eccfa90b84..f0f494b04c7 100644 --- a/gix/src/types.rs +++ b/gix/src/types.rs @@ -202,7 +202,7 @@ pub struct ThreadSafeRepository { /// A store for references to point at objects pub refs: crate::RefStore, /// A store for objects that contain data - pub objects: gix_features::threading::OwnShared<gix_odb::Store>, + pub objects: gix_parallel::OwnShared<gix_odb::Store>, /// The path to the worktree at which to find checked out files pub work_tree: Option<PathBuf>, /// The path to the index selected when the repository was opened. diff --git a/gix/src/worktree/add.rs b/gix/src/worktree/add.rs index 9540b4f3367..eccb3e957f0 100644 --- a/gix/src/worktree/add.rs +++ b/gix/src/worktree/add.rs @@ -4,7 +4,7 @@ use std::{ }; use gix_error::{ClassificationMarker, ErrorExt, ResultExt, bail, ensure, message}; -use gix_features::progress::{NestedProgress, Progress}; +use gix_utils::progress::{NestedProgress, Progress}; use crate::{Result, repository::FormatVersion}; use gix_ref::transaction::{LogChange, PreviousValue, RefEdit, RefLog}; diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index cacc96e3469..34a8ae1f63d 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -22,7 +22,7 @@ use crate::{ }; #[cfg(feature = "index")] -pub(crate) type IndexStorage = gix_features::threading::OwnShared<gix_fs::SharedFileSnapshotMut<gix_index::File>>; +pub(crate) type IndexStorage = gix_parallel::OwnShared<gix_fs::SharedFileSnapshotMut<gix_index::File>>; /// A lazily loaded and auto-updated worktree index. #[cfg(feature = "index")] pub type Index = gix_fs::SharedFileSnapshot<gix_index::File>; diff --git a/gix/src/worktree/remove.rs b/gix/src/worktree/remove.rs index fb2a3ed535c..d1903e30194 100644 --- a/gix/src/worktree/remove.rs +++ b/gix/src/worktree/remove.rs @@ -3,8 +3,8 @@ use std::path::Path; use gix_error::{ClassificationMarker, ErrorExt, ResultExt, bail, message}; use crate::{Result, bstr::BString}; -use gix_features::progress::{Count, NestedProgress, Progress}; use gix_path::realpath::MAX_SYMLINKS; +use gix_utils::progress::{Count, NestedProgress, Progress}; pub use gix_worktree::remove::Options; @@ -114,7 +114,7 @@ impl Target<'_> { } = self; let work_dir = proxy.parent.current_dir().join(work_dir); let mut validation = progress.add_child("validate"); - validation.init(Some(1), gix_features::progress::count("worktree")); + validation.init(Some(1), gix_utils::progress::count("worktree")); let git_dir = proxy.parent.current_dir().join(proxy.git_dir()); let ignore_case = proxy.parent.config.ignore_case; diff --git a/gix/tests/gix/remote/fetch.rs b/gix/tests/gix/remote/fetch.rs index 23df49bfffa..fd32ea9e73b 100644 --- a/gix/tests/gix/remote/fetch.rs +++ b/gix/tests/gix/remote/fetch.rs @@ -20,9 +20,9 @@ mod blocking_and_async_io { remote::{Direction::Fetch, fetch, fetch::Status}, }; - use gix_features::progress; use gix_protocol::bisync; use gix_testtools::tempfile::TempDir; + use gix_utils::progress; use crate::{ remote, diff --git a/gix/tests/gix/remote/ref_map.rs b/gix/tests/gix/remote/ref_map.rs index c8951b4912a..b0862cf5103 100644 --- a/gix/tests/gix/remote/ref_map.rs +++ b/gix/tests/gix/remote/ref_map.rs @@ -1,9 +1,9 @@ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))] mod blocking_and_async_io { use gix::{config::tree::Protocol, remote::Direction::Fetch}; - use gix_features::progress; use gix_protocol::bisync; use gix_testtools::TestResult; + use gix_utils::progress; use crate::{ remote, diff --git a/gix/tests/gix/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs index 9ce8e4b987e..e78d8937ece 100644 --- a/gix/tests/gix/repository/worktree.rs +++ b/gix/tests/gix/repository/worktree.rs @@ -1696,7 +1696,7 @@ fn archive() -> TestResult { repo.worktree_archive( stream, std::io::Cursor::new(&mut buf), - gix_features::progress::Discard, + gix_utils::progress::Discard, &std::sync::atomic::AtomicBool::default(), Default::default(), )?; diff --git a/justfile b/justfile index 16304062c3a..dc5ee91df02 100755 --- a/justfile +++ b/justfile @@ -87,7 +87,9 @@ unit-tests: env GIX_TEST_FIXTURE_HASH=sha1 cargo nextest run -p gix-object --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix-object --no-fail-fast cargo nextest run -p gix-tempfile --features signals --no-fail-fast - cargo nextest run -p gix-features --all-features --no-fail-fast + cargo nextest run -p gix-parallel --no-default-features --no-fail-fast + cargo nextest run -p gix-parallel --all-features --no-fail-fast + cargo nextest run -p gix-utils --all-features --no-fail-fast cargo nextest run -p gix-fs --all-features --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha1 cargo nextest run -p gix-ref --all-features --no-fail-fast env GIX_TEST_FIXTURE_HASH=sha256 cargo nextest run -p gix-ref --all-features --no-fail-fast diff --git a/src/shared.rs b/src/shared.rs index 0e60edcd64c..86274b38fc4 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -44,8 +44,8 @@ fn progress_tree() -> LogCreator { pub mod pretty { use std::io::{self, stderr, stdout}; + use gix::progress; use gix::{Result, error::ResultExt}; - use gix_features::progress; use crate::shared::ProgressRange; From 91db7f2f14e86158def6d07d80edfb3264005f94 Mon Sep 17 00:00:00 2001 From: Codex GPT-6 <codex@openai.com> Date: Wed, 7 Oct 2026 22:09:09 +0200 Subject: [PATCH 28/28] Compare disjoint traversal with Git without snapshotting graph layout The `test-fixtures-windows (windows-11-arm)` CI job fails in `disjoint_hidden_and_interesting` with Git 2.56.0: `git log --graph` draws an extra lane before the root of a disconnected branch, so the ASCII graph snapshot differs even though every commit ID and relationship is unchanged. This test and snapshot predate the PR's base. Replace the drawing snapshot with a `git rev-list disjoint --not main` comparison against the same expected commit IDs used by the traversal checks. The test now verifies the hidden-branch behavior against Git without depending on a version-specific graph drawing, and retains coverage for all sortings with and without the commit-graph. Validation: all eight `commit::simple::hide` tests pass with regenerated fixtures. Formatting and `git diff --check` pass. The failing Windows ARM runner's log provides the before-change snapshot difference. --- gix-traverse/tests/traverse/commit/simple/hide.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/gix-traverse/tests/traverse/commit/simple/hide.rs b/gix-traverse/tests/traverse/commit/simple/hide.rs index 434b4b65f6e..40622102e2c 100644 --- a/gix-traverse/tests/traverse/commit/simple/hide.rs +++ b/gix-traverse/tests/traverse/commit/simple/hide.rs @@ -31,15 +31,6 @@ fn assert_simple_repo_graph(repo_dir: &std::path::Path) -> Result { fn disjoint_hidden_and_interesting() -> gix_testtools::TestResult { let (repo_dir, odb) = named_fixture("make_repos.sh", "disjoint_branches")?; - insta::assert_snapshot!(git_graph(&repo_dir)?, @r" - * Oid(1) (HEAD -> disjoint) b3 - * Oid(2) b2 - * Oid(3) b1 - * Oid(4) (main) a3 - * Oid(5) a2 - * Oid(6) a1 - "); - let tip = hex_to_id("e07cf1277ff7c43090f1acfc85a46039e7de1272"); // b3 let hidden = [hex_to_id("b5665181bf4c338ab16b10da0524d81b96aff209")]; // a3 let expected = [ @@ -47,6 +38,11 @@ fn disjoint_hidden_and_interesting() -> gix_testtools::TestResult { hex_to_id("94cf3f3a4c782b672173423e7a4157a02957dd48"), // b2 hex_to_id("34e5ff5ce3d3ba9f0a00d11a7fad72551fff0861"), // b1 ]; + assert_eq!( + git_rev_list(&repo_dir, &["disjoint", "--not", "main"])?, + expected, + "Git confirms that hiding main preserves every commit on the disjoint branch" + ); for sorting in all_sortings() { let result = traverse_both([tip], &odb, sorting, Parents::All, hidden)?;