From ac5ea7bc390413cf3cb79b79833b9b9a504be31d Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:03:25 +0800 Subject: [PATCH 01/11] feat(gix-stash): scaffold crate with push + pop skeletons New plumbing crate placeholder for git-stash workflows. push() / pop() currently return Error::NotImplemented; full implementation follows in subsequent commits. Crate layout mirrors gix-blame: lib.rs re-exports, push/ and pop/ modules each with their own Options/Outcome/Error types. The plumbing takes lower-level handles (index, ODB, ref store, worktree path) rather than gix::Repository so it stays in the plumbing layer; gix porcelain will wrap it as Repository::stash_push / stash_pop. Wired into the workspace and exposed through gix as the gated `stash` feature, matching the blame/worktree-stream pattern. Reference: crate-status.md gix-stash checklist. Co-authored-by: Claude --- Cargo.toml | 3 +- gix-stash/Cargo.toml | 45 +++++++++++++++++++++++++++ gix-stash/src/lib.rs | 39 ++++++++++++++++++++++++ gix-stash/src/pop/mod.rs | 47 ++++++++++++++++++++++++++++ gix-stash/src/push/mod.rs | 64 +++++++++++++++++++++++++++++++++++++++ gix/Cargo.toml | 4 +++ gix/src/lib.rs | 2 ++ 7 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 gix-stash/Cargo.toml create mode 100644 gix-stash/src/lib.rs create mode 100644 gix-stash/src/pop/mod.rs create mode 100644 gix-stash/src/push/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 23147e565e1..a76f2571285 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -296,7 +296,8 @@ members = [ "gix-fsck", "tests/tools", "tests/it", - "gix-shallow" + "gix-shallow", + "gix-stash", ] [workspace.dependencies] diff --git a/gix-stash/Cargo.toml b/gix-stash/Cargo.toml new file mode 100644 index 00000000000..ae86312a434 --- /dev/null +++ b/gix-stash/Cargo.toml @@ -0,0 +1,45 @@ +lints.workspace = true + +[package] +name = "gix-stash" +version = "0.0.0" +repository = "https://github.com/GitoxideLabs/gitoxide" +license = "MIT OR Apache-2.0" +description = "A crate of the gitoxide project providing `git stash` plumbing (push + pop)" +authors = ["Sebastian Thiel "] +edition = "2024" +rust-version = "1.85" +include = ["/src/**/*", "/LICENSE-*"] + +[lib] +doctest = false + +[features] +## Enable support for the SHA-1 hash by forwarding the feature to dependencies. +sha1 = ["gix-hash/sha1", "gix-index/sha1", "gix-object/sha1"] + +[dependencies] +gix-hash = { version = "^0.25.0", path = "../gix-hash" } +gix-object = { version = "^0.60.0", path = "../gix-object" } +gix-index = { version = "^0.51.0", path = "../gix-index" } +gix-ref = { version = "^0.63.0", path = "../gix-ref" } +gix-actor = { version = "^0.41.0", path = "../gix-actor" } +gix-date = { version = "^0.15.3", path = "../gix-date" } +gix-trace = { version = "^0.1.19", path = "../gix-trace" } +gix-features = { version = "^0.48.0", path = "../gix-features", features = ["progress"] } +gix-dir = { version = "^0.25.0", path = "../gix-dir" } +gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } +gix-merge = { version = "^0.16.0", path = "../gix-merge" } +gix-worktree-state = { version = "^0.30.0", path = "../gix-worktree-state" } + +bstr = { version = "1.12.0", default-features = false, features = ["std"] } +smallvec = "1.15.1" +thiserror = "2.0.18" + +[dev-dependencies] +gix-testtools = { path = "../tests/tools" } +gix-hash = { path = "../gix-hash", features = ["sha1"] } +gix-odb = { path = "../gix-odb", features = ["sha1"] } + +[package.metadata.docs.rs] +features = ["sha1"] diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs new file mode 100644 index 00000000000..efb604ab165 --- /dev/null +++ b/gix-stash/src/lib.rs @@ -0,0 +1,39 @@ +//! Plumbing for [`git stash`](https://git-scm.com/docs/git-stash) workflows. +//! +//! This crate implements the `push` and `pop` operations as a starting MVP. +//! Additional operations (`apply`, `drop`, `list`, `show`, `branch`, +//! `autostash`) are tracked in [`crate-status.md`] and may follow. +//! +//! [`crate-status.md`]: https://github.com/GitoxideLabs/gitoxide/blob/main/crate-status.md +//! +//! # Stash representation +//! +//! A stash entry is a merge commit with 2 or 3 parents stored at the single +//! ref `refs/stash`, with the reflog providing the stack of older entries: +//! +//! * `parent[0]` — the commit that `HEAD` pointed at when the stash was made +//! * `parent[1]` — a commit whose tree is the **index** at stash time +//! * `parent[2]` — *(optional, only when `--include-untracked` is used)* — a +//! commit whose tree contains the **untracked** files at stash time +//! +//! The stash commit's own tree is the **working tree** at stash time. +//! +//! # API +//! +//! * [`push`] — capture working tree (+ index, + optional untracked) and reset +//! to `HEAD`. +//! * [`pop`] — apply the latest stash to the working tree (3-way merge) and +//! drop it from `refs/stash`. +//! +//! Both functions operate on plumbing handles (index, ODB, ref store, worktree +//! path) rather than a high-level repository — the porcelain layer in `gix` +//! wraps them and provides `Repository::stash_push` / `Repository::stash_pop`. + +#![deny(missing_docs, rust_2018_idioms)] +#![forbid(unsafe_code)] + +pub mod pop; +pub mod push; + +pub use pop::{Outcome as PopOutcome, function::pop}; +pub use push::{Options as PushOptions, Outcome as PushOutcome, function::push}; diff --git a/gix-stash/src/pop/mod.rs b/gix-stash/src/pop/mod.rs new file mode 100644 index 00000000000..0f04108797a --- /dev/null +++ b/gix-stash/src/pop/mod.rs @@ -0,0 +1,47 @@ +//! Implementation of [`stash pop`](https://git-scm.com/docs/git-stash#Documentation/git-stash.txt-pop). + +use gix_hash::ObjectId; + +/// Result of a successful [`function::pop`]. +#[derive(Debug, Clone)] +pub struct Outcome { + /// The id of the stash commit that was applied + dropped. + pub applied: ObjectId, + + /// The new value of `refs/stash` after dropping the applied entry — `None` + /// when no older stash entries remain. + pub new_top: Option, + + /// Whether the apply step produced merge conflicts. When `true`, the + /// working tree contains conflict markers and `refs/stash` is left + /// untouched (matching `git stash pop` behaviour: only drop on clean + /// apply). + pub had_conflicts: bool, +} + +/// Errors returned by [`function::pop`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("no stash entries to pop (refs/stash is unborn)")] + NoStash, + #[error("not implemented yet")] + NotImplemented, +} + +pub(crate) mod function { + use super::{Error, Outcome}; + + /// Apply the latest stash entry to the working tree (3-way merge against + /// the stash commit's parents) and remove it from `refs/stash`. + /// + /// If the merge produces conflicts, the working tree is left in a + /// conflicted state and the stash is **not** dropped — matching + /// `git stash pop` semantics. + /// + /// Returns [`Outcome`] on success, [`Error::NotImplemented`] until the + /// MVP lands. + pub fn pop() -> Result { + Err(Error::NotImplemented) + } +} diff --git a/gix-stash/src/push/mod.rs b/gix-stash/src/push/mod.rs new file mode 100644 index 00000000000..f59b730f64d --- /dev/null +++ b/gix-stash/src/push/mod.rs @@ -0,0 +1,64 @@ +//! Implementation of [`stash push`](https://git-scm.com/docs/git-stash#Documentation/git-stash.txt-push). + +use bstr::BString; +use gix_hash::ObjectId; + +/// Options controlling [`function::push`]. +#[derive(Debug, Clone, Default)] +pub struct Options { + /// Include untracked (but not git-ignored) files in `parent[2]` of the + /// stash commit and remove them from the working tree. + pub include_untracked: bool, + + /// Also include ignored files when `include_untracked` is set. Has no + /// effect on its own. + pub include_ignored: bool, + + /// Keep the index state intact in the working tree after stashing — the + /// stash still captures it, but the on-disk working tree continues to + /// reflect what was staged. + pub keep_index: bool, + + /// Optional explicit message — written to the stash commit subject and + /// the reflog entry. When `None`, the message defaults to + /// `WIP on : `. + pub message: Option, +} + +/// Result of a successful [`function::push`]. +#[derive(Debug, Clone)] +pub struct Outcome { + /// The id of the newly-created stash commit (now `refs/stash`). + pub stash: ObjectId, + + /// The id of the index-state commit (`parent[1]` of the stash commit). + pub index_commit: ObjectId, + + /// The id of the untracked-files commit (`parent[2]`), if one was created. + pub untracked_commit: Option, + + /// The previous value of `refs/stash`, now reachable only via reflog. + pub previous: Option, +} + +/// Errors returned by [`function::push`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("not implemented yet")] + NotImplemented, +} + +pub(crate) mod function { + use super::{Error, Options, Outcome}; + + /// Capture the current working tree (+ index, + optional untracked) as a + /// new stash commit at `refs/stash`, then reset the working tree and + /// index to match `HEAD`. + /// + /// Returns [`Outcome`] on success, [`Error::NotImplemented`] until the + /// MVP lands. + pub fn push(_options: Options) -> Result { + Err(Error::NotImplemented) + } +} diff --git a/gix/Cargo.toml b/gix/Cargo.toml index c4d95eb9978..2d8a24dc846 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -169,6 +169,9 @@ merge = ["tree-editor", "blob-diff", "dep:gix-merge", "attributes"] ## Add blame command similar to `git blame`. blame = ["dep:gix-blame", "blob-diff"] +## Add stash plumbing (push + pop) similar to `git stash`. +stash = ["dep:gix-stash"] + ## Make it possible to turn a tree into a stream of bytes, which can be decoded to entries and turned into various other formats. worktree-stream = ["gix-worktree-stream", "attributes"] @@ -394,6 +397,7 @@ gix-command = { version = "^0.9.1", path = "../gix-command", optional = true } gix-worktree-stream = { version = "^0.33.0", path = "../gix-worktree-stream", optional = true } gix-archive = { version = "^0.33.0", path = "../gix-archive", default-features = false, optional = true } gix-blame = { version = "^0.14.0", path = "../gix-blame", optional = true } +gix-stash = { version = "^0.0.0", path = "../gix-stash", optional = true } # For communication with remotes gix-protocol = { version = "^0.62.0", path = "../gix-protocol" } diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 18756a81fda..5a47a30e226 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -113,6 +113,8 @@ pub use gix_actor as actor; pub use gix_attributes as attrs; #[cfg(feature = "blame")] pub use gix_blame as blame; +#[cfg(feature = "stash")] +pub use gix_stash as stash; #[cfg(feature = "command")] pub use gix_command as command; pub use gix_commitgraph as commitgraph; From 78ca8d8a9a3f62b08e69b0673c955b8a58586802 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:15:44 +0800 Subject: [PATCH 02/11] feat(gix-stash): add list module skeleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third MVP op — walks refs/stash reflog and returns entries newest-first (matching git stash list output order). Entry carries index, commit oid, reflog message, and Unix timestamp; returns Vec wrapped in Outcome. Empty Outcome when refs/stash is unborn. Co-authored-by: Claude --- gix-stash/src/lib.rs | 8 ++++-- gix-stash/src/list/mod.rs | 55 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 gix-stash/src/list/mod.rs diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs index efb604ab165..d2d7c822d87 100644 --- a/gix-stash/src/lib.rs +++ b/gix-stash/src/lib.rs @@ -24,16 +24,20 @@ //! to `HEAD`. //! * [`pop`] — apply the latest stash to the working tree (3-way merge) and //! drop it from `refs/stash`. +//! * [`list`] — walk the `refs/stash` reflog and return every stash entry. //! -//! Both functions operate on plumbing handles (index, ODB, ref store, worktree +//! All three operate on plumbing handles (index, ODB, ref store, worktree //! path) rather than a high-level repository — the porcelain layer in `gix` -//! wraps them and provides `Repository::stash_push` / `Repository::stash_pop`. +//! wraps them and provides `Repository::stash_push` / `Repository::stash_pop` +//! / `Repository::stash_list`. #![deny(missing_docs, rust_2018_idioms)] #![forbid(unsafe_code)] +pub mod list; pub mod pop; pub mod push; +pub use list::{Entry as ListEntry, Outcome as ListOutcome, function::list}; pub use pop::{Outcome as PopOutcome, function::pop}; pub use push::{Options as PushOptions, Outcome as PushOutcome, function::push}; diff --git a/gix-stash/src/list/mod.rs b/gix-stash/src/list/mod.rs new file mode 100644 index 00000000000..4ffd32217f0 --- /dev/null +++ b/gix-stash/src/list/mod.rs @@ -0,0 +1,55 @@ +//! Implementation of [`stash list`](https://git-scm.com/docs/git-stash#Documentation/git-stash.txt-list). + +use bstr::BString; +use gix_hash::ObjectId; + +/// A single stash entry as found in the `refs/stash` reflog. +/// +/// The newest entry is index `0` (the current value of `refs/stash`); older +/// entries follow in reverse-chronological order, matching what +/// `git stash list` prints (`stash@{0}`, `stash@{1}`, …). +#[derive(Debug, Clone)] +pub struct Entry { + /// Stack position — `0` is the newest. + pub index: usize, + + /// The stash commit's object id. + pub commit: ObjectId, + + /// The reflog message (e.g. `WIP on main: abc1234 commit subject`). + pub message: BString, + + /// Seconds since the Unix epoch, from the reflog committer line. + pub time_seconds: u64, +} + +/// Result of [`function::list`]. +#[derive(Debug, Clone, Default)] +pub struct Outcome { + /// The stash entries, newest first. Empty when `refs/stash` is unborn + /// (no stashes have ever been created in this repo). + pub entries: Vec, +} + +/// Errors returned by [`function::list`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("not implemented yet")] + NotImplemented, +} + +pub(crate) mod function { + use super::{Error, Outcome}; + + /// Walk the reflog of `refs/stash` and return every stash entry, newest + /// first. + /// + /// Returns an empty [`Outcome`] when `refs/stash` is unborn — matching + /// `git stash list` which prints nothing in that case. + /// + /// Returns [`Error::NotImplemented`] until the MVP lands. + pub fn list() -> Result { + Err(Error::NotImplemented) + } +} From 8145e1f1fc90a4b3814e35d08ee636c7c930364d Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:22:29 +0800 Subject: [PATCH 03/11] feat(gix-stash): implement list Co-authored-by: Claude --- gix-stash/Cargo.toml | 1 + gix-stash/src/list/mod.rs | 64 ++++++++++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/gix-stash/Cargo.toml b/gix-stash/Cargo.toml index ae86312a434..5fe8f4f6d0d 100644 --- a/gix-stash/Cargo.toml +++ b/gix-stash/Cargo.toml @@ -31,6 +31,7 @@ gix-dir = { version = "^0.25.0", path = "../gix-dir" } gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } gix-merge = { version = "^0.16.0", path = "../gix-merge" } gix-worktree-state = { version = "^0.30.0", path = "../gix-worktree-state" } +gix-lock = { version = "^23.0.0", path = "../gix-lock" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } smallvec = "1.15.1" diff --git a/gix-stash/src/list/mod.rs b/gix-stash/src/list/mod.rs index 4ffd32217f0..34f97fab4ce 100644 --- a/gix-stash/src/list/mod.rs +++ b/gix-stash/src/list/mod.rs @@ -33,14 +33,22 @@ pub struct Outcome { /// Errors returned by [`function::list`]. #[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] pub enum Error { - #[error("not implemented yet")] - NotImplemented, + /// The reflog file for `refs/stash` could not be read or seeked. + /// + /// This is extracted from [`gix_ref::file::log::Error`] since I/O is + /// the only variant that can occur when a hard-coded, valid ref name is used. + #[error("failed to perform I/O while reading the refs/stash reflog")] + Io(#[from] std::io::Error), + /// An individual reflog line failed to decode. + #[error("failed to decode a reflog line for refs/stash")] + DecodeReflog(#[from] gix_ref::file::log::iter::reverse::Error), } pub(crate) mod function { - use super::{Error, Outcome}; + use gix_ref::FullName; + + use super::{Entry, Error, Outcome}; /// Walk the reflog of `refs/stash` and return every stash entry, newest /// first. @@ -48,8 +56,50 @@ pub(crate) mod function { /// Returns an empty [`Outcome`] when `refs/stash` is unborn — matching /// `git stash list` which prints nothing in that case. /// - /// Returns [`Error::NotImplemented`] until the MVP lands. - pub fn list() -> Result { - Err(Error::NotImplemented) + /// `refs` is the file-based ref store for the repository (typically the + /// `.git/` directory or the common-dir for linked worktrees). + pub fn list(refs: &gix_ref::file::Store) -> Result { + // Parse the well-known ref name once. FullName validates at + // creation time so the expect is safe for this hard-coded literal. + let stash_name: FullName = "refs/stash".try_into().expect("refs/stash is a valid ref name"); + + // 4 KiB sliding window for the reverse-reflog iterator. + let mut buf = vec![0u8; 4 * 1024]; + + let iter = match refs + .reflog_iter_rev(stash_name.as_ref(), &mut buf) + .map_err(|e| match e { + gix_ref::file::log::Error::Io(io) => Error::Io(io), + // Cannot happen: we pass a hard-coded valid ref name. + gix_ref::file::log::Error::RefnameValidation(_) => { + unreachable!("refs/stash is always a valid ref name") + } + })? { + // refs/stash has never been written — no stash entries exist. + None => return Ok(Outcome::default()), + Some(iter) => iter, + }; + + let mut entries: Vec = Vec::new(); + + for line_result in iter { + let line = line_result?; + + let commit = line.new_oid; + let message = line.message.clone(); + // `gix_actor::Signature` carries a parsed `gix_date::Time` field + // whose `seconds` is `i64`; stash entries cannot predate the epoch. + let time_seconds = line.signature.time.seconds.max(0) as u64; + + entries.push(Entry { + // entries are read newest-first, so index 0 = newest. + index: entries.len(), + commit, + message, + time_seconds, + }); + } + + Ok(Outcome { entries }) } } From 5a96ce3c474010671e5a89561d2c3a08c399c4c2 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:31:52 +0800 Subject: [PATCH 04/11] feat(gix-stash): implement push Co-authored-by: Claude --- gix-stash/src/lib.rs | 2 +- gix-stash/src/push/mod.rs | 457 +++++++++++++++++++++++++++++++++++++- gix/src/lib.rs | 4 +- 3 files changed, 449 insertions(+), 14 deletions(-) diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs index d2d7c822d87..7d47f9a2b90 100644 --- a/gix-stash/src/lib.rs +++ b/gix-stash/src/lib.rs @@ -40,4 +40,4 @@ pub mod push; pub use list::{Entry as ListEntry, Outcome as ListOutcome, function::list}; pub use pop::{Outcome as PopOutcome, function::pop}; -pub use push::{Options as PushOptions, Outcome as PushOutcome, function::push}; +pub use push::{Context as PushContext, Options as PushOptions, Outcome as PushOutcome, function::push}; diff --git a/gix-stash/src/push/mod.rs b/gix-stash/src/push/mod.rs index f59b730f64d..f4e1e0444b4 100644 --- a/gix-stash/src/push/mod.rs +++ b/gix-stash/src/push/mod.rs @@ -8,10 +8,19 @@ use gix_hash::ObjectId; pub struct Options { /// Include untracked (but not git-ignored) files in `parent[2]` of the /// stash commit and remove them from the working tree. + /// + /// Note that `.gitignore` rules are **not** consulted in the current + /// implementation — all untracked files are included. A future + /// implementation will wire up `gix-worktree`'s exclude stack to provide + /// full `.gitignore` support. + /// + /// TODO(gix-stash): respect .gitignore via `gix-worktree` exclude stack. pub include_untracked: bool, /// Also include ignored files when `include_untracked` is set. Has no /// effect on its own. + /// + /// Not yet implemented; included for API completeness. pub include_ignored: bool, /// Keep the index state intact in the working tree after stashing — the @@ -43,22 +52,448 @@ pub struct Outcome { /// Errors returned by [`function::push`]. #[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] pub enum Error { - #[error("not implemented yet")] - NotImplemented, + /// The repository has no commits yet — stash requires at least one. + #[error("cannot stash in an empty repository (HEAD has no commits)")] + EmptyRepository, + + /// There are no local changes to stash. + #[error("no local changes to save")] + NoLocalChanges, + + /// An index entry's mode could not be converted to a tree entry mode. + #[error("index entry at path {path:?} has an unrecognised file mode ({mode:#o})")] + InvalidIndexEntryMode { + /// Repository-relative path of the offending entry. + path: BString, + /// The raw mode bits that could not be mapped. + mode: u32, + }, + + /// A tree could not be written to the object database. + #[error("failed to write tree object to the object database")] + WriteTree(#[source] Box), + + /// A blob could not be written to the object database. + #[error("failed to write blob object to the object database")] + WriteBlob(#[source] Box), + + /// A commit could not be written to the object database. + #[error("failed to write commit object to the object database")] + WriteCommit(#[source] Box), + + /// An object could not be found in the database. + #[error("required object was not found in the object database")] + FindObject(#[from] gix_object::find::existing_object::Error), + + /// Reading a worktree file failed while building the untracked-files tree. + #[error("failed to read worktree file at {path:?}")] + ReadFile { + /// The path that failed. + path: std::path::PathBuf, + /// The underlying I/O error. + #[source] + source: std::io::Error, + }, + + /// Walking the worktree for untracked files failed. + #[error("failed to walk the worktree directory")] + WalkWorktree(#[source] std::io::Error), + + /// The tree editor encountered a problem assembling a tree. + #[error("failed to assemble tree from index or worktree entries")] + TreeEditor(#[from] gix_object::tree::editor::Error), + + /// Preparing the ref transaction failed. + #[error("failed to prepare the refs/stash ref transaction")] + PrepareTransaction(#[from] gix_ref::file::transaction::prepare::Error), + + /// Committing the ref transaction failed. + #[error("failed to commit the refs/stash ref transaction")] + CommitTransaction(#[from] gix_ref::file::transaction::commit::Error), +} + +/// Repository-level plumbing handles required by [`function::push`]. +/// +/// Grouping these together avoids crossing the "too many arguments" threshold +/// that clippy enforces. +pub struct Context<'a> { + /// The file-based ref store for the repository. + pub refs: &'a gix_ref::file::Store, + /// A writable ODB handle. + pub odb: &'a dyn gix_object::Write, + /// A readable ODB handle. + pub find: &'a dyn gix_object::FindExt, + /// The current in-memory index state. + pub index: &'a gix_index::State, + /// Absolute path to the working-tree root. + pub worktree: &'a std::path::Path, + /// Identity and timestamp to use for all created commits. + pub committer: gix_actor::SignatureRef<'a>, } pub(crate) mod function { - use super::{Error, Options, Outcome}; + use std::path::Path; + + use bstr::{BStr, BString, ByteSlice}; + use gix_hash::ObjectId; + use gix_object::{Tree, tree::EntryKind}; + use gix_ref::{ + FullName, + transaction::{Change, LogChange, PreviousValue, RefEdit}, + }; + + use super::{Context, Error, Options, Outcome}; + + /// Capture the current working tree (+ index, + optional untracked files) + /// as a new stash commit at `refs/stash`. + /// + /// All plumbing handles are passed via [`Context`]. The remaining + /// parameters are: + /// + /// * `head_commit` — OID of the commit `HEAD` currently points at. + /// * `head_tree` — OID of the root tree of `head_commit`. + /// * `head_branch` — full name of the current branch (e.g. + /// `refs/heads/main`), or `None` when `HEAD` is detached. + /// * `options` — behavioural flags. + /// + /// # Limitations + /// + /// * The **on-disk working tree is not reset** to HEAD after stashing. + /// This requires `gix-worktree-state::checkout`, which in turn needs + /// `gix-filter`, `gix-fs`, and `gix-worktree` types not yet present in + /// `gix-stash`'s dependency graph. Callers that need the full + /// `git stash push` experience must call `gix_worktree_state::checkout` + /// themselves after this function returns. + /// + /// TODO(gix-stash): wire up `gix_worktree_state::checkout`. + /// + /// * The **WIP tree** currently captures the index state rather than the + /// true working-tree state for tracked files. Unstaged modifications + /// are therefore not recorded in the stash commit's tree. + /// + /// TODO(gix-stash): capture working-tree content for modified-but-not-staged files. + /// + /// * `.gitignore` rules are **not consulted** when `include_untracked` is + /// set — all non-tracked, non-`.git` files are included. + /// + /// TODO(gix-stash): wire up `gix-worktree` exclude stack. + pub fn push( + ctx: Context<'_>, + head_commit: ObjectId, + head_tree: ObjectId, + head_branch: Option<&gix_ref::FullNameRef>, + options: Options, + ) -> Result { + let Context { + refs, + odb, + find, + index, + worktree, + committer, + } = ctx; + // ------------------------------------------------------------------ // + // Guard: make sure there is something to stash. + // ------------------------------------------------------------------ // + if index.entries().is_empty() && !options.include_untracked { + return Err(Error::NoLocalChanges); + } + + // ------------------------------------------------------------------ // + // Build common text fragments used in commit messages. + // ------------------------------------------------------------------ // + let head_subject = first_line_of_commit_message(find, head_commit)?; + let short_hash = short_id(head_commit); + let branch_name: BString = head_branch.map_or_else(|| BString::from("HEAD"), |n| n.shorten().to_owned()); + + // ------------------------------------------------------------------ // + // parent[1] — "index on : …" + // ------------------------------------------------------------------ // + let index_tree_oid = write_tree_from_index(index, find, odb, head_tree)?; + + let index_msg = format!( + "index on {branch}: {short} {subj}", + branch = branch_name.as_bstr(), + short = short_hash.as_bstr(), + subj = head_subject.as_bstr(), + ); + let index_commit_oid = write_commit( + odb, + index_tree_oid, + &[head_commit], + committer, + index_msg.as_bytes().as_bstr(), + )?; + + // ------------------------------------------------------------------ // + // parent[2] — untracked files commit (optional). + // ------------------------------------------------------------------ // + let untracked_commit_oid = if options.include_untracked { + let empty_tree = ObjectId::empty_tree(head_commit.kind()); + let untracked_tree = write_untracked_tree(find, odb, worktree, index)?; + if untracked_tree != empty_tree { + let msg = format!( + "untracked files on {branch}: {short} {subj}", + branch = branch_name.as_bstr(), + short = short_hash.as_bstr(), + subj = head_subject.as_bstr(), + ); + Some(write_commit( + odb, + untracked_tree, + &[], + committer, + msg.as_bytes().as_bstr(), + )?) + } else { + None + } + } else { + None + }; + + // ------------------------------------------------------------------ // + // Stash commit — WIP (uses index tree as proxy for the WT tree). + // ------------------------------------------------------------------ // + let stash_msg: BString = options.message.clone().unwrap_or_else(|| { + format!( + "WIP on {branch}: {short} {subj}", + branch = branch_name.as_bstr(), + short = short_hash.as_bstr(), + subj = head_subject.as_bstr(), + ) + .into() + }); + + let mut stash_parents: Vec = vec![head_commit, index_commit_oid]; + if let Some(u) = untracked_commit_oid { + stash_parents.push(u); + } + let stash_oid = write_commit(odb, index_tree_oid, &stash_parents, committer, stash_msg.as_bstr())?; - /// Capture the current working tree (+ index, + optional untracked) as a - /// new stash commit at `refs/stash`, then reset the working tree and - /// index to match `HEAD`. + // ------------------------------------------------------------------ // + // Update refs/stash via transaction. + // ------------------------------------------------------------------ // + let stash_ref_name: FullName = "refs/stash".try_into().expect("refs/stash is a valid ref name"); + + let previous = refs + .try_find(stash_ref_name.as_ref()) + .ok() + .flatten() + .and_then(|r| r.target.try_id().map(ToOwned::to_owned)); + + let expected = match &previous { + Some(prev_oid) => PreviousValue::ExistingMustMatch(gix_ref::Target::Object(*prev_oid)), + None => PreviousValue::Any, + }; + + let edit = RefEdit { + change: Change::Update { + log: LogChange { + mode: gix_ref::transaction::RefLog::AndReference, + force_create_reflog: true, + message: stash_msg.clone(), + }, + expected, + new: gix_ref::Target::Object(stash_oid), + }, + name: stash_ref_name, + deref: false, + }; + + let committer_owned: gix_actor::Signature = committer.into(); + let mut time_buf = gix_date::parse::TimeBuf::default(); + refs.transaction() + .prepare( + std::iter::once(edit), + gix_lock::acquire::Fail::Immediately, + gix_lock::acquire::Fail::Immediately, + )? + .commit(committer_owned.to_ref(&mut time_buf))?; + + Ok(Outcome { + stash: stash_oid, + index_commit: index_commit_oid, + untracked_commit: untracked_commit_oid, + previous, + }) + } + + // ======================================================================= // + // Private helpers + // ======================================================================= // + + /// Build a tree mirroring the current index state and write it to the ODB. + fn write_tree_from_index( + index: &gix_index::State, + find: &dyn gix_object::FindExt, + odb: &dyn gix_object::Write, + head_tree: ObjectId, + ) -> Result { + let object_hash = index.object_hash(); + + // Seed the editor with HEAD's root tree so existing sub-tree objects + // can be reused without being re-fetched. + let mut buf = Vec::new(); + let root_tree = find.find_tree(&head_tree, &mut buf)?.to_owned(); + let mut editor = gix_object::tree::Editor::new(root_tree, find, object_hash); + + let paths = index.path_backing(); + for entry in index.entries() { + // Skip sparse-checkout directory markers. + if entry.mode.is_sparse() { + continue; + } + let path = entry.path_in(paths); + let entry_kind = entry + .mode + .to_tree_entry_mode() + .ok_or_else(|| Error::InvalidIndexEntryMode { + path: path.to_owned(), + mode: entry.mode.bits(), + })? + .kind(); + + // Split the path on `/` to feed into the tree editor. + let components: Vec<&BStr> = path.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); + editor.upsert(components, entry_kind, entry.id)?; + } + + editor.write(|tree| odb.write(tree).map_err(Error::WriteTree)) + } + + /// Walk the worktree recursively for files not in `index`, write them as + /// blobs, and assemble them into a tree. + /// + /// Uses `std::fs::read_dir` rather than `gix-dir` to avoid pulling in + /// `gix-pathspec` / `gix-worktree` as direct dependencies. `.gitignore` + /// rules are **not** respected. /// - /// Returns [`Outcome`] on success, [`Error::NotImplemented`] until the - /// MVP lands. - pub fn push(_options: Options) -> Result { - Err(Error::NotImplemented) + /// TODO(gix-stash): consult `.gitignore` via the `gix-worktree` exclude stack. + fn write_untracked_tree( + find: &dyn gix_object::FindExt, + odb: &dyn gix_object::Write, + worktree: &Path, + index: &gix_index::State, + ) -> Result { + let object_hash = index.object_hash(); + let mut editor = gix_object::tree::Editor::new(Tree::empty(), find, object_hash); + + let paths_storage = index.path_backing(); + let tracked: std::collections::BTreeSet = index + .entries() + .iter() + .map(|e| e.path_in(paths_storage).to_owned()) + .collect(); + + collect_untracked(worktree, worktree, &tracked, odb, &mut editor)?; + editor.write(|tree| odb.write(tree).map_err(Error::WriteTree)) + } + + /// Recursively walk `dir` and add untracked files to `editor`. + fn collect_untracked( + worktree: &Path, + dir: &Path, + tracked: &std::collections::BTreeSet, + odb: &dyn gix_object::Write, + editor: &mut gix_object::tree::Editor<'_>, + ) -> Result<(), Error> { + let read_dir = std::fs::read_dir(dir).map_err(Error::WalkWorktree)?; + + for dir_entry_result in read_dir { + let dir_entry = dir_entry_result.map_err(Error::WalkWorktree)?; + let name = dir_entry.file_name(); + let name_bytes = name.as_encoded_bytes(); + + // Never recurse into .git. + if name_bytes == b".git" { + continue; + } + + let abs_path = dir_entry.path(); + let file_type = dir_entry.file_type().map_err(|e| Error::ReadFile { + path: abs_path.clone(), + source: e, + })?; + + if file_type.is_dir() { + collect_untracked(worktree, &abs_path, tracked, odb, editor)?; + } else if file_type.is_file() || file_type.is_symlink() { + let rela = rela_path(worktree, &abs_path); + if tracked.contains(&rela) { + continue; + } + + let content = std::fs::read(&abs_path).map_err(|e| Error::ReadFile { + path: abs_path.clone(), + source: e, + })?; + let blob_oid = odb + .write_buf(gix_object::Kind::Blob, &content) + .map_err(Error::WriteBlob)?; + + let kind = if file_type.is_symlink() { + EntryKind::Link + } else { + EntryKind::Blob + }; + + let rela_bstr: &BStr = rela.as_bstr(); + let components: Vec<&BStr> = rela_bstr.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); + editor.upsert(components, kind, blob_oid)?; + } + // Special files (sockets, devices, pipes) are silently skipped. + } + Ok(()) + } + + /// Compute a `/`-separated path relative to `worktree`. + fn rela_path(worktree: &Path, abs: &Path) -> BString { + let rel = abs + .strip_prefix(worktree) + .unwrap_or(abs) + .components() + .filter_map(|c| match c { + std::path::Component::Normal(s) => Some(s.as_encoded_bytes().to_vec()), + _ => None, + }) + .collect::>() + .join(b"/" as &[u8]); + BString::from(rel) + } + + /// Write a commit object to the ODB and return its OID. + fn write_commit( + odb: &dyn gix_object::Write, + tree: ObjectId, + parents: &[ObjectId], + committer: gix_actor::SignatureRef<'_>, + message: &BStr, + ) -> Result { + let sig: gix_actor::Signature = committer.into(); + let commit = gix_object::Commit { + tree, + parents: parents.iter().copied().collect(), + author: sig.clone(), + committer: sig, + encoding: None, + message: message.to_owned(), + extra_headers: Vec::new(), + }; + odb.write(&commit).map_err(Error::WriteCommit) + } + + /// Return the first line (subject) of a commit's message. + fn first_line_of_commit_message(find: &dyn gix_object::FindExt, commit_oid: ObjectId) -> Result { + let mut buf = Vec::new(); + let commit = find.find_commit(&commit_oid, &mut buf)?; + Ok(commit.message.lines().next().unwrap_or(b"").as_bstr().to_owned()) + } + + /// Return a 7-character hex prefix of the given OID. + fn short_id(oid: ObjectId) -> BString { + let s = oid.to_hex().to_string(); + BString::from(&s.as_bytes()[..7.min(s.len())]) } } diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 5a47a30e226..41b3866b4ff 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -113,8 +113,6 @@ pub use gix_actor as actor; pub use gix_attributes as attrs; #[cfg(feature = "blame")] pub use gix_blame as blame; -#[cfg(feature = "stash")] -pub use gix_stash as stash; #[cfg(feature = "command")] pub use gix_command as command; pub use gix_commitgraph as commitgraph; @@ -153,6 +151,8 @@ pub use gix_ref as refs; pub use gix_refspec as refspec; pub use gix_revwalk as revwalk; pub use gix_sec as sec; +#[cfg(feature = "stash")] +pub use gix_stash as stash; pub use gix_tempfile as tempfile; pub use gix_trace as trace; pub use gix_traverse as traverse; From 80d3305961e55b544089d802cdae8cfa0a92e6f4 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:35:54 +0800 Subject: [PATCH 05/11] feat(gix-stash): implement pop Co-authored-by: Claude --- gix-stash/src/lib.rs | 2 +- gix-stash/src/pop/mod.rs | 188 +++++++++++++++++++++++++++++++++++---- 2 files changed, 172 insertions(+), 18 deletions(-) diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs index 7d47f9a2b90..d3ac7d66c7a 100644 --- a/gix-stash/src/lib.rs +++ b/gix-stash/src/lib.rs @@ -39,5 +39,5 @@ pub mod pop; pub mod push; pub use list::{Entry as ListEntry, Outcome as ListOutcome, function::list}; -pub use pop::{Outcome as PopOutcome, function::pop}; +pub use pop::{Context as PopContext, Outcome as PopOutcome, function::pop}; pub use push::{Context as PushContext, Options as PushOptions, Outcome as PushOutcome, function::push}; diff --git a/gix-stash/src/pop/mod.rs b/gix-stash/src/pop/mod.rs index 0f04108797a..3de07835e99 100644 --- a/gix-stash/src/pop/mod.rs +++ b/gix-stash/src/pop/mod.rs @@ -8,40 +8,194 @@ pub struct Outcome { /// The id of the stash commit that was applied + dropped. pub applied: ObjectId, + /// The tree recorded in the stash commit — the working-tree state at stash + /// time. Callers **must** apply this tree to the index and on-disk working + /// tree themselves; `pop` cannot do so without `gix-filter` / `gix-worktree` + /// infrastructure that is not yet wired into `gix-stash`. + /// + /// TODO(gix-stash): perform the worktree application here once + /// `gix_worktree_state::checkout` dependencies are in scope. + pub stash_tree: ObjectId, + + /// OID of parent[0] of the stash commit — the commit `HEAD` pointed at + /// when the stash was created. Callers can use this together with + /// [`stash_tree`](Outcome::stash_tree) to run a 3-way merge if desired. + /// + /// TODO(gix-stash): call `gix_merge::tree` here once `gix_diff::blob::Platform` + /// and `gix_merge::blob::Platform` are available without adding `gix-filter` / + /// `gix-worktree` as direct dependencies. + pub base_commit: ObjectId, + /// The new value of `refs/stash` after dropping the applied entry — `None` /// when no older stash entries remain. pub new_top: Option, - /// Whether the apply step produced merge conflicts. When `true`, the - /// working tree contains conflict markers and `refs/stash` is left - /// untouched (matching `git stash pop` behaviour: only drop on clean - /// apply). + /// Whether the apply step produced merge conflicts. + /// + /// Always `false` in the current implementation — a real 3-way merge is not + /// yet performed (see [`stash_tree`](Outcome::stash_tree) / [`base_commit`](Outcome::base_commit)). pub had_conflicts: bool, } /// Errors returned by [`function::pop`]. #[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] pub enum Error { + /// `refs/stash` is unborn — no stash entries exist. #[error("no stash entries to pop (refs/stash is unborn)")] NoStash, - #[error("not implemented yet")] - NotImplemented, + + /// Looking up `refs/stash` in the ref store failed. + #[error("failed to read refs/stash from the ref store")] + FindRef(#[from] gix_ref::file::find::Error), + + /// An object could not be found in the database. + #[error("required object was not found in the object database")] + FindObject(#[from] gix_object::find::existing_object::Error), + + /// The reflog for `refs/stash` could not be read. + #[error("failed to read the refs/stash reflog")] + Io(#[from] std::io::Error), + + /// A reflog line for `refs/stash` failed to decode. + #[error("failed to decode a reflog line for refs/stash")] + DecodeReflog(#[from] gix_ref::file::log::iter::reverse::Error), + + /// Preparing the ref transaction failed. + #[error("failed to prepare the refs/stash ref transaction")] + PrepareTransaction(#[from] gix_ref::file::transaction::prepare::Error), + + /// Committing the ref transaction failed. + #[error("failed to commit the refs/stash ref transaction")] + CommitTransaction(#[from] gix_ref::file::transaction::commit::Error), +} + +/// Repository-level plumbing handles required by [`function::pop`]. +pub struct Context<'a> { + /// The file-based ref store for the repository. + pub refs: &'a gix_ref::file::Store, + /// A readable ODB handle — used to decode the stash commit. + pub find: &'a dyn gix_object::FindExt, + /// Identity and timestamp to use for the ref-transaction committer line. + pub committer: gix_actor::SignatureRef<'a>, } pub(crate) mod function { - use super::{Error, Outcome}; + use gix_hash::ObjectId; + use gix_ref::{ + FullName, + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, + }; - /// Apply the latest stash entry to the working tree (3-way merge against - /// the stash commit's parents) and remove it from `refs/stash`. + use super::{Context, Error, Outcome}; + + /// Drop the latest stash entry from `refs/stash` and return the stash + /// commit's OID plus the tree it carries. + /// + /// # Limitations /// - /// If the merge produces conflicts, the working tree is left in a - /// conflicted state and the stash is **not** dropped — matching - /// `git stash pop` semantics. + /// * The stash is **not merged into the working tree** — the caller must + /// apply [`Outcome::stash_tree`] themselves (e.g. via + /// `gix_worktree_state::checkout`). This mirrors the limitation in + /// [`push`](crate::push()) where the worktree reset is also deferred to + /// the caller. /// - /// Returns [`Outcome`] on success, [`Error::NotImplemented`] until the - /// MVP lands. - pub fn pop() -> Result { - Err(Error::NotImplemented) + /// TODO(gix-stash): perform 3-way merge + worktree update in-crate once + /// `gix_diff::blob::Platform` and `gix_merge::blob::Platform` can be + /// constructed without `gix-filter` / `gix-worktree` direct deps. + /// + /// * [`Outcome::had_conflicts`] is always `false` until the merge is wired in. + pub fn pop(ctx: Context<'_>) -> Result { + let Context { refs, find, committer } = ctx; + + let stash_ref: FullName = "refs/stash".try_into().expect("refs/stash is a valid ref name"); + + // ------------------------------------------------------------------ // + // Read the current tip of refs/stash. + // ------------------------------------------------------------------ // + let stash_oid = refs + .try_find(stash_ref.as_ref())? + .ok_or(Error::NoStash)? + .target + .try_id() + .map(ToOwned::to_owned) + .ok_or(Error::NoStash)?; + + // ------------------------------------------------------------------ // + // Decode the stash commit to extract tree + base commit (parent[0]). + // ------------------------------------------------------------------ // + let mut commit_buf = Vec::new(); + let stash_commit = find.find_commit(&stash_oid, &mut commit_buf)?; + + let stash_tree = stash_commit.tree(); + + // parent[0] is the original HEAD at stash time. + let base_commit = stash_commit.parents().next().ok_or(Error::NoStash)?; // malformed stash; treat as absent + + // ------------------------------------------------------------------ // + // Look up the second-newest entry so we know what to set refs/stash to + // after dropping the top. We read the reflog in reverse (newest first); + // index 0 = current tip (what we are popping), index 1 = the one before. + // ------------------------------------------------------------------ // + let mut reflog_buf = vec![0u8; 4 * 1024]; + let new_top: Option = { + let mut iter = refs + .reflog_iter_rev(stash_ref.as_ref(), &mut reflog_buf) + .map_err(|e| match e { + gix_ref::file::log::Error::Io(io) => Error::Io(io), + gix_ref::file::log::Error::RefnameValidation(_) => { + unreachable!("refs/stash is always a valid ref name") + } + })? + .ok_or(Error::NoStash)?; + + // Index 0 = current tip (what we are popping); index 1 = the one before. + iter.nth(1).transpose()?.map(|line| line.new_oid) + }; + + // ------------------------------------------------------------------ // + // Drop the stash: update refs/stash to `new_top`, or delete it entirely. + // ------------------------------------------------------------------ // + let edit = if let Some(next_oid) = new_top { + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: true, + message: "drop stash".into(), + }, + expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), + new: gix_ref::Target::Object(next_oid), + }, + name: stash_ref, + deref: false, + } + } else { + RefEdit { + change: Change::Delete { + expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), + log: RefLog::AndReference, + }, + name: stash_ref, + deref: false, + } + }; + + let committer_owned: gix_actor::Signature = committer.into(); + let mut time_buf = gix_date::parse::TimeBuf::default(); + refs.transaction() + .prepare( + std::iter::once(edit), + gix_lock::acquire::Fail::Immediately, + gix_lock::acquire::Fail::Immediately, + )? + .commit(committer_owned.to_ref(&mut time_buf))?; + + Ok(Outcome { + applied: stash_oid, + stash_tree, + base_commit, + new_top, + had_conflicts: false, + }) } } From 96a9880c6d9a5735e4a561ed9f78033b3d363396 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 22:59:34 +0800 Subject: [PATCH 06/11] fix(gix-stash): complete push + pop semantics (real merge + WT reset) Push: build WIP tree from actual working-tree file content (not index OIDs) so unstaged modifications are captured; reset WT to HEAD after stash commit via gix_worktree_state::checkout; remove untracked files from disk when include_untracked is set. Pop: perform 3-way merge (base=stash parent[0] tree, ours=current HEAD tree, theirs=stash WIP tree) via gix_merge::tree; write merged result to WT; restore untracked files from parent[2] on clean merge; leave refs/stash intact on conflict. Context structs are now generic over Objects (Find + FindHeader + Write + Send + Clone) matching gix-blame/gix-merge conventions. Remove stash_tree and base_commit from pop::Outcome; pop now does the merge itself. Add gix-validate and gix-path to direct deps. Co-authored-by: Claude --- gix-stash/Cargo.toml | 2 + gix-stash/src/lib.rs | 6 +- gix-stash/src/pop/mod.rs | 351 +++++++++++++++++++++++++++++--------- gix-stash/src/push/mod.rs | 269 ++++++++++++++++++++++------- 4 files changed, 487 insertions(+), 141 deletions(-) diff --git a/gix-stash/Cargo.toml b/gix-stash/Cargo.toml index 5fe8f4f6d0d..152beffdae8 100644 --- a/gix-stash/Cargo.toml +++ b/gix-stash/Cargo.toml @@ -27,6 +27,8 @@ gix-actor = { version = "^0.41.0", path = "../gix-actor" } gix-date = { version = "^0.15.3", path = "../gix-date" } gix-trace = { version = "^0.1.19", path = "../gix-trace" } gix-features = { version = "^0.48.0", path = "../gix-features", features = ["progress"] } +gix-path = { version = "^0.12.0", path = "../gix-path" } +gix-validate = { version = "^0.11.1", path = "../gix-validate" } gix-dir = { version = "^0.25.0", path = "../gix-dir" } gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } gix-merge = { version = "^0.16.0", path = "../gix-merge" } diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs index d3ac7d66c7a..d44dcee1039 100644 --- a/gix-stash/src/lib.rs +++ b/gix-stash/src/lib.rs @@ -39,5 +39,7 @@ pub mod pop; pub mod push; pub use list::{Entry as ListEntry, Outcome as ListOutcome, function::list}; -pub use pop::{Context as PopContext, Outcome as PopOutcome, function::pop}; -pub use push::{Context as PushContext, Options as PushOptions, Outcome as PushOutcome, function::push}; +pub use pop::{Context as PopContext, Error as PopError, Outcome as PopOutcome, function::pop}; +pub use push::{ + Context as PushContext, Error as PushError, Options as PushOptions, Outcome as PushOutcome, function::push, +}; diff --git a/gix-stash/src/pop/mod.rs b/gix-stash/src/pop/mod.rs index 3de07835e99..906f375eda6 100644 --- a/gix-stash/src/pop/mod.rs +++ b/gix-stash/src/pop/mod.rs @@ -8,32 +8,16 @@ pub struct Outcome { /// The id of the stash commit that was applied + dropped. pub applied: ObjectId, - /// The tree recorded in the stash commit — the working-tree state at stash - /// time. Callers **must** apply this tree to the index and on-disk working - /// tree themselves; `pop` cannot do so without `gix-filter` / `gix-worktree` - /// infrastructure that is not yet wired into `gix-stash`. - /// - /// TODO(gix-stash): perform the worktree application here once - /// `gix_worktree_state::checkout` dependencies are in scope. - pub stash_tree: ObjectId, - - /// OID of parent[0] of the stash commit — the commit `HEAD` pointed at - /// when the stash was created. Callers can use this together with - /// [`stash_tree`](Outcome::stash_tree) to run a 3-way merge if desired. - /// - /// TODO(gix-stash): call `gix_merge::tree` here once `gix_diff::blob::Platform` - /// and `gix_merge::blob::Platform` are available without adding `gix-filter` / - /// `gix-worktree` as direct dependencies. - pub base_commit: ObjectId, - /// The new value of `refs/stash` after dropping the applied entry — `None` /// when no older stash entries remain. pub new_top: Option, /// Whether the apply step produced merge conflicts. /// - /// Always `false` in the current implementation — a real 3-way merge is not - /// yet performed (see [`stash_tree`](Outcome::stash_tree) / [`base_commit`](Outcome::base_commit)). + /// When `true` the merged result (including conflict markers) has been + /// written to the working tree, but `refs/stash` has **not** been dropped + /// so that the stash entry can be re-applied after manual resolution; + /// [`Outcome::had_conflicts`] is set to `true`. pub had_conflicts: bool, } @@ -67,20 +51,62 @@ pub enum Error { /// Committing the ref transaction failed. #[error("failed to commit the refs/stash ref transaction")] CommitTransaction(#[from] gix_ref::file::transaction::commit::Error), + + /// The 3-way tree merge failed. + #[error("failed to merge stash tree into the working tree")] + Merge(#[from] gix_merge::tree::Error), + + /// Writing the merged tree to the object database failed. + #[error("failed to write merged tree to the object database")] + WriteTree(#[source] Box), + + /// Constructing the merge-result index for the worktree checkout failed. + #[error("failed to construct index from merged tree")] + IndexFromTree(#[from] gix_index::init::from_tree::Error), + + /// Writing the merge result to the working tree failed. + #[error("failed to write merge result to the working tree")] + Checkout(#[from] gix_worktree_state::checkout::Error), + + /// Reading a blob to restore an untracked file failed. + #[error("failed to read untracked blob for restore at {path:?}")] + RestoreUntracked { + /// The path where the untracked file was to be written. + path: std::path::PathBuf, + /// The underlying I/O error. + #[source] + source: std::io::Error, + }, } /// Repository-level plumbing handles required by [`function::pop`]. -pub struct Context<'a> { +/// +/// `Objects` must implement [`gix_object::Find`], [`gix_object::FindHeader`], +/// and [`gix_object::Write`] — all of which are satisfied by the typical +/// `gix_odb::Handle` / `gix::Repository` object store. +pub struct Context<'a, Objects> { /// The file-based ref store for the repository. pub refs: &'a gix_ref::file::Store, - /// A readable ODB handle — used to decode the stash commit. - pub find: &'a dyn gix_object::FindExt, + /// A combined readable + writable ODB handle. + pub objects: &'a Objects, /// Identity and timestamp to use for the ref-transaction committer line. pub committer: gix_actor::SignatureRef<'a>, + /// Absolute path to the working-tree root. + pub worktree: &'a std::path::Path, + /// Pre-configured blob merge platform for 3-way content merges. + pub blob_merge: &'a mut gix_merge::blob::Platform, + /// Pre-configured diff resource cache for rename tracking during tree merge. + pub diff_cache: &'a mut gix_diff::blob::Platform, + /// Options controlling the worktree checkout after a successful merge. + pub checkout_options: gix_worktree_state::checkout::Options, } pub(crate) mod function { + use std::path::Path; + + use bstr::ByteSlice; use gix_hash::ObjectId; + use gix_object::FindExt; use gix_ref::{ FullName, transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, @@ -88,24 +114,45 @@ pub(crate) mod function { use super::{Context, Error, Outcome}; - /// Drop the latest stash entry from `refs/stash` and return the stash - /// commit's OID plus the tree it carries. + /// Apply the latest stash entry to the working tree and drop it from + /// `refs/stash`. + /// + /// # Parameters /// - /// # Limitations + /// * `head_tree` — OID of the root tree of the current `HEAD` commit. + /// This is the "ours" side of the 3-way merge. /// - /// * The stash is **not merged into the working tree** — the caller must - /// apply [`Outcome::stash_tree`] themselves (e.g. via - /// `gix_worktree_state::checkout`). This mirrors the limitation in - /// [`push`](crate::push()) where the worktree reset is also deferred to - /// the caller. + /// # Merge semantics /// - /// TODO(gix-stash): perform 3-way merge + worktree update in-crate once - /// `gix_diff::blob::Platform` and `gix_merge::blob::Platform` can be - /// constructed without `gix-filter` / `gix-worktree` direct deps. + /// Performs a 3-way merge of: + /// * **base** — the tree of `parent[0]` of the stash commit (HEAD at stash + /// time) + /// * **ours** — `head_tree` (current HEAD tree) + /// * **theirs** — the stash commit's own tree (working-tree state at stash + /// time) /// - /// * [`Outcome::had_conflicts`] is always `false` until the merge is wired in. - pub fn pop(ctx: Context<'_>) -> Result { - let Context { refs, find, committer } = ctx; + /// If the merge is clean the result is checked out into the working tree + /// and `refs/stash` is dropped. On conflict, the working tree receives + /// conflict markers and `refs/stash` is left in place so the entry can be + /// re-applied after manual resolution; [`Outcome::had_conflicts`] is set + /// to `true` in that case. + /// + /// If the stash commit has a third parent (`parent[2]`), its tree is + /// treated as the untracked-files snapshot and those files are restored to + /// the working tree after a clean merge. + pub fn pop(ctx: Context<'_, Objects>, head_tree: ObjectId) -> Result + where + Objects: gix_object::Find + gix_object::FindHeader + gix_object::Write + Send + Clone, + { + let Context { + refs, + objects, + committer, + worktree, + blob_merge, + diff_cache, + checkout_options, + } = ctx; let stash_ref: FullName = "refs/stash".try_into().expect("refs/stash is a valid ref name"); @@ -121,20 +168,92 @@ pub(crate) mod function { .ok_or(Error::NoStash)?; // ------------------------------------------------------------------ // - // Decode the stash commit to extract tree + base commit (parent[0]). + // Decode the stash commit. // ------------------------------------------------------------------ // let mut commit_buf = Vec::new(); - let stash_commit = find.find_commit(&stash_oid, &mut commit_buf)?; + let stash_commit = objects.find_commit(&stash_oid, &mut commit_buf)?; + // The stash commit's own tree is the WIP working-tree state. let stash_tree = stash_commit.tree(); - // parent[0] is the original HEAD at stash time. - let base_commit = stash_commit.parents().next().ok_or(Error::NoStash)?; // malformed stash; treat as absent + // parent[0] is the original HEAD at stash time — the merge base. + let base_commit = stash_commit.parents().next().ok_or(Error::NoStash)?; + + // parent[2] (optional) is the untracked-files commit. + let untracked_commit: Option = stash_commit.parents().nth(2); + + drop(stash_commit); + + // Resolve the base tree from parent[0]. + let mut base_buf = Vec::new(); + let base_commit_obj = objects.find_commit(&base_commit, &mut base_buf)?; + let base_tree = base_commit_obj.tree(); + drop(base_commit_obj); + + // ------------------------------------------------------------------ // + // 3-way tree merge: + // base = HEAD tree at stash time (stash parent[0]'s tree) + // ours = current HEAD tree + // theirs = stash WIP tree + // ------------------------------------------------------------------ // + let mut diff_state = gix_diff::tree::State::default(); + let labels = gix_merge::blob::builtin_driver::text::Labels::default(); + + let merge_outcome = gix_merge::tree( + &base_tree, + &head_tree, + &stash_tree, + labels, + objects, + |buf: &[u8]| objects.write_buf(gix_object::Kind::Blob, buf), + &mut diff_state, + diff_cache, + blob_merge, + gix_merge::tree::Options::default(), + )?; + + let had_conflicts = merge_outcome.has_unresolved_conflicts(gix_merge::tree::TreatAsUnresolved::git()); + + // Write the merged tree to the ODB. + // `tree` is an Editor; we write it by consuming it via the write() method. + let mut merge_tree_editor = merge_outcome.tree; + let merged_tree_oid = merge_tree_editor.write(|tree| objects.write(tree).map_err(Error::WriteTree))?; + + // ------------------------------------------------------------------ // + // Checkout merged tree into the working tree. + // ------------------------------------------------------------------ // + let mut merged_index = gix_index::State::from_tree( + &merged_tree_oid, + objects, + gix_validate::path::component::Options::default(), + )?; + let should_interrupt = std::sync::atomic::AtomicBool::new(false); + gix_worktree_state::checkout( + &mut merged_index, + worktree, + objects.clone(), + &gix_features::progress::Discard, + &gix_features::progress::Discard, + &should_interrupt, + checkout_options, + )?; + + // ------------------------------------------------------------------ // + // Restore untracked files if the stash had a parent[2] and merge clean. + // ------------------------------------------------------------------ // + if !had_conflicts { + if let Some(untracked_commit_oid) = untracked_commit { + let mut uc_buf = Vec::new(); + let uc_commit = objects.find_commit(&untracked_commit_oid, &mut uc_buf)?; + let untracked_tree_oid = uc_commit.tree(); + drop(uc_commit); + restore_tree_to_worktree(&untracked_tree_oid, worktree, objects)?; + } + } // ------------------------------------------------------------------ // // Look up the second-newest entry so we know what to set refs/stash to - // after dropping the top. We read the reflog in reverse (newest first); - // index 0 = current tip (what we are popping), index 1 = the one before. + // after dropping the top. // ------------------------------------------------------------------ // let mut reflog_buf = vec![0u8; 4 * 1024]; let new_top: Option = { @@ -153,49 +272,123 @@ pub(crate) mod function { }; // ------------------------------------------------------------------ // - // Drop the stash: update refs/stash to `new_top`, or delete it entirely. + // Drop or update refs/stash — only if the merge was clean. // ------------------------------------------------------------------ // - let edit = if let Some(next_oid) = new_top { - RefEdit { - change: Change::Update { - log: LogChange { - mode: RefLog::AndReference, - force_create_reflog: true, - message: "drop stash".into(), + if !had_conflicts { + let edit = if let Some(next_oid) = new_top { + RefEdit { + change: Change::Update { + log: LogChange { + mode: RefLog::AndReference, + force_create_reflog: true, + message: "drop stash".into(), + }, + expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), + new: gix_ref::Target::Object(next_oid), }, - expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), - new: gix_ref::Target::Object(next_oid), - }, - name: stash_ref, - deref: false, - } - } else { - RefEdit { - change: Change::Delete { - expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), - log: RefLog::AndReference, - }, - name: stash_ref, - deref: false, - } - }; + name: stash_ref, + deref: false, + } + } else { + RefEdit { + change: Change::Delete { + expected: PreviousValue::MustExistAndMatch(gix_ref::Target::Object(stash_oid)), + log: RefLog::AndReference, + }, + name: stash_ref, + deref: false, + } + }; - let committer_owned: gix_actor::Signature = committer.into(); - let mut time_buf = gix_date::parse::TimeBuf::default(); - refs.transaction() - .prepare( - std::iter::once(edit), - gix_lock::acquire::Fail::Immediately, - gix_lock::acquire::Fail::Immediately, - )? - .commit(committer_owned.to_ref(&mut time_buf))?; + let committer_owned: gix_actor::Signature = committer.into(); + let mut time_buf = gix_date::parse::TimeBuf::default(); + refs.transaction() + .prepare( + std::iter::once(edit), + gix_lock::acquire::Fail::Immediately, + gix_lock::acquire::Fail::Immediately, + )? + .commit(committer_owned.to_ref(&mut time_buf))?; + } Ok(Outcome { applied: stash_oid, - stash_tree, - base_commit, new_top, - had_conflicts: false, + had_conflicts, }) } + + /// Walk `tree_oid` recursively and write every blob to its corresponding + /// path under `dir`. Used to restore untracked files from `parent[2]`. + fn restore_tree_to_worktree( + tree_oid: &gix_hash::oid, + dir: &Path, + find: &impl gix_object::FindExt, + ) -> Result<(), super::Error> { + let mut buf = Vec::new(); + restore_tree_recursive(tree_oid, dir, find, &mut buf) + } + + fn restore_tree_recursive( + tree_oid: &gix_hash::oid, + dir: &Path, + find: &impl gix_object::FindExt, + buf: &mut Vec, + ) -> Result<(), super::Error> { + use gix_object::tree::EntryKind; + + buf.clear(); + let tree = find.find_tree(tree_oid, buf)?.to_owned(); + + for entry in tree.entries { + let name_bytes: &bstr::BStr = entry.filename.as_ref(); + let entry_path = dir.join(gix_path::from_bstr(name_bytes)); + + match entry.mode.kind() { + EntryKind::Tree => { + std::fs::create_dir_all(&entry_path).map_err(|e| super::Error::RestoreUntracked { + path: entry_path.clone(), + source: e, + })?; + let mut sub_buf = Vec::new(); + restore_tree_recursive(&entry.oid, &entry_path, find, &mut sub_buf)?; + } + EntryKind::Blob | EntryKind::BlobExecutable => { + let mut blob_buf = Vec::new(); + let blob = find.find_blob(&entry.oid, &mut blob_buf)?; + if let Some(parent) = entry_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| super::Error::RestoreUntracked { + path: entry_path.clone(), + source: e, + })?; + } + std::fs::write(&entry_path, blob.data).map_err(|e| super::Error::RestoreUntracked { + path: entry_path, + source: e, + })?; + } + EntryKind::Link => { + let mut blob_buf = Vec::new(); + let blob = find.find_blob(&entry.oid, &mut blob_buf)?; + let target = gix_path::from_bstr(blob.data.as_bstr()); + if let Some(parent) = entry_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| super::Error::RestoreUntracked { + path: entry_path.clone(), + source: e, + })?; + } + // Remove any existing entry before creating the symlink. + let _ = std::fs::remove_file(&entry_path); + std::os::unix::fs::symlink(&target, &entry_path).map_err(|e| super::Error::RestoreUntracked { + path: entry_path, + source: e, + })?; + } + EntryKind::Commit => { + // Submodule — skip. + } + } + } + Ok(()) + } } diff --git a/gix-stash/src/push/mod.rs b/gix-stash/src/push/mod.rs index f4e1e0444b4..9bb8b314845 100644 --- a/gix-stash/src/push/mod.rs +++ b/gix-stash/src/push/mod.rs @@ -86,7 +86,7 @@ pub enum Error { #[error("required object was not found in the object database")] FindObject(#[from] gix_object::find::existing_object::Error), - /// Reading a worktree file failed while building the untracked-files tree. + /// Reading a worktree file failed while building the WIP tree or untracked-files tree. #[error("failed to read worktree file at {path:?}")] ReadFile { /// The path that failed. @@ -111,31 +111,47 @@ pub enum Error { /// Committing the ref transaction failed. #[error("failed to commit the refs/stash ref transaction")] CommitTransaction(#[from] gix_ref::file::transaction::commit::Error), + + /// Constructing the HEAD index for the worktree reset failed. + #[error("failed to construct HEAD index for worktree reset")] + IndexFromTree(#[from] gix_index::init::from_tree::Error), + + /// Resetting the working tree to HEAD after stashing failed. + #[error("failed to reset working tree to HEAD after stashing")] + Checkout(#[from] gix_worktree_state::checkout::Error), } /// Repository-level plumbing handles required by [`function::push`]. /// /// Grouping these together avoids crossing the "too many arguments" threshold /// that clippy enforces. -pub struct Context<'a> { +/// +/// `Objects` must implement [`gix_object::Find`], [`gix_object::FindHeader`], +/// and [`gix_object::Write`] — all of which are satisfied by the typical +/// `gix_odb::Handle` / `gix::Repository` object store. +pub struct Context<'a, Objects> { /// The file-based ref store for the repository. pub refs: &'a gix_ref::file::Store, - /// A writable ODB handle. - pub odb: &'a dyn gix_object::Write, - /// A readable ODB handle. - pub find: &'a dyn gix_object::FindExt, + /// A combined readable + writable ODB handle. + pub objects: &'a Objects, /// The current in-memory index state. pub index: &'a gix_index::State, /// Absolute path to the working-tree root. pub worktree: &'a std::path::Path, /// Identity and timestamp to use for all created commits. pub committer: gix_actor::SignatureRef<'a>, + /// Options controlling the worktree-reset checkout that runs after the + /// stash commit is recorded. The caller is responsible for populating + /// `attributes` (`.gitattributes` from the index) and `filters` + /// (a fully-configured `gix_filter::Pipeline`). The remaining fields + /// can be left at their defaults for a typical stash. + pub checkout_options: gix_worktree_state::checkout::Options, } pub(crate) mod function { use std::path::Path; - use bstr::{BStr, BString, ByteSlice}; + use bstr::{BString, ByteSlice}; use gix_hash::ObjectId; use gix_object::{Tree, tree::EntryKind}; use gix_ref::{ @@ -159,39 +175,27 @@ pub(crate) mod function { /// /// # Limitations /// - /// * The **on-disk working tree is not reset** to HEAD after stashing. - /// This requires `gix-worktree-state::checkout`, which in turn needs - /// `gix-filter`, `gix-fs`, and `gix-worktree` types not yet present in - /// `gix-stash`'s dependency graph. Callers that need the full - /// `git stash push` experience must call `gix_worktree_state::checkout` - /// themselves after this function returns. - /// - /// TODO(gix-stash): wire up `gix_worktree_state::checkout`. - /// - /// * The **WIP tree** currently captures the index state rather than the - /// true working-tree state for tracked files. Unstaged modifications - /// are therefore not recorded in the stash commit's tree. - /// - /// TODO(gix-stash): capture working-tree content for modified-but-not-staged files. - /// /// * `.gitignore` rules are **not consulted** when `include_untracked` is /// set — all non-tracked, non-`.git` files are included. /// /// TODO(gix-stash): wire up `gix-worktree` exclude stack. - pub fn push( - ctx: Context<'_>, + pub fn push( + ctx: Context<'_, Objects>, head_commit: ObjectId, head_tree: ObjectId, head_branch: Option<&gix_ref::FullNameRef>, options: Options, - ) -> Result { + ) -> Result + where + Objects: gix_object::Find + gix_object::FindHeader + gix_object::Write + Send + Clone, + { let Context { refs, - odb, - find, + objects, index, worktree, committer, + checkout_options, } = ctx; // ------------------------------------------------------------------ // // Guard: make sure there is something to stash. @@ -203,14 +207,14 @@ pub(crate) mod function { // ------------------------------------------------------------------ // // Build common text fragments used in commit messages. // ------------------------------------------------------------------ // - let head_subject = first_line_of_commit_message(find, head_commit)?; + let head_subject = first_line_of_commit_message(objects, head_commit)?; let short_hash = short_id(head_commit); let branch_name: BString = head_branch.map_or_else(|| BString::from("HEAD"), |n| n.shorten().to_owned()); // ------------------------------------------------------------------ // // parent[1] — "index on : …" // ------------------------------------------------------------------ // - let index_tree_oid = write_tree_from_index(index, find, odb, head_tree)?; + let index_tree_oid = write_tree_from_index(index, objects, objects, head_tree)?; let index_msg = format!( "index on {branch}: {short} {subj}", @@ -219,7 +223,7 @@ pub(crate) mod function { subj = head_subject.as_bstr(), ); let index_commit_oid = write_commit( - odb, + objects, index_tree_oid, &[head_commit], committer, @@ -229,9 +233,9 @@ pub(crate) mod function { // ------------------------------------------------------------------ // // parent[2] — untracked files commit (optional). // ------------------------------------------------------------------ // - let untracked_commit_oid = if options.include_untracked { + let (untracked_commit_oid, untracked_paths) = if options.include_untracked { let empty_tree = ObjectId::empty_tree(head_commit.kind()); - let untracked_tree = write_untracked_tree(find, odb, worktree, index)?; + let (untracked_tree, paths) = write_untracked_tree(objects, objects, worktree, index)?; if untracked_tree != empty_tree { let msg = format!( "untracked files on {branch}: {short} {subj}", @@ -239,22 +243,25 @@ pub(crate) mod function { short = short_hash.as_bstr(), subj = head_subject.as_bstr(), ); - Some(write_commit( - odb, - untracked_tree, - &[], - committer, - msg.as_bytes().as_bstr(), - )?) + ( + Some(write_commit( + objects, + untracked_tree, + &[], + committer, + msg.as_bytes().as_bstr(), + )?), + paths, + ) } else { - None + (None, Vec::new()) } } else { - None + (None, Vec::new()) }; // ------------------------------------------------------------------ // - // Stash commit — WIP (uses index tree as proxy for the WT tree). + // Stash commit — WIP tree captures the *actual* working-tree state. // ------------------------------------------------------------------ // let stash_msg: BString = options.message.clone().unwrap_or_else(|| { format!( @@ -266,11 +273,13 @@ pub(crate) mod function { .into() }); + let wip_tree_oid = write_wip_tree(index, objects, objects, head_tree, worktree)?; + let mut stash_parents: Vec = vec![head_commit, index_commit_oid]; if let Some(u) = untracked_commit_oid { stash_parents.push(u); } - let stash_oid = write_commit(odb, index_tree_oid, &stash_parents, committer, stash_msg.as_bstr())?; + let stash_oid = write_commit(objects, wip_tree_oid, &stash_parents, committer, stash_msg.as_bstr())?; // ------------------------------------------------------------------ // // Update refs/stash via transaction. @@ -312,6 +321,32 @@ pub(crate) mod function { )? .commit(committer_owned.to_ref(&mut time_buf))?; + // ------------------------------------------------------------------ // + // Reset working tree to HEAD. + // ------------------------------------------------------------------ // + // Build a fresh index from HEAD's tree, then use gix_worktree_state::checkout + // to overwrite the working tree with HEAD content. + let mut head_index = + gix_index::State::from_tree(&head_tree, objects, gix_validate::path::component::Options::default())?; + let should_interrupt = std::sync::atomic::AtomicBool::new(false); + gix_worktree_state::checkout( + &mut head_index, + worktree, + objects.clone(), + &gix_features::progress::Discard, + &gix_features::progress::Discard, + &should_interrupt, + checkout_options, + )?; + + // ------------------------------------------------------------------ // + // Remove untracked files that were captured in parent[2]. + // ------------------------------------------------------------------ // + for abs_path in &untracked_paths { + // Best-effort: ignore errors (file may have already been removed). + let _ = std::fs::remove_file(abs_path); + } + Ok(Outcome { stash: stash_oid, index_commit: index_commit_oid, @@ -324,11 +359,116 @@ pub(crate) mod function { // Private helpers // ======================================================================= // + /// Build a WIP tree that captures the **actual working-tree state** for + /// every tracked entry, not just the index content. + /// + /// For each index entry: + /// * Regular files and executables: read the file from disk, hash it as a + /// blob, and use the resulting OID in the tree. This captures unstaged + /// modifications. If the WT file is missing (a `git rm`-style change), + /// the index OID is reused — the file is still represented in the stash. + /// * Symlinks: read the link target from disk and store it as a blob. + /// * Submodules (`Commit` mode): reuse the index OID without recursing. + /// + /// The resulting tree therefore reflects the state an observer would see + /// by reading every file from the worktree. + fn write_wip_tree( + index: &gix_index::State, + find: &impl gix_object::FindExt, + odb: &impl gix_object::Write, + head_tree: ObjectId, + worktree: &Path, + ) -> Result { + let object_hash = index.object_hash(); + + // Seed the editor with HEAD's root tree so existing sub-tree objects + // can be reused without being re-fetched. + let mut buf = Vec::new(); + let root_tree = find.find_tree(&head_tree, &mut buf)?.to_owned(); + let mut editor = gix_object::tree::Editor::new(root_tree, find, object_hash); + + let paths = index.path_backing(); + for entry in index.entries() { + // Skip sparse-checkout directory markers. + if entry.mode.is_sparse() { + continue; + } + let path = entry.path_in(paths); + let entry_kind = entry + .mode + .to_tree_entry_mode() + .ok_or_else(|| Error::InvalidIndexEntryMode { + path: path.to_owned(), + mode: entry.mode.bits(), + })? + .kind(); + + let components: Vec<&bstr::BStr> = path.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); + + let blob_oid = match entry_kind { + EntryKind::Blob | EntryKind::BlobExecutable => { + // Read the actual working-tree file so that unstaged + // modifications are captured. + let abs_path = worktree.join(gix_path::from_bstr(path).as_ref()); + match std::fs::read(&abs_path) { + Ok(content) => odb + .write_buf(gix_object::Kind::Blob, &content) + .map_err(Error::WriteBlob)?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + // File deleted from WT but index still tracks it. + // Keep the index OID so the stash still records + // the last known content. + // TODO(gix-stash): represent deleted-from-WT files + // as a deletion in the WIP tree so pop can replay them. + entry.id + } + Err(e) => { + return Err(Error::ReadFile { + path: abs_path, + source: e, + }); + } + } + } + EntryKind::Link => { + // Symlinks are stored as blobs containing the link target. + let abs_path = worktree.join(gix_path::from_bstr(path).as_ref()); + match std::fs::read_link(&abs_path) { + Ok(target) => { + let target_bytes = gix_path::into_bstr(target); + odb.write_buf(gix_object::Kind::Blob, target_bytes.as_ref()) + .map_err(Error::WriteBlob)? + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => entry.id, + Err(e) => { + return Err(Error::ReadFile { + path: abs_path, + source: e, + }); + } + } + } + EntryKind::Commit => { + // Submodule — record the checked-out commit OID as-is. + entry.id + } + EntryKind::Tree => { + // Should not appear in index entries, but be defensive. + entry.id + } + }; + + editor.upsert(components, entry_kind, blob_oid)?; + } + + editor.write(|tree| odb.write(tree).map_err(Error::WriteTree)) + } + /// Build a tree mirroring the current index state and write it to the ODB. fn write_tree_from_index( index: &gix_index::State, - find: &dyn gix_object::FindExt, - odb: &dyn gix_object::Write, + find: &impl gix_object::FindExt, + odb: &impl gix_object::Write, head_tree: ObjectId, ) -> Result { let object_hash = index.object_hash(); @@ -356,7 +496,7 @@ pub(crate) mod function { .kind(); // Split the path on `/` to feed into the tree editor. - let components: Vec<&BStr> = path.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); + let components: Vec<&bstr::BStr> = path.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); editor.upsert(components, entry_kind, entry.id)?; } @@ -366,19 +506,24 @@ pub(crate) mod function { /// Walk the worktree recursively for files not in `index`, write them as /// blobs, and assemble them into a tree. /// + /// Returns the tree OID **and** the list of absolute paths that were + /// captured. The paths list is used by the caller to remove those files + /// from disk after the stash ref is committed. + /// /// Uses `std::fs::read_dir` rather than `gix-dir` to avoid pulling in - /// `gix-pathspec` / `gix-worktree` as direct dependencies. `.gitignore` - /// rules are **not** respected. + /// `gix-pathspec` as a direct dependency. `.gitignore` rules are **not** + /// respected. /// /// TODO(gix-stash): consult `.gitignore` via the `gix-worktree` exclude stack. fn write_untracked_tree( - find: &dyn gix_object::FindExt, - odb: &dyn gix_object::Write, + find: &impl gix_object::FindExt, + odb: &impl gix_object::Write, worktree: &Path, index: &gix_index::State, - ) -> Result { + ) -> Result<(ObjectId, Vec), Error> { let object_hash = index.object_hash(); let mut editor = gix_object::tree::Editor::new(Tree::empty(), find, object_hash); + let mut abs_paths: Vec = Vec::new(); let paths_storage = index.path_backing(); let tracked: std::collections::BTreeSet = index @@ -387,8 +532,9 @@ pub(crate) mod function { .map(|e| e.path_in(paths_storage).to_owned()) .collect(); - collect_untracked(worktree, worktree, &tracked, odb, &mut editor)?; - editor.write(|tree| odb.write(tree).map_err(Error::WriteTree)) + collect_untracked(worktree, worktree, &tracked, odb, &mut editor, &mut abs_paths)?; + let tree_oid = editor.write(|tree| odb.write(tree).map_err(Error::WriteTree))?; + Ok((tree_oid, abs_paths)) } /// Recursively walk `dir` and add untracked files to `editor`. @@ -396,8 +542,9 @@ pub(crate) mod function { worktree: &Path, dir: &Path, tracked: &std::collections::BTreeSet, - odb: &dyn gix_object::Write, + odb: &impl gix_object::Write, editor: &mut gix_object::tree::Editor<'_>, + abs_paths: &mut Vec, ) -> Result<(), Error> { let read_dir = std::fs::read_dir(dir).map_err(Error::WalkWorktree)?; @@ -418,7 +565,7 @@ pub(crate) mod function { })?; if file_type.is_dir() { - collect_untracked(worktree, &abs_path, tracked, odb, editor)?; + collect_untracked(worktree, &abs_path, tracked, odb, editor, abs_paths)?; } else if file_type.is_file() || file_type.is_symlink() { let rela = rela_path(worktree, &abs_path); if tracked.contains(&rela) { @@ -439,9 +586,11 @@ pub(crate) mod function { EntryKind::Blob }; - let rela_bstr: &BStr = rela.as_bstr(); - let components: Vec<&BStr> = rela_bstr.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); + let rela_bstr: &bstr::BStr = rela.as_bstr(); + let components: Vec<&bstr::BStr> = + rela_bstr.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); editor.upsert(components, kind, blob_oid)?; + abs_paths.push(abs_path); } // Special files (sockets, devices, pipes) are silently skipped. } @@ -465,11 +614,11 @@ pub(crate) mod function { /// Write a commit object to the ODB and return its OID. fn write_commit( - odb: &dyn gix_object::Write, + odb: &impl gix_object::Write, tree: ObjectId, parents: &[ObjectId], committer: gix_actor::SignatureRef<'_>, - message: &BStr, + message: &bstr::BStr, ) -> Result { let sig: gix_actor::Signature = committer.into(); let commit = gix_object::Commit { @@ -485,7 +634,7 @@ pub(crate) mod function { } /// Return the first line (subject) of a commit's message. - fn first_line_of_commit_message(find: &dyn gix_object::FindExt, commit_oid: ObjectId) -> Result { + fn first_line_of_commit_message(find: &impl gix_object::FindExt, commit_oid: ObjectId) -> Result { let mut buf = Vec::new(); let commit = find.find_commit(&commit_oid, &mut buf)?; Ok(commit.message.lines().next().unwrap_or(b"").as_bstr().to_owned()) From 5c36dc0c829678e7a545af90e49adf04fe20b2a0 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 23:17:36 +0800 Subject: [PATCH 07/11] test(gix-stash): add integration tests for list, push, pop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover all three operations with bash fixture scripts and Rust tests. Fix NoLocalChanges guard in push to compare WIP/index trees against HEAD rather than checking for an empty index. - list: 3 tests (newest-first ordering, empty repo, positive timestamps) - push: 6 tests (unstaged capture, staged capture, clean-WT bug→fix, include_untracked flag, exclude_untracked flag, empty-repo doc) - pop: 5 tests (clean apply + ref drop, multi-stash stack, no-stash error, untracked restore, conflict keeps ref intact) Co-authored-by: Claude --- gix-stash/Cargo.toml | 17 +- gix-stash/src/push/mod.rs | 19 +- .../make_list_empty_repo.tar | Bin 0 -> 62464 bytes .../generated-archives/make_list_repo.tar | Bin 0 -> 81408 bytes .../make_pop_conflict_repo.tar | Bin 0 -> 76288 bytes .../generated-archives/make_pop_repo.tar | Bin 0 -> 71680 bytes .../make_pop_two_stashes_repo.tar | Bin 0 -> 77824 bytes .../make_pop_untracked_repo.tar | Bin 0 -> 73216 bytes .../generated-archives/make_push_repo.tar | Bin 0 -> 62464 bytes .../tests/fixtures/make_list_empty_repo.sh | 11 + gix-stash/tests/fixtures/make_list_repo.sh | 23 ++ .../tests/fixtures/make_pop_conflict_repo.sh | 21 ++ gix-stash/tests/fixtures/make_pop_repo.sh | 15 + .../fixtures/make_pop_two_stashes_repo.sh | 20 ++ .../tests/fixtures/make_pop_untracked_repo.sh | 15 + gix-stash/tests/fixtures/make_push_repo.sh | 11 + gix-stash/tests/stash/list.rs | 73 ++++ gix-stash/tests/stash/main.rs | 135 +++++++ gix-stash/tests/stash/pop.rs | 309 ++++++++++++++++ gix-stash/tests/stash/push.rs | 339 ++++++++++++++++++ 20 files changed, 996 insertions(+), 12 deletions(-) create mode 100644 gix-stash/tests/fixtures/generated-archives/make_list_empty_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_list_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_pop_conflict_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_pop_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_pop_two_stashes_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_pop_untracked_repo.tar create mode 100644 gix-stash/tests/fixtures/generated-archives/make_push_repo.tar create mode 100755 gix-stash/tests/fixtures/make_list_empty_repo.sh create mode 100755 gix-stash/tests/fixtures/make_list_repo.sh create mode 100755 gix-stash/tests/fixtures/make_pop_conflict_repo.sh create mode 100755 gix-stash/tests/fixtures/make_pop_repo.sh create mode 100755 gix-stash/tests/fixtures/make_pop_two_stashes_repo.sh create mode 100755 gix-stash/tests/fixtures/make_pop_untracked_repo.sh create mode 100755 gix-stash/tests/fixtures/make_push_repo.sh create mode 100644 gix-stash/tests/stash/list.rs create mode 100644 gix-stash/tests/stash/main.rs create mode 100644 gix-stash/tests/stash/pop.rs create mode 100644 gix-stash/tests/stash/push.rs diff --git a/gix-stash/Cargo.toml b/gix-stash/Cargo.toml index 152beffdae8..8908c4f7f2b 100644 --- a/gix-stash/Cargo.toml +++ b/gix-stash/Cargo.toml @@ -40,9 +40,20 @@ smallvec = "1.15.1" thiserror = "2.0.18" [dev-dependencies] -gix-testtools = { path = "../tests/tools" } -gix-hash = { path = "../gix-hash", features = ["sha1"] } -gix-odb = { path = "../gix-odb", features = ["sha1"] } +gix-testtools = { path = "../tests/tools" } +gix-hash = { path = "../gix-hash", features = ["sha1"] } +gix-odb = { path = "../gix-odb", features = ["sha1"] } +gix-ref = { path = "../gix-ref" } +gix-actor = { path = "../gix-actor" } +gix-date = { path = "../gix-date" } +gix-index = { path = "../gix-index", features = ["sha1"] } +gix-object = { path = "../gix-object" } +gix-worktree = { path = "../gix-worktree" } +gix-diff = { path = "../gix-diff", default-features = false, features = ["blob"] } +gix-merge = { path = "../gix-merge" } +gix-filter = { path = "../gix-filter" } +gix-worktree-state = { path = "../gix-worktree-state" } +bstr = { version = "1.12.0", default-features = false, features = ["std"] } [package.metadata.docs.rs] features = ["sha1"] diff --git a/gix-stash/src/push/mod.rs b/gix-stash/src/push/mod.rs index 9bb8b314845..c4cf6763575 100644 --- a/gix-stash/src/push/mod.rs +++ b/gix-stash/src/push/mod.rs @@ -198,9 +198,17 @@ pub(crate) mod function { checkout_options, } = ctx; // ------------------------------------------------------------------ // - // Guard: make sure there is something to stash. + // Build the WIP and index trees early so we can detect the no-changes + // case before writing any commits or updating any refs. // ------------------------------------------------------------------ // - if index.entries().is_empty() && !options.include_untracked { + let wip_tree_oid = write_wip_tree(index, objects, objects, head_tree, worktree)?; + let index_tree_oid = write_tree_from_index(index, objects, objects, head_tree)?; + + // Guard: if neither the working tree nor the index differs from HEAD, + // and we are not capturing untracked files, there is nothing to stash. + let has_wt_changes = wip_tree_oid != head_tree; + let has_index_changes = index_tree_oid != head_tree; + if !has_wt_changes && !has_index_changes && !options.include_untracked { return Err(Error::NoLocalChanges); } @@ -211,11 +219,6 @@ pub(crate) mod function { let short_hash = short_id(head_commit); let branch_name: BString = head_branch.map_or_else(|| BString::from("HEAD"), |n| n.shorten().to_owned()); - // ------------------------------------------------------------------ // - // parent[1] — "index on : …" - // ------------------------------------------------------------------ // - let index_tree_oid = write_tree_from_index(index, objects, objects, head_tree)?; - let index_msg = format!( "index on {branch}: {short} {subj}", branch = branch_name.as_bstr(), @@ -273,8 +276,6 @@ pub(crate) mod function { .into() }); - let wip_tree_oid = write_wip_tree(index, objects, objects, head_tree, worktree)?; - let mut stash_parents: Vec = vec![head_commit, index_commit_oid]; if let Some(u) = untracked_commit_oid { stash_parents.push(u); diff --git a/gix-stash/tests/fixtures/generated-archives/make_list_empty_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_list_empty_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..9aca87fcdfd81b39111d6cca1996a2e73f7e832b GIT binary patch literal 62464 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 w2S;rZo#u+MTToLAgexN0`25S{2 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 literal 0 HcmV?d00001 diff --git a/gix-stash/tests/fixtures/generated-archives/make_pop_conflict_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_pop_conflict_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..5d70a5e73c88c9ce4dd3745cd8ea985140b34dda GIT binary patch literal 76288 zcmeHweQaFWb)VMu2Icq<2PlfRC<5Fa(MwXx`FJzK8IoFRS4(ME)Jmi*QPH}h#CQbfHsbc!gXWBHQK}o;G*7SV+;rx|(EqadE6vQD;3xj# zpYqJg7K&Cjm(AtneJP)tIWaTzk)eZ*-?m#yJ#tqvwaDZixxwRUAiYs*54$IV=Aig5 z&E!V-A2wppKJlNaHLBha`#L!Ot!ybf;{PF{By2jwf5yFEsdpSVd~(<553z6Z|H1bE*#BA9X#YP%oCHmW_}A-y@c2kS4}pJ3rP2QXNRhBhV=?@T{m;RdME=>|C*AIivOe z06={g747nW+uj(G5^xy#Uo4bH>;Ey(Q5XNP;rW|G>G3e~Kbs%z|AvB$SfeFu?<%>9 zU8>AfigR<8Q*t2m?Q&%FlwUvVV= zkAcQQ{Ab=i`(G(v?6dzBawGl!7|0)ZK)~K~>c71AIm2LR%^y9?7JKlG}a9H~uVatUlavq+` zyYy1Rzer};|H$^c$vQOb=1ooQf~|OsHe9c2aUY9zm;Z-n1SFCVX#cZ{h!-2he+^#( z^x}UwM!-YhKUXS@?7zduzKQ>${+Ee4fse3r;(xFBpP6hSzW(Qn#p1~ScZ4|DttsMW zKY#Cn_5U5b%I*gJxBq!e{tv|nMEcr`|8xENe?FTX#s3@!b?k@tSw`*GP;>|#cBP>h zb~Mj0LVuIGtkz$j+lT&o95DK6?nC&$)^ObW!|$i}?HC~-{11U! zt!)gg*TMW>>f`@hZsh+N8bTrs->i5o_f}%8NUcI&NaD%n1JlRkiCreqsA)*X!6ncAAde#+d+9>?We{TWGssZ?W{M>$lZ) zJW7nYSnumBhDTA`;Ap{*pypV<3jZ&zT)w=tx_$CqO1a-9k zKXOFu(4eus%h8=c@7|Byc!#D&kKTn45dPovygNgR{UJ!m{|B}Iqr%Ake+=~0#eb&R z@i$X#FICxeD|ft3JMD|l?$HM^0RJ;Q>lxes&CHZ=Ui8ts9^ArveE)ayiA=fH$oQKH zoR2))z(Gqvx}r!>ex+4w>hqMM+2L5G5QpoeKp)TrT~|Rz#9>XGAtm|XuqT>X-K_b# z0esdPcfC6<4s~v`RI24x+}d3?)wC;jCao!TZ_{lk)NZ+UrH#|19!sGOwx;ZcqXL~0 z>ZRLq8x?obpW@lk8cKJS)@dm#jqyY&Yon2NtHu+mI1Z|Z&sXJ^-KcEpW2+cmrPD&I zJdbK-rZ_WpjKXNbj5qDLjqOEU_Bq{x-ZXVucAI+gUAtAY%XL?wO`ck9d#J3tU26Dk zoUPNad2U-dZq@G8+tbSLU?4ab%c-Io6=)xj!ckTZ^q$wcBi?30*$fC1houHBd%8>u zYI)m@z(H-=J_@6gTGhZy@c^&17|s<9L}#mYfYTZmWgGw=&_Aqo?TWw`!!(T=v!5L3 z-IfEudh&pyr^`#D4fh^E%^72&?OL-t6^)m4FY%x*4g(8R@YTbnq*cQDh9_Qg(5&81Shv`asSjM=N)$q8S2CA7m!fn_dkbbnJy_jlAnN zYILY$4^*qxDuVHV)Ll*KhE8}MI*44*&iH$qH6b^Qy4DQvW`M@QYhnWWH_)TR7I4Nw zug-izqh?{;B6}{iRmCQYf?q^ovsJ@uq(0`hJPy3w5n9(|B;ydunhaRX@mwFR_}(-b zZVQ4B)y<@~rMys=!kpvuHGt9&&^$EjGE?Q8n>*e_bq!yxWk0ur9@#xGWKT))-=>2SJW5eg4J z9st@1DY7((FeFzpRurHSU}_BW?79((`n0-8Mo!TuQEjVMry=qW63j`7NKAE9YD+>y z03s_XGXhi{5~ioztxlbTr_8LnU?j`{N9ZX4&=MpYhu9(+BL>F7Y%3HnXhcvRNLmOy zO>6CYp6-?VcG4G1#K%%2;(-PbF#>EbX!e2S9Vi0fp@NUbF%ry`zXA+ZO2(_l!|y^38Aacrj1Bg15x4&@@r4(#Q&D5CVdIG~8kD!c7_u0d|lX_^$a zigKE%0WcdJ*>Y1}D}~$zKGB5q0&Q@V4+v8#1Kf}_^?JZ}i@>`>5+h?vms%Eg1%bQ) z3?_V$Qc-58VT zWr}VA3Y!tKx>v(k!T;`kx6)~&mr!vbQSy3|$f3bW(<`h|ph+j0sMaFQlcvJl7O!?{Seke4A=>l}v-E221EghH?O+|Z zH-Ho)$aGIe-4SYRd7TZgbA$lBDiTltG)nP!{W@0Xbtp$Pvw={aQAx&;CDpt-fBDMl zN7q+jIA6bZ;jD$j*e?-8!i;uXCZJNTmMOV28OObwX>{s!GibKGAR%e4806&l_#|ti z$IX;imc$z=qq3z^ej8{~PlxSH?Eo}`kyHS*iVd;g>m~~ z{*uvl7bQgqC5Wb+UOfqvO6e|O<(vlV|L^e5 ziQ#=4?f=aFk78=~4PY7T`yJJ3*bT5AWG=LKoEOvU6SNAKni|)DMW`2eiKQS(@e^J5 zl16E5ZFEEvm%Jn58}K)6E9D{ck9+|G>un@-rH1UO+QEi$8VyPfc|x}Y*QTVV+@xMs z7sZ){p`_#wU&OqX#=yLLkqO|1(z}ha=ha;*bT0Ok&?&LVVBLyOHCUUa9ZV4lOrWqT z3@dP-q?PLK@u(ndLqats+E~^JpU7Aox9+xGfN{V`q#PGffq4Hg*oic8m_f*vW_$1; zZ1xVVew*rv-Uk5&c9n776$oxBJn6{HWk4W+J?E}9yZuf-&;vn6T1)C;;;hx%or%?8xvZrxsINFpvvz?8qgW2Hjpk|F%O^`XjUyEY@Yj)Iuy+WG#^s@ zg6t%~BiI?>doGy(o9OTPBo$*;yk=uVVJsO`A8xAD$FK!?0lpph`IG9_X#xrglx*RW z=Y%e~>kxJ_)}BzCUYy^O-tgfiY&{1r^zjm=}WZ^2w=0g~vmZ zNwCK`&X1oj#p# zeY+Cy+9Ivwin!$&V~eg(sa(^Mke)>?L71m9sEht~5~b2n3x@(BNR8*`urX z>FAh9yQL;>W>a%^s(NceO=KsgG?yn$-$}UCljCeL!UTF)>q4cv>xAAVL|+(^4!0WZ zU^;qdU;rL);B?TTv?{Pi00DA{afQm9n$~j#(8pi{ZKQg120ZjPy;ghOG`2AQzy&jsvyrKf=+B_n+7}|nI?C@*kxL5w+{e=NFzG~fm_`; zjT_}5B-vnL>T)AUB{?1!$-=IC5b|Q6W!MojF?YWEA=fPthu&mbPxU5~*J}z!B<{|&?G`xF|X;`0V{4}-_tUlnkF4NJC z#1dxxFydS^y{@Q=Equ@8m?qsf36W)^#BjqiO&mwd5&KUQM%R|hqqj$F1EL+06 z(6G>YumP1FDOzn%BEqsn!Nad~h@p*`4^&%$I685uU_fw8(IzGSGq&5!Hw_&G8wS`g zd~K#5x3Hg+zB^4HzC2qy&JnZ9K!SU4mSIMWX5f%$&Kb`I70ilPb~!T5Mvj`K^Kh$) zJ-N6l~u7NS=!-Y)j-8~b=(b%VctE)QQYIEA%u(9tW~;Z2XP-U7}uym7}A#8 zqLTu3CK~Sj_C%mN2oyKmwt&tIa2hzO*zm7wm4ZW)=nI{P8y*+LDNr>I2Oel#%}%*4 ztAH}@47ha6vjz5=AhtC~wB-T|ErWfb1)ZeYlQSm$Hm9?Y6vba_Ry1ws{fU0}GO5Sk z-YR?bS_Sf{D#rbn)oZUA*K<~#hHn(!Lzw;pzhbKcXH__82SzsWU5JZ>Mg_BwLUD-!rKKu?t}R&MyfsmxnX@`H6W zq&xJV@EY~)byirnV!#PuaXY}z_yfa253#TGu}6M6Qa!gzbr`YD-wxFS=qAS_w)>|?hq{iUO32x8(FsBJE5$xP{LR}z1I!%(bxNk);>vn-Jqj(++ zVPjfk7(-g+IwXHPPAj`(jUVY)` zZ)R^{GcV7#{0-V+P6Z`k)-l!}8hiDQwM$Hb@<5``uQigUnaK|uba3cXXmHDJl6}X< zGPfrKBhXV*D#P+)lUTVH&M0e&zaA>TS+BJxGi!|u3?DK)>z2Htoq}VBWnfE9hE2xW zoJ^mZ8rKbv-PA3Q=VX36ww=2*C4oU>q>0HVL=5@ZQ*=T9KsrpF0hAt^qEdDounK;U zHQ_^Qt81(0&R@N%_(oW_JGZj9et!8}BC$&MlUbBSr|@BC-{{yaClC-?f{IK=D38QUlo7rX zEQv;82xUH&*9Hv!Xjq5&RT-pa`#$_)4gv*48qFsX>XceRz+-&npbBCyOH$Yk#Hh{};B9gWC4rzTdxSWNi36M9# ze_*KMDrgAjmM%9(|Zw2w^|J-D%c9pN4$2T~=0UL_UH1 zrE*2wzF3LOPQ`!<@YEX;v9%D20aKG?gt6Iml=ejt3=t?1kPPDj*?_jR?xr^&h%T0{ zZG`6{N(|(xt2SMa`LWorEqkX$DMNNIpW2>{x1aeEFt($F^&lexX1DgcdZ zPa$te!>gcS0T*GZ8JI0Ab(xzv#5_VV!7V$C`|8zwr1lPsjU82Vj^acP)LOYxHo|8I=50$MkSY}BA z@I++v?paJ-vwX`?j5OxFI5JSERu#3Z4pveb+oJSC31Eg2%0`kJb~hN#4XPI0l>|aU zZ+aFf+k;ICr3!Yw-2Ve2>=g$rz)$d(C!R9}yB)7`f!=B&E>%HvlZdrp$87e3{mg9* zO|DqGlrXrI5_%s2hDKBgIU6z7!s!HlWP;FJSiC#E=wNdFgVBIZr=>BBl$#YUOZZRh zF@X@+T@)q)^>6q#xVN*1KKrj6&pBEH@ZR>{ERKHl-Tzm>bu=UU?=cWYul@IIs6}ey zsR;>!$Pjgqf&wlhIacB|8S;ftZfyw;&W)Kv5#0H6kw&0av^1Cy0ty;^cv~O~mf_7? zm_S36KrNTC%H67HB|PQW=uMg)ES!B{kF2tk6Kbk}-gIP?Is#$71ZN&M4s7uNc^ z7_ZUvVeFDpNQYI3gHHe?u0iAP6T?avs1=z?O1uBYA4n4?Pc1wQo#J$nBHDleq~oyb z-xI?n_U?#6nXIX&U_`)s2P@wwfch91@NMoQD*tgWXKENh-~;4;7Wa?DqFPwrx0)(w=B;3rucXm&cOoX%ERAA;dlkt+n(i2V!ZE*VlV^C*aa9*hDjZSwwI)y~}AO3%v@w>9cK0+065dRlv`t-kaqt+l90BLz>4IiB;V5(4+3xS7z70ZY%SWD zuAOIMdlzd<%%5)#8Ps4N*M?41Mjwc+#EEM2@r749{I8BGp;7$`!%dT!2{%tvF*NtgRrC5?D9c6}oh6Nh)EnQihpI>RZjofL*A?&_igxkX1 zfT`V8Hw-bM_~d50-Sp=(nW|lJabn^Qsc*AWPS?ClKyw-71fk89wk5K0lO7|O%Miq& z4d!SEk?XQolptdqw~_GuK+y=~3ZzsMJ~h0x6{MGBvzrW4U?`Z$x!tTI*rFR9c?Ui_ zIUEjK6wkQ92*sQbl7@h#A@pGXGV;ZdOd&FuFuW2Cuvs+1tuId%MBNA4ogqmi(73iK zQba|or_~h*4j35`L4{_-RAf@?iL5nx}G_X&jWCD zaz9IFwUE|zSR%K0PEgcK$1Qg@HZU3V-{U7v;xJLr=nid)GZ=NuX^oIJB>4b{FrWIJ+;gjCdZ{>4TkP#rH&BUI_83-DZ7wZIA+r#i+T`wh?l^m zsxVO}&m{4W5PL8`CPad0NaG{PQGs)74ag_NR!fcfPP;k_Fye)#Up$5iU`Dsr(4*6_ z3pguP_aKH*gaZv4LS)3s^;L8nwq;r}ji7P;Ai0)hPdX#V_%umCKhdvsSHP$STQGxl zX(qrOVBds!k9j*^y&no^4h%$`>?PJDLq#Mf!}F4YfkY1KHHG1YMQqzdZFn>yn_BXitIoDb1s2Oi>c6eLeRkMVb$!^*~k|_^+_M)DU%{2cVO9aKTB+s+9zO8_Xes5dqoM8Ss*A z@W6MYvdxGR_r6Fwgc>0$3uK;ClW4DoL?&=GO;&4e-SLwwRC)UbH2MeCg&P`kkp~A}T?{7l{DP8kN&`+LPC<(ms0$*bqRWuPD1uwaJzNkVRZwrT zADq()2m27d;3B%RlGG7Q{iTHQlKs!uq=T`Z7pCi1U%d(9(J%%jMF%Hqv^w`a$Jf(Q*WjH)}(9;|{q!~Cs!o0efB%qVGrsZ)2r1HvZqlueI ztVB}E$iL*RTPXby|45`1V7{ABCFDb9l0gzfgXO9eO>;RjaqoK{Z<;@}JyzWvOLbR{CQJFi_Ai=gN(^9o+~PfY~Q z4Wm)(TffLjS}a08K?ti^i1sIgM7##w2|1ID2K9oReh6$1)M3zR(84}D?KC7`Q&>$c zysk30*B)RUIkbr1Nuw}=9vXx!&cWI*m`5r(2roo$>UU5v@WRvR6ltv1sGzQ{AjgiqVs_k?05=Myoj3)I0#6#3(MhunKGRF|EX4##UDG z#D$!s>+QO_?n43XSz*V<3_J*#K!+T($fD97(2P@)E*ogb`&$2{b<)Njd>BGR<5S8G zmM(@_@FCJAE=lnFhG91BO0lD}>-X$!MrMc_(%s`RdLII=T5DlDsRgr7rymzYDJpNd z*Gw|7&79%`kwBidx09+`$H`Vu6&GdF9E1^SNCV*jbhC1@h9#k3p)hkI>J2l3>KKoX zqX#x>Nm`$|=C1FpEoSVoziG}%Y&FR29$<-FiqR?rN@-AwhDDP2J zM|cL6VnXAQ(5%t2;|5?dnMqDj!wz&o=!IAj`-E0+{{vJp;en9K49d7w(kgOnD`v#X zxY>MjcNR#E+bfi16I=eaKY=4EYTP`iY;Pb$Z^CE_k&5868?{D0H$VS^+g`lq7$EnQ zcpcu9%-ULJEkk+mP#+3?AP~?SKh(pEvFL!%E}qWMS4@+HkeOGs7^nECCZtjQcQFJa z78_)TbmU@&l&tQ|TW~ZPp!c@_W($S>`~QkK6*{v29s^x<{Xejh?QYHuC_?Y8Gn4qzm!@LWNN5f%qT`m|j9H!M-=HxLv=A~C5m-3D!vVwBsBgC zPUwUwQaJ?)Q>W*`bzpf&1pTg8sKBiwPYaM{@+(sd(}!lEKC-*{Z~)>N*`cdRW?-eS z1S+cZ4&K8_o-w(t2TOn0oyIhbRZt&#`Z*6HH!MxP6l=&-PqBym(6*?mTPl^uu7yn{ zsqbhNgFE2;4V)$|3Cxv&v7VlaOhTTeoAn)MV$NjisT{03Xvn}F++0HI-A;Nv4P|al6 zXQ*4vE7@<%8?!NG1ePv|u?qU^O+W@VMh0TwG03jR&l@XOUzD`L31nQ3LI)5aEp4Dl z#WUcynFyIk1cZ6?5oyUa0mVdvywQhg!#6!LN(x|hrUMfK%dzZEtUD6(>dNZbOTj2O ztPoi;%|WOlieb}jYIkG+G6*0!`klgLBjA`&=U-Y{6|x9tWw;+Fg^xkaddGGV(f?(Y z6`7mDS=%Gh{WFdkPcP{bRiuc z;~vV}XrW6)@2Kc6p85?F!f=>6=_;kJ>p=EUvojM5CZ&yGqUY{AcC|Zuvd|7uW4aH# z+eg%xF3W~`qQ=4|qETa!hX;!qi|{{|pOl;@GqGw%CIJTVf3g4k&kQbw8twm&K?3ZT z|FKIM82^cFbv*yK(34604d0X<tbwR)9Ev?7-gL*iNIQWf&k74FDD0lwKMBnUK$2%MLN_Ga#)P|MRfck@0=d z#{mA%&GfJT1#86r0r-ac+2wyQ;O@)+9_&w&!SOp4V|_HoHwYY*9P!{r$%8}F2#q>e zZ}pCirvY(umCiz8@-T#HE?GtLE$GIinR>V93(+yBK#(ltf8q6L4mfhxh~%2Ezm^nhx!hWSMr839`4bj z)j;CtC7nCXlt2UHMHu*skr8>o8V!%U1_5Djv_|qNy9sP^PwQX`?8lMcU4Qp#4!)al z>lfw+-*mCER{}hNEKHSJ8M z(fw<4Tc=gW7o@{_)TG=AeLR%s_w~J4Om=O)VWtLi@=*7`j;9Hp62-b@cGLnfKG*5G zB&m0pSnDJbF*Y0+rnEg4zQ1``8szSuNE6+*d)gQSuwXf&b9xzp)Y`%f+ZJwqWR5nP zNDOdU2$vac&>Yzv51yK?(86_dvm#VvCSfME#v5d`_6`kpN0(0bw$2QF9NnU%Sf_~) z_%lu5g>K6jWV*TL8j8sq#o#e<8x@%NNe0jC?;%uDJm0HZ;DOr!6k?^3JMErlZl*|w zBp5S1AaPovIt*X3f$lL{jGAf3gxB9axp8~ff100+_J4#nfI;hjDOcz_|B)?~M*9Cy zSAN}icm3ac{~y>UDQGqOMCI)cn!n*OEr3~zi&n5?a&HZpjk9E2;qexd+O%r6-_wju zm5Wh$ri}HY*EN{a54sBKno7vG<0E}Zg?W@#QH85Jo;5oyT)`qK9{czE-P$_*MN*Zq zKT!=B)4Q8?!Rz-}&!TEM5lj3rt_X;kVRozRQ7!J1?XzJ$Y? zpd)6gp-8fUmsQC1qp6m@?Gj)nWI|j<(*g5^)Yo-zNuZ9Lmuxw5Q6&M&AtS4s$xaLH zk++xfc8&^yx-?NC#Sx0ZhiU?#d?-AUm^`4R$cZ4RfzVZgBo**1@L&AKFE&kqH^-r&RF(ZIk8;{8VSgV5mcQpIyAs%8~HVnb_7z{|E7Cq)|fFl0! z?xn2y`WoTxF%zc}y_^+0dz`XnmQ6jXc`YY{e1-^`(HZ0A7@)qnH4(P6`=zd#;7(Y5 zW0Tkqn4hsX{TmG=XRj?TEs3&2fee)iK4q+re)kT0u7bEIwDjA2VeuK+=RpzCsP2Bp4)T8 z6OHY6&kfL$LZ!;>GfZUc5!VT$b>v2ELB+r>oF*A$lcd388kfQ#y(D8-IDb&3L;~ZO zBe%4rr*(p1S7M?H1}x8nP8af(jI^KzabsyRmpdL2vTx zBbuqwGGGin&_$s<$vI;g8e@%^*Fcj97yjTz=Tz_VFVS(Sl$5|O03cjA%iH|M1Nui= zf2A%huAjYhX&?%I0&)*6(8*VMg=O5tt_9tboM3>Bab>6*{4JfAqvLml(# zETXA!>w=IDf=FO6>mhsDih!dO(*KIZKKoChP#Ecd0sMyfxnKQnulNt>ReS1vG))Bl2_0E1g3uUoC{|?o?plKa zlS*Kw=vs4vt2X*w#c0A9A&kQ*%ontyvluddCruHp7+o>aCk6R%;zhbFr!79+!?P5< z`Up5yAVxy&!(2f;oa4nxSj{nJEY)`C;KCJxa7NHJ!RPw)c3nH&*p)HwMQp!lOPijM ziiC;3k$s648H|Q$#%V0<-*=zAzItKh(3@@@s^}wDG1yYWAb}uC9YLv|ojC)&3bBjm zURLkGq5~ZO6pG_?9mLh+hr5Gu2Ux_iWoSyLWkEAKNitVZ>R*!E#NL{~;*707GJWcF zO-b!QnKa&onB9OoaSj!l3vX?Kl7ic^UWJ+x7#`4W=JriBcI%YIe>2VmE;^2chtlzI zZm*F;z2b1>P9D+>G+ua*7(bY|_=YinyyzQ!9$ zS;X$Nz46S=lc%43V*JCmpL}ipmXvPd^}~q-f|#(2@BtxTEEo&G^MmeW9;(#ZgW2h| zhw991@D)Q$1SM$nh*wFUN+!y6yKzT1IH6%YPMg#@=uG5U1Cp);Uh4wu$eadickSitTnO8~9gWAl&(QVDkk z?LNZjXr3r{s|1cc5pi8$2cXtF7_E!LWnJNLAfAwg-DS>(c|`>6*>Nvu%BDibgg|E= zG*7>J8aWg)w+-sUCqYpiAPs^Ap0xpwUPEFwh*$3V3on}(NJx-2ZzKRk2zAMhQsNH! zWD5wQTOa!dCYU!88&|~YT`Ak(?NyrXskFWD zKu)-sVGStSF4AnlYOJD<4)vY0z@G#=^YdNu7sY>uNmYk1Zo*~v`#<@%5C6G5V!}rF z9|AIBP4?hF@5J67%}*lufcUpcxc_y8|D%EbUaiFNpQ+&Bg*#Wtx%FTC6EFMnf0st*KMn-qLl?gP*i%y%FJI3VE4YOt zo3}o4;p^X6`p#3oIcb0LAAdf(@PEGcOLM>foxk|qzwz~n-~CwTjVnKQ;_P!j`+pDJ z+o<9@4FWy5Jcd>LWmHX{Bhr zl`YKVm*2dQE97!kK3BN3T(n*+7IJS~xcK75rCg!#y^GVSKlpcH#_xUg_rCJ%*+2QpSHAM`4}Sm4pZa$4@eh3cXTJ5-Kf2Rs zY<}RoANeAOFQ8>R{B&I~oE#{GYdmGS|CjscV_L`xk@QtDm&FedDbeG=5ns>R;^hZhQWjNJ4pWT zp#PU(F&N4JgMs-_rFYQ({}lTFk01ZUp_&_2dY3_x8IpV;01|Gn@3>p%Y7 zKiK~KPbNS8Z@>0)->QA_M-Mu0z5hi1`ycw%V;R|~ljDFu5C7W-RP6RSz(eW(R&J#K z?-Ra{QY?o5Os-^C%GO*yJ6p{=1=lW>W~)VK4%cza<(+)K;KIM_%pR=Af%<>{`7hZ2 zizEI2U|>E}>D}f3fBNSy{nsCU%hg%io^!L+oK-DZg=!@?Tg~RO zRXbnK&Q|hsW&40`qCxUMpDo7yza<2WjO70#;B~)MyY~N_h0Es{3cO$Y|8d5Def=MP z=F^}0o1g#W=f3dCU;Rg)*?8Rl!gE*4ub%q$Z2CmAdFj*po$aVx90EQ3UwQ|{|BUQE zaa0ZEcMSg-r(DTZ>{4Z>Qk8%U-7vF&2_l?4`RV*&eCHZQ-xzb+xeKlvwr`NV(wz|a5jpF;DqbCsE_UC5TL@@yer1oPU}StsX|4`?PFB>#8v|3b`+uqHVMGIDjT%IWu5O`R^=P(8n zZ}UB#|KWOrxwv>GMUWu#bZ{dYQb|5l&4ubddsSvZ#Ty$d5%) z6ljY+&pGeCb7zL+kQ7I2uZFNSGxxrq=RN1V=j%OhJaKH~e@Xlma=Byt#9#lXJWD0B z`BXBUOlRbMA(PG>%Z+?w=%CYTJ56JrxGR}jXmU^7;L~X!zFuvQx+j9>pzB}ArN`?( z>V-l3T>nJ1UhziQ*TL66l`JI3^gqHS37QVpKjA(o)jDN2cyn}Lxc>QUwx93&zmu7K z@>nnbPo*=N!m;Gh4Pp0|o^k$v+<*a?S}Rtm*4&nf#k}^0+vFFE53x$q+ngUjlgM#% zxmrh!_J-3o+(X-DtJ>Z$Q^%)_({xSA+uU^P?N%HmU-O#AF^#78wp(hOO|R9iZJTP{ zEX<<1Ybsu?=51B$>t@UA)XJvlqV&4wl}*cS8?S;E>|jj8X}8^Gy=A65b=w1**fiCO z+4eeS%c-|b+cTY(E6-=AK{|drkieM&fuR`V#9UHt;D8Nt&fRh^l<;U^dR)Vd~Nadwb3zwq4b|i_0xYU zT}X}T|0y6zrJT>Fa-~eJlrNXk>57{ymn%*-oi1fdZoW_{CS5mo`gCQ?6Q06i{1Cbj z`X~P%Vc(?xgX#a!|5B;3{y)Ms@tY3TU-y6i@sWNWV*OKv!dU-5(nZ*%vA*>e`EQq^ zhkksB{U6)^SpPpXzzh)m2x3FN%zzaQFX8M9wQ(IMpj z<+@?m(Hz4FTO0PnEko8WS1Z+$)2@2sn55xLd8iUX|J8cgeQ>Di`>I6z*^~a^>+g&I zOC@<;WM8d6VWDSy|3ANYZ2=H`9{)f0_QdzRzf!#W8~*39=RWo6=l=P^uOB=1bH9D; z*rz_y|BwCE#CQMt=icl5O#Jd+eD5!pfAX9EeY)~b{`}Zy7(g6vKfq~{m1`F+puN9& zEM=0B|1BmC!0#i_OM3+kg&t3iM4G|EmJX98@o zTZqDMqV2k~$ z_1BMl+{5TUiCCY${U1)p{CNLA@|75Du+P#ySYZ6*M;Zb`{~MlncSNy21Rm-Cp!R=M z7~B7!0_JtsKhfy4HezirR@!h&cfC$K-V&ePlMiCZ`saAovycDhas`|hee$k{xA2VX zf5%@)6sz?_Ya@d5k>~0-XeoIF>2M;g(~-56FV9Yak=yuqMus z5`A#k6V0q_R9o5rKCAWn-dz}TirXv|Yq}-3df$yToYLLt)Qs8MaO(!Oo32x8<20$q zQfPy%8K+)0zDx=6(rvo+k~`g+;n~qDN_U0USt%=x@kA+Wqmg#A$`h(M4ywcFt76ls zmp1g+Du!3;G|?*0quQAn&Ws(SFq*LAjhEf}_JWpuRa;P{snZnml)3LTt4^`z8nnq% zt8EXJwcEvdtBtdD3Y%BAP1&tDomzX=v^p3F&c$-7s73+01xVp2D+jveHSdb6nNYR^ z!o*>zLCc<&X+kV-yCFE34X1^|=%iY)@G?BWD=h|dMFZj4sx$nIu0;_CfQR%CYhAk} z@Wn7~qt@&v2YR>V0I*ITaP+jiG+KAJ0BX({6Kz);-Kprh#Cwqkb#WM2pn|U+l#*r@ zY6!t&p@;^G?uK)}>UElDfOK0f3h8XAuoPXKECzh4j6RU_$l=P}s%VCQ(}T=}dc&*Y zBr$X$Ossz2tyh60JqAf1)m_DOizhq}t!JNs({dhqYojXYhEZ1={<>M9aqx;vSpIeN zD1il>vEXZYE}}~_KbazVF1Ag{A&L5JJErc{V_$Ha9tYm;2(Bv{$v6bFrhO7Cdu|J@ zw7gjo+$Itlc(s5PYlUG)Z7ieO>9XzkS7TkNu(*9R}CFDInqbZv0}UvfHXQfjXQn zNQBITkB5LZgo-Q;Bn;4%gcSy82$;IUJiBh0qMlY4&d4dYh*aCA*{KWv0|zU|geAs0 zCblW@AplV-Dl-C9%S24ixSO3C2Tz_^aY0C!0glj<0iY#tHV&~#G{zbBGG<#6`}c80 zC!f=T;VG`Qw><5YdOPupCHluw<-%hbfW!!}!Jyd(ws$NM5Dyhvz*UR{bLB5z0??7^ zJHZ{rR;(eg8ochV>o)8PnVMH}YQY-YY4q?gnP!=Mk!T0vtL_hOMBS5;XwkfFhf2%xlJwyP(BoLVAHVD5?br(|Q4LgVWS%KHV)~-OEHV61H@y zytvB<V(`?k~Qtt+_{I2f>24AL`B*?}a0$7G0LtWF7rqcRr1wmoay_&TPvgVbt5bx{siRpb zz|;_cwGjcN>X8ZZqUenV7l#HX#aCFPfRp8Dq*4tvPn-&JTiokZu{H19L%8XicI#E@ z1EghJ?O-2w*0Cs-k!ep>+!1VSdYyHUa|i*w3KCENG{*3F{Tg=XH3&yEvjI_FHc(dQ_0;0ten$u`TrR&5z_w@o=2wjbpbm>|0^Kx zTfhF75r1K~iP3!<>wi}NBb(Y?0W4yFziT>mrw-Bs&xQ1k^J2O`L8@>msc{cjfOvtI z*b1U#KjD2Zsyn^YT~H>%s}f;h7RuOQ9#%RhpJDtv8@w6p|O5jTGzriMGy{B3Mkk9695g<^B0}}M>9%Gi@m5K;|;h-P;ys<*;frDuqsqEw1{RrjdJi*1pST{bTqAE8-Vz#!>PrS462Q5S-0P(C^n zDLGJ$qm!YEQR-vS8+DROImC^lC(I)gMHP$kCr+F&S8X1;P@6`!63EKRsG)dbFX%;! zi8E&+Zp$h8w6;Jix#8MVw+1~-D*BP6De%POaemO&0$mb@Q#lL6?Mi`)M?mO!ga!u- z%^q2OKt{(z+D$WcD;YcO#45L^%v5q}MyWh%`;Nk;9-U;1AtKOWtqGQD*Adkc!Y?dJ z2dYNfpN?t<7T`Vw&iWlns{(rj5Fm#bcc{dPS)D6@J^>qOJ=P;L;9+aSYqr;HWAl>_ z?YHB{yKOv1F=1fQ$$s*Sninuj#vz0zIT2dl6=>7_z0R7+o?fvmEipq_?hEu$CfMCW z?ed3PM(~1}qFBSd5M0T@n#5WLYa#Xo3Pj-~guu^+vkqa&=bmUw8QTOxDlkK!R1e&A zc@%CIOVCpvDD(xM4+X?|3K$d|bn`yIjQ1QRy4E4~uWNY00^08xrFrp1(P^=9!2>`8 z*Q8y1bJhgfs7arFF@;&{UKL(|6~KMUuYx?22s&}f+ce;{kZE#<6}!R^((MDlAk@gt zK%lDIkKf@q_6EoV#3G%|IuSW*Z)uZ&w|(3ZdTVf+Ge^mV{k${Kg)klnKwH1s%I9= zMF%P&TsSC8F)P}|nW%a&z^bTFzD@69(S=xqLTU&iVnx-#+A?K2J1IBA;_|jvRQEld zYG@GNR!CTA-B&ATZt!I$T#;~X)o3?$fsvkWt0Gy{i3bIxSiuV8n)qRWvf8I{d6 zorjwZ=;Y$6l*fE8v(sn-yS2l^szAkfHSPw+FmH=-6kAjp0=RgMYN=~>5cd&-ajhtX zA#J)%Iw??Rs_s5$Px-QgKylq|3+U_sXR$^l2mW;xDL6EVzR*0}@VFpOfueCRa9`qT zbc!|E1&noPz@=lK&9m1CS6e}%mJ4jO4EBW-bev*O+M4v+oX&hy1b>yRXxdQyMBlwd z>}hRp7QI@v1pZVJh49eY0q`M2!Lo_B zQUU}v9mITkCV{AQ>WUcrazp?Iv=LyzqZXaC z^L+HE*L}YP?aT(qVyNnA%lG<(!VPpJppcyha|S?_#1j+(;cKk6<3W6a zT|qTdiD--0K!1iY!Q3lxJFrVg*hgHvVTiZz=PofNUAiKIzl`On(8WrH50uKh^+Z2d zM+3Y={}HcV+g@XZwNxK)0$AMf*JtvfC85XAD_hW!-wIVv?@}E`Z2PxEbsxIX$tzMc zgDu#EvJ6YEJh9*#ITSs)0)0~&M9vTZeVV~M6P;F5V&vd7uZd|2W8B;}x@zEMljp9z z{Z$p`v=bNo{+F?%kC1BRE)*l*sRb%ZUlPtN<(dSm{QQOS)=j?KD=rd?=(`k@= zCngeirhOyO$r+Pi`H5-lT=Qp5YKFfan^vP%ZBHjw>j@Y>WO%9D@``o}jvbbPO*0)d z+1KWD{KU+pHau}lTb@kI{B~?ReS1a%gC>X*(=P}cYC)&yg8rd&7&{9nJ+?)q>?C02 z|DI^ThtxLLSI%F!cFpjO%xq$!gP1qyEWrcXh9-JTM|DQ%Aa&b3k%zLidy1ltrd!!Op(kahheHK_~|mo{Uf)iJ2%O ze8t}qt-uiQd~C0E82r(&hWS+(q~^3*@QalZC?MQuE)p>(%rXKV`*#kCAlCC1wL_mZ z{x{J+$*&^d3Kq-6Q-z%bwkYi7(=Nm&cx^Pe0X!Wj0bjoa8r8NThwRpGz;{bfF)>j3 z2Esk~LzrdP=nmyor7m+XQ-?v_RndfTG3C&nhvMJ{2q|H>6YIN9vsT3@H?fZ(l&l2_ zQ9S#)1=t-6J*f9a9FBww_f$t3z#PM2VgBGS6j@GG1u{S!ZB|Q+{tUv@WKXa&U=W+^ z!LHNdU@%06JZTt=6?`-d48A>r?I!rBY!=)V{J9F31duIr*V^G=IEn@A(Z`}$1uHiu zTx>!&W0XuFY71DzQG+-oIGJ=1x;qQ}YnB0i!5(*)3) z;lHqw({|n`8X2uOx;0d0U=D!Zuyg6LxD z+Cg|OqQro%TD9SN%#Q`dyjBsfz1`JV&dS&AoC&{#{+9>x&uZ}1FBmRYlp!wkl(gl-#rVDZr%fgV>Zm^yfPR@^i;P4f z=Vmbl?q3U{r}Z)stQ}bSIfl-G5@9Z=J^+)qBuN{9C47}&m5FMdSEUZ(Lq#%e7!T0tdTxs5gF`uD_e z3Edr0DAQGQ5=I2PFJtE$2T(r+0{kI&5$6B6moqksAn+mlKbbBJaa9*h5jZb$xIfjJkAO3%b@w=j8AE5|1jQ;bv0r@YNNsZ6{Kl$noZ(+am51KBgCIkItwoLL>O2$M zyI5QL{Q35fK^5k4HFTmf`aoO2$LDLU0UTKWP0#0p%We6fw@o*F_00( z$%SG#O^49nP6JWPuC@&Sww$!<#x%xaEXk9OGQ(Da0-6?{ffPwMcm^vAqd?orgGB+|-7oz`4YtuvCeVnG>|H`Kz6O7}3 zhSTqpmH_>S{hN^pH*)ogoRRNaPLpvrE&a84bzyF9x#8B+XBdaDyS@mwg}V+@yK8P* zWJ2-jjdr`ynoA@qPRYfIiMzzUjZQIM^%6eLC6E(@Hdoq~NX89%j9@N95QiGf(GDWl zMOTy{V;r}U@O@v<@c9b3R0BRWymbt?mt?b>4pLwkn8~Sb)(~vbjgGtvpPd{Ihb@X{ z++c)aP6$auz|vrPuzy+pQkG01GMFH|5)H6fG(y#vrwYRE19fLeR$DZ#Hbt_iaP_#k z3dR8=BO<8KjF{@|*=c}-rJEiMe~jG3_wcE0nNN#inw-xgD`p?-@Y^iixO`9sc(3C& zw*yWPwT*K~5&>_WKg=E&4;Y6Dag*Nq(C#DfJ5NvAm4I8CkYCf8ljj6QyezxL&iXnggZ_K`#8Dh3@*CZuO>qXJjy z>+xeY_n4_iIEtQHb_~;#Qn3m{_bXCIkZcWe!O@gmP%Id;ZIMMiusp;|U{V#BsH112 z_(zc4pC1zj)S&rOSTa-t`DNCN%o`}IVNX`0{TQ>YgbrN1)D#EHEG6QJHWmH z^B(hd-g^)*XATTV9PLHcC__agC&O!!f`Ld5?6rmAg+*-JRBx4lLMDcghn%c>nMZz- zgayLMI?cKX&^vY(dqM0hfg3wphD26&r*S?+$BuRApQB)*KSw%<=VfWr)2k;IE?z#r zCIGhI7nf$uln)&fdmG_wbsfdXvh#P6<{gBDypy3Oa`iD$Sk#JE@iR!@I z2apL{vQ=r4lP!0ON_R^F8yLJp4YKP5045_`c~Ysg9i zMs=KxtRT$?(t04P4g6QwUaE+?&;e)?4{kV7*|nlr-#T-MU_@AK$_#kPHhAE>UfO0v ziTgmf9YT$el?6PnT$O0Ax$H;q=RZmrykvRJJO9l~!!X^D^JT%MgXBI)8(8|s6D z>cS0;xyXY9uPz1?cz%J&IHdt65~rYS1>%A*sqi)=F^ZrTQV%x-a23>>9t7vC!NER+ zFSv-VEJZbfX|R+KUUcyJs&p{X^TKw0@7^sSkHQ$36ds)Hk+Y1#LR1quk)q7JdFiF- z1mcu9=!siVc}n)wOK^Ddu%|h8NHeg%2y^CElz@)jo|VTTkjg8sjV5kIu@gxt%m1Rc zZ=>{M{3DT4fcb7rm5>jJNe0Ok8qQaxXq?-b<*QxxsSv=l`xvH{5PDu*vT#BvWvccu zeiMd1tH4rmN3<+6amKvlvxBqT`1Ug|;+3$>?!0#0Ec~Lo%*%g0I5lB7H?2fHC%P>u zN!cRg6NIqZjc9*7NZ702ouD((Xjm`E>4(5(UmOOW`Yr6U(@tITH3iko{1;8)&gw(# zBZp@3J4qCl(L;ie&Dr1kW#y4d4$KSDoB9qShCIP)Yh!;%oNP?$LpRbfU@9plk)^gwfNG@Naz z&z?)Lsq-9%RjMT@2|6`$ZX)v}Hs<@)atGc??zY$#1y6f~cMdih-Of!k7+#7 zGV8MJr~phy6VVw;*uE?XxezsQ#hhxChddL_BulJrmUn8st7u}S*>T%b91k|?S-wf1#(Y`FT$IWSY1u5 zCddyS>!Hwx0s&S0P!BIA!UICPcse&%vP}{~c3$CPoZ^$3kjC{t!4L>pY>*wYEEhAx zWOrxYf}_a*y|?{0naw7%gZAIC|L3V-RoDLmE7|VmTpuIE{|k}EOkgF+7Z#6tnaOpzVbE|GH|Lm>3(eUjV;WQ_vL$BWMYJPr ze-KP(d;_xBzjYB|RA7r-NzHWYw~5C5lv_bGsPDq-o~5-GVLuTzkKM{tH5<0IQ1nbv zb2V6dx&6Tdp24A3#&uCD5|^S>m60;AOryX3{OKOEq)E1cWdypD9WS8HKcZ>v6w5gvV4Bo$j=Qip4brautyRSe+egaf)uHof`qBl zbK#a@d5HM^u9Yajtszef7R}~YrWB@!W+6VZySZQh;u+bYt7&FnrLP1cs`L)p!%3bA zxvd9Vf6$$-G>lacAA0(^03$anO}!Xv(Nw3{gMMgRMAc0b%Rp;kQ&Gx0Dq>Ir-rvA! z(vrYj85ry7RAdtJEZ(T?I1_s&Tc>ic?jRv!?cnATT<>-YVG71gIl(XsN)rawYHy>Y zt|$`SwoCEwXuySNRYHjQ(fmb%I7AI5ybi0%g)8S4FR#6}c=>|HAznZtw|OMo1~S$u z(=h{le>yIojVRJ%eAE_pBa#_-pV?4lw^m39L)rl4Xfr;~KwF!4+laMB#r9?cBC?6W zJUJ+0ys3X$n>gjSz+A#6R$*RQ0YC9CDzYp29FFf{#Jshb^KT_!OW+rNR_IPw{IonN zhNI9zvN39G!5{)=anmx-aQpWBs6U#1*z62YzaeJLA~}G{BvdmR^cl!jb4K)yIcqj1 zkHFR?F;;$`y$Q%b#>ha7b@a3A@$=^LwM&vVID(AJVdww?#HDpKX?O0Ds3&SH zXd)an7J7KFsId_JWBZB8c`_TTc4Qo282#r5&;R6bDbzUr>nU)6{n9_Ql%esTP^UWE*Rmsw`}9$(kN$aB>&WQd?_&u4r*nh*e>OFy ze;<4!{p`{|2ypl9e-H8}%Ha5&=_7qK$2SNZlpOJ(M#+Oi(+G_^*l$&1<7q(LT%}nk zY#xRn%_XZyz6EVex>>YGdF|OmG7aAb1It$Lby%=;Ng7Sxd}YUK1JL1qllVP+9yCSV z5+Wsh6Pld|M2YTQU+joJV6{2QVLCrq+>L9ODR%5#7SO{y*$SJ0 zm%qU)o6I+H3$fMqcxIG*^O_Ldr5;SppPTJyMFhoWqh~e)))2%-*i`HuLO7k zS(qwS;^F9-FNd9nB~m$J%Ea7aF^k?@yc#vrR1!H-Y#q>xTP^0g(f-xk)@j!81?jM! zG%0sPkB9R7zTS()WY_i^W@=YX9%}zZ|E@cN z8zyfQgU8ezRAAyK89cMU2T+Oee6O~^1GhdX#7ZM~+P%iyOrZ{mFm`xY#94{zuyn}= zy2ofSYNj0%TL0wa#_iqzX@0WO{}I{%hVB1_^q~JQStyL<|B>$e+IV;W-&_CpZIfiQ zN4Mkqv7j9| z_Dk%oJSA`l#iEb!s9M0M!Hgv;q0~#YjwbZtZLl`kLQBG7ZO{=j)sUs$mlG!hX!N+b ziUe1fxLnW_9;<~HP9Pv{uUl*Q0JI~=q|~zr<`;B~#giyHuA!v#wH9TrD=w>$>qlcv zz3mcUCS-!O45tI;3#qSla7my>&P%o&xu}u=<&crp&1R>C^vK&wc{@i5L0yVeNO6Q> z@S)lOs1^hsNlfnJQs_hw#6ZX@ev%4!7I?7dI&K=|RmQj>5I80gxcvhc9EBqj;Kb&J zosn6+U@#I%RMrPe)CAXr&llGqdmaQLWPQc*Ei7NT0;Bkq>#vLNhF7@SN7t8cTwAy> zKbem3s=xWkOeE5C2Ft{%@uG9r#IT}M8q2Bylhd1tJ2)P+_nIc+aTGf~nV8`Q*f$Ef zDaa<7=o!uIB*!8tC*UBReMHXM%7s4-hNCv>p1VBaKQ3sCkP9eTyK$#euz; z@)l}NP*KNd6EMvico7jjck-l}N3j1+gEvTFf!+{ZDPR1AyVsXrwZZ^=&*B}?sjXbN zwzhcw!qN(}sREg>^-r}BW{UXCv#T@l6O)M^1ei0csYC;T{l_ItjtAJ9;CgM`uFAmv zW((3qwH{MXFq~TY6fj2r^r#GgT5g3=ivVw7>VY@O1cX4mLh7vW3A|wS3EU7&0jL)g zLTkj-lP7nA5SlxnKcjd5KWfXL?lD95|4b^M>bL)Av$zv}eExIv0>FOv|2+%bZl%a$OxAAqZ#An7@*$V8VTCj{ZiM2ePZ|4Tt9yq%cR}2*6xWN(P{(PNs>~164a^>fVu)#mn&O3r<1%{z?!41MMd_d+88_+~iJ z3!6NczTME_2KQP#+m0hd$c%@>Ej15|Nm&GEk@*F(D1Mam1=k^@&L__`VxdB>Is8pyv!$ig& zah)()M{d+6L=0%*G|3>FBn=+3xD*EIB^kTI`2#B@5*WuEsV+;3&SDigpdmVY=DJsN zny8yfLKqGnlCKB7P-Jf9dqU!qN#A}f}jq9NMJDQL3>d} zK+z($;a4vI#L8i5*Fkv@&SVCN#Ae%ZRQO$%UbihjIht0>qv2 z{c5XwVnkFR!8r&NsI#aYF&{4MSeOP0mP;VlW5_QKuk5!$B2`DMSOS?MT6mI;oS>@E z(tA#QtP?tp5H!A_g2okL4j2b|VsEfkVetg9B_;*NSQR`tR7l6bZ`yT4X;YR#tfy*Z z>DU+cx@#_$8wBp?y$iwO;)>|eZ)xKm4gi6W{HNQ|5!!wHVGNW1^7#S#Pd1w!%YQ!n zM)|p4`ERfI56D$}%6&9V`2GovEEYy+jW}d0vVC{0LB2`FH&b-2IsR1}gRWvUVT};h z;S}Tx+R<4I9>0^Mh*qqu80wRZd^GVQU6NCaPxtUFS+5=e#}33u$bFbAxDMxeu@ZK3 zj2T>}v`z52p5Csh(~VtO^IpjIi?-DCgj6I<{Eh5Ow8&sIOfybn;h^4q z?#9Z+d?O*qovIBh#m*Yl^A^W!icd`s@bO#5q(*F1)n` zLJDrnx(6}EH$0%-#GPAa;`WIY|4o#qaM5unJd}=y^LvdP;uVJ@ck&QtAo0R`#2BK; z1P_As$mIx4MZD`eWL$uf30hX;w$3!=)<>GpW@dGgV-ObxaW&Ww@;WvLViBFb^F$dcw+VTqt(?HAMu7#7I8XlZ!&S~_?cH;nEc$G7az^t zmeLKpemoUH5EHbB79i9T3&uRw`C)f5k4$emk-gl)MoMLA%EKHA06J2Dcf;j}mTG6)X;YLyAO=u@z zLD!j2j0Y##3b+T(S+-z`#fb?!Dd{O5(Q(>)j1d|RV<+Y7>vyXSVJ%oXMMN369iA#5 zs%nI7#FnN(&4LO!O(hVLIneRZ26$pN)3 zs1*)EXMn*RF0HNN>hdqKfZbEJ09v;v=D-c467CGzeT3oByin{`@f~}@;<~{0L9H5$ z%HnWYmpB}-CuCuFnX_TeaDn#hxEC}W~ zQnIJeE?XYL+uKJfLW}&TR)*H7R!2G{3mj6bw7=4ZQt$!j@u4GQ1 zI_2a}oz5kTr%I5+(wS_|bx)OwPAXR_y18t*+}FsEpIrYv`G0C=^i^*D_Gr&9y^e<6pQP*25(@{HSmApd{+J0E^H z`}t(HP?_ddaPd-pX)*KrUwh|b_T6`~xr<-Dn9t`k>3r(Sm3J>Ly^+mcS$ZRvTU`9p zH)dn+eKT?GXRqZy`=4KUH=6nbGj$_z`sF`f`gi~S8y|e|hu`?aufKBcdtd+h*MIgW ziZ>VE{)f;1y!)y3;zzkcA^Y=x@%?Xo@c!(3mp}Nqx4-}Vv5%(b{?@@TYFzrLAkahq zyxQrg8VzdD{)e&sF9gI9eeGHQ)Bz1U`yA6D)<2ccjMsmk;C-TEee0jdxXIIQ zG2^762dytgF*RlrMvro zI^`T}|37hY_Kgoue*WSmoYl`>$>*=+FD>TNZ(PbnV}Cz?^)rW?{xe^zhyHO7-zfeL#sAY||IaAaqQA|)^-mWY7%cjbwv)R1c-^$*hkYqQ>jbeKcM{ocfV4|W|ngAT)vWC%BB|=7t>2`T>0I1-nqD# z&o8AeWz)%}^u_GbV(t(A`~aH5FAr!6-?uu#m#?n=;+wzz+?TIi|7*=3Tu=Xp^gsHk zOX(l{;Mh<7$?Y5a#Eoa5SP%WD4`|rmCwvaa|I=gszfbr+NwL25Pn46XTsl`eUC0$G zX%}9fVi`%Q0V{~rv_hbisHe`VLdnfTV_ z?|g9SgU?_5_IFM$et7avpFg(wtRju{~j3x zxWe)FgZ9WeK*Pi6KM7xNDw)lvlIdhR!y_~)#0!r1|52`q-?rWmBgtrx@Cu1k{1@ZT zX95CiYdAaXJ;1^6wN1C}tgVe;)`!sl(z&t!X9O!T*yOyGITWl9T8Wth>}s2IJeQ3p$2-75VfaZdKS=-B3hHfS4m-M84W<87VSxTK s`LX_YbStoXOR3DMQ#kKa$Yoa^ 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 H2?GBg&l(pU literal 0 HcmV?d00001 diff --git a/gix-stash/tests/fixtures/generated-archives/make_pop_untracked_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_pop_untracked_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..e4d425c25458e49dbf4852b1111c8713f3d98a4e GIT binary patch literal 73216 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 qzoWyz&Ml=eJv;Qs@IkdD{@ literal 0 HcmV?d00001 diff --git a/gix-stash/tests/fixtures/generated-archives/make_push_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_push_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..b597bac883cf69dcc38250219963de6e29e3a915 GIT binary patch literal 62464 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�B91Vv!THCiVsZvzDzhk~ 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 file.txt +git add file.txt +git commit -q -m "initial commit" diff --git a/gix-stash/tests/fixtures/make_list_repo.sh b/gix-stash/tests/fixtures/make_list_repo.sh new file mode 100755 index 00000000000..49a65b8d6cf --- /dev/null +++ b/gix-stash/tests/fixtures/make_list_repo.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Initial commit so we have a HEAD. +echo "initial" > file.txt +git add file.txt +git commit -q -m "initial commit" + +# First stash. +echo "change one" > file.txt +git stash push -m "first stash" + +# Second stash. +echo "change two" > file.txt +git stash push -m "second stash" + +# Third stash. +echo "change three" > file.txt +git stash push -m "third stash" diff --git a/gix-stash/tests/fixtures/make_pop_conflict_repo.sh b/gix-stash/tests/fixtures/make_pop_conflict_repo.sh new file mode 100755 index 00000000000..d481d41bb23 --- /dev/null +++ b/gix-stash/tests/fixtures/make_pop_conflict_repo.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Base commit: file.txt = "content A" +echo "content A" > file.txt +git add file.txt +git commit -q -m "base commit" + +# Modify to "content B" and stash — stash records WIP=B on base=A. +echo "content B" > file.txt +git stash push -m "stash: content B" + +# Now modify HEAD to "content C" and commit. +# When we pop, base=A, ours=C, theirs=B → conflict. +echo "content C" > file.txt +git add file.txt +git commit -q -m "commit with content C" diff --git a/gix-stash/tests/fixtures/make_pop_repo.sh b/gix-stash/tests/fixtures/make_pop_repo.sh new file mode 100755 index 00000000000..3a4158be50b --- /dev/null +++ b/gix-stash/tests/fixtures/make_pop_repo.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Base commit. +echo "original content" > file.txt +git add file.txt +git commit -q -m "initial commit" + +# Make a change and stash it — leaves WT clean. +echo "stashed modification" > file.txt +git stash push -m "stash: stashed modification" diff --git a/gix-stash/tests/fixtures/make_pop_two_stashes_repo.sh b/gix-stash/tests/fixtures/make_pop_two_stashes_repo.sh new file mode 100755 index 00000000000..dce1130f54d --- /dev/null +++ b/gix-stash/tests/fixtures/make_pop_two_stashes_repo.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Base commit. +echo "original content" > file.txt +git add file.txt +git commit -q -m "initial commit" + +# First stash (becomes stash@{1} after the second push). +echo "older stash" > file.txt +git stash push -m "stash: older modification" + +# Second stash (newest, becomes stash@{0}). +echo "newer stash" > other.txt +git add other.txt +git stash push -m "stash: newer modification" diff --git a/gix-stash/tests/fixtures/make_pop_untracked_repo.sh b/gix-stash/tests/fixtures/make_pop_untracked_repo.sh new file mode 100755 index 00000000000..7a0b021c682 --- /dev/null +++ b/gix-stash/tests/fixtures/make_pop_untracked_repo.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Base commit with a tracked file. +echo "tracked content" > tracked.txt +git add tracked.txt +git commit -q -m "initial commit" + +# Create an untracked file and stash including untracked. +echo "untracked content" > untracked.txt +git stash push --include-untracked -m "stash: with untracked file" diff --git a/gix-stash/tests/fixtures/make_push_repo.sh b/gix-stash/tests/fixtures/make_push_repo.sh new file mode 100755 index 00000000000..127242f5baa --- /dev/null +++ b/gix-stash/tests/fixtures/make_push_repo.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q +git config user.name "Test User" +git config user.email "test@example.com" + +# Commit a tracked file at HEAD. +echo "original content" > tracked.txt +git add tracked.txt +git commit -q -m "initial commit" diff --git a/gix-stash/tests/stash/list.rs b/gix-stash/tests/stash/list.rs new file mode 100644 index 00000000000..098caf0f6ca --- /dev/null +++ b/gix-stash/tests/stash/list.rs @@ -0,0 +1,73 @@ +use crate::{git_dir, open_ref_store}; + +/// Fixture: three stashes pushed. +fn list_repo() -> gix_testtools::Result { + gix_testtools::scripted_fixture_read_only("make_list_repo.sh") +} + +/// Fixture: repo with no stashes at all. +fn list_empty_repo() -> gix_testtools::Result { + gix_testtools::scripted_fixture_read_only("make_list_empty_repo.sh") +} + +#[test] +fn lists_entries_newest_first() -> gix_testtools::Result { + let worktree = list_repo()?; + let refs = open_ref_store(&git_dir(&worktree)); + + let outcome = gix_stash::list(&refs)?; + + assert_eq!(outcome.entries.len(), 3, "expected 3 stash entries"); + + // Entries are newest-first: index 0 = most recently pushed ("third stash"). + assert_eq!(outcome.entries[0].index, 0); + assert_eq!(outcome.entries[1].index, 1); + assert_eq!(outcome.entries[2].index, 2); + + // The messages should follow the stack order (newest first). + // git stash push -m "third stash" was the last push. + let msg0 = outcome.entries[0].message.to_string(); + let msg2 = outcome.entries[2].message.to_string(); + assert!( + msg0.contains("third"), + "entries[0] should contain 'third', got: {msg0:?}" + ); + assert!( + msg2.contains("first"), + "entries[2] should contain 'first', got: {msg2:?}" + ); + + Ok(()) +} + +#[test] +fn empty_repo_returns_empty_outcome() -> gix_testtools::Result { + let worktree = list_empty_repo()?; + let refs = open_ref_store(&git_dir(&worktree)); + + let outcome = gix_stash::list(&refs)?; + + assert!( + outcome.entries.is_empty(), + "no stashes should produce an empty outcome, not an error" + ); + Ok(()) +} + +#[test] +fn time_seconds_is_positive() -> gix_testtools::Result { + let worktree = list_repo()?; + let refs = open_ref_store(&git_dir(&worktree)); + + let outcome = gix_stash::list(&refs)?; + + for entry in &outcome.entries { + assert!( + entry.time_seconds > 0, + "stash entry {} has non-positive time: {}", + entry.index, + entry.time_seconds + ); + } + Ok(()) +} diff --git a/gix-stash/tests/stash/main.rs b/gix-stash/tests/stash/main.rs new file mode 100644 index 00000000000..c0b330b28b3 --- /dev/null +++ b/gix-stash/tests/stash/main.rs @@ -0,0 +1,135 @@ +mod list; +mod pop; +mod push; + +use std::path::{Path, PathBuf}; + +use gix_ref::store::WriteReflog; + +/// Open a read-enabled `gix_ref::file::Store` pointed at `git_dir`. +pub(crate) fn open_ref_store(git_dir: &Path) -> gix_ref::file::Store { + let object_hash = gix_testtools::object_hash(); + gix_ref::file::Store::at( + git_dir.to_owned(), + gix_ref::store::init::Options { + write_reflog: WriteReflog::Normal, + object_hash, + ..Default::default() + }, + ) +} + +/// Open a `gix_odb::HandleArc` pointed at `/objects`. +/// +/// Using `HandleArc` (backed by `Arc`) ensures the handle is `Send + Clone`, +/// which is required by the `push` and `pop` generic bounds. +pub(crate) fn open_odb(git_dir: &Path) -> gix_testtools::Result { + let object_hash = gix_testtools::object_hash(); + let odb = gix_odb::at_opts( + git_dir.join("objects"), + Vec::new(), + gix_odb::store::init::Options { + object_hash, + ..Default::default() + }, + )? + .into_arc()?; + Ok(odb) +} + +/// Return a fixed `gix_actor::Signature` suitable for test commits. +pub(crate) fn test_committer() -> gix_actor::Signature { + gix_actor::Signature { + name: "Test User".into(), + email: "test@example.com".into(), + time: gix_date::Time::new(1_700_000_000, 0), + } +} + +/// Resolve the OID that `HEAD` points to in the repo rooted at `worktree_path`. +pub(crate) fn head_commit_oid( + _worktree_path: &Path, + refs: &gix_ref::file::Store, + odb: &impl gix_object::FindExt, +) -> gix_testtools::Result { + use gix_ref::file::ReferenceExt; + let mut reference = refs.find("HEAD")?; + Ok(reference.peel_to_id(refs, odb)?) +} + +/// Return the tree OID for the given commit OID. +pub(crate) fn commit_tree( + odb: &impl gix_object::FindExt, + commit_oid: gix_hash::ObjectId, +) -> gix_testtools::Result { + let mut buf = Vec::new(); + let commit = odb.find_commit(&commit_oid, &mut buf)?; + Ok(commit.tree()) +} + +/// Read a blob from a tree by name and return its content. +/// +/// Only works for top-level file names in the tree. +pub(crate) fn blob_content_in_tree( + odb: &impl gix_object::FindExt, + tree_oid: gix_hash::ObjectId, + filename: &[u8], +) -> gix_testtools::Result> { + use bstr::ByteSlice; + let mut buf = Vec::new(); + let tree = odb.find_tree(&tree_oid, &mut buf)?; + for entry in &tree.entries { + if entry.filename.as_bstr() == filename { + let mut blob_buf = Vec::new(); + let blob = odb.find_blob(entry.oid, &mut blob_buf)?; + return Ok(blob.data.to_owned()); + } + } + Err(format!("file {filename:?} not found in tree {tree_oid}").into()) +} + +/// Build a minimal `gix_diff::blob::Platform` for use with pop's `Context`. +pub(crate) fn new_diff_cache(worktree: &Path) -> gix_diff::blob::Platform { + gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new(Default::default(), Default::default(), Vec::new(), Default::default()), + Default::default(), + gix_worktree::Stack::new( + worktree, + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ), + ) +} + +/// Build a `gix_merge::blob::Platform` for use with pop's `Context`. +pub(crate) fn new_blob_merge_platform(worktree: &Path) -> gix_merge::blob::Platform { + let attributes = gix_worktree::Stack::new( + worktree, + gix_worktree::stack::State::AttributesStack(gix_worktree::stack::state::Attributes::default()), + Default::default(), + Vec::new(), + Vec::new(), + ); + let filter = gix_merge::blob::Pipeline::new( + Default::default(), + gix_filter::Pipeline::default(), + gix_merge::blob::pipeline::Options { + large_file_threshold_bytes: 0, + }, + ); + gix_merge::blob::Platform::new( + filter, + gix_merge::blob::pipeline::Mode::ToGit, + attributes, + vec![], + Default::default(), + ) +} + +/// Return `.git` directory for a worktree path. +pub(crate) fn git_dir(worktree_path: &Path) -> PathBuf { + worktree_path.join(".git") +} diff --git a/gix-stash/tests/stash/pop.rs b/gix-stash/tests/stash/pop.rs new file mode 100644 index 00000000000..6812fd52f6f --- /dev/null +++ b/gix-stash/tests/stash/pop.rs @@ -0,0 +1,309 @@ +use gix_date::parse::TimeBuf; + +use crate::{ + commit_tree, git_dir, head_commit_oid, new_blob_merge_platform, new_diff_cache, open_odb, open_ref_store, + test_committer, +}; + +fn pop_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_pop_repo.sh") +} + +fn pop_two_stashes_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_pop_two_stashes_repo.sh") +} + +fn pop_untracked_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_pop_untracked_repo.sh") +} + +fn pop_conflict_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_pop_conflict_repo.sh") +} + +fn empty_repo_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_list_empty_repo.sh") +} + +#[test] +fn pop_applies_stash_to_clean_wt() -> gix_testtools::Result { + let tmp = pop_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let outcome = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_tree, + )?; + + assert!(!outcome.had_conflicts, "pop of a clean stash should have no conflicts"); + assert!( + outcome.new_top.is_none(), + "after popping the only stash, new_top must be None" + ); + + // refs/stash must be gone. + assert!( + refs.try_find("refs/stash")?.is_none(), + "refs/stash must be deleted after popping the last entry" + ); + + // The working tree file should now contain the stashed modification. + let content = std::fs::read_to_string(worktree.join("file.txt"))?; + assert_eq!( + content.trim(), + "stashed modification", + "pop must restore the stashed working tree content" + ); + + Ok(()) +} + +#[test] +fn pop_drops_only_top_with_multiple_stashes() -> gix_testtools::Result { + let tmp = pop_two_stashes_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + // Record the old stash tip before popping. + let old_stash_tip = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash must have an OID target") + .to_owned(); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let outcome = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_tree, + )?; + + assert_eq!(outcome.applied, old_stash_tip, "applied must be the old tip"); + + // refs/stash must still exist (there is one more entry). + assert!( + refs.try_find("refs/stash")?.is_some(), + "refs/stash must still exist after popping from a multi-entry stack" + ); + + // The new top must be Some and different from the old tip. + let new_top = outcome.new_top.expect("new_top must be Some when more stashes remain"); + assert_ne!(new_top, old_stash_tip, "new_top must differ from the popped entry"); + + // refs/stash must point at new_top. + let current_stash = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash OID") + .to_owned(); + assert_eq!(current_stash, new_top); + + Ok(()) +} + +#[test] +fn pop_returns_no_stash_when_unborn() -> gix_testtools::Result { + let tmp = empty_repo_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let result = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: Default::default(), + }, + head_tree, + ); + + match result { + Err(gix_stash::PopError::NoStash) => {} + other => { + return Err(format!("expected Err(NoStash) for a repo with no stash, got: {other:?}").into()); + } + } + + Ok(()) +} + +#[test] +fn pop_restores_untracked_when_present() -> gix_testtools::Result { + let tmp = pop_untracked_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + // After the fixture runs `git stash --include-untracked`, the untracked + // file should NOT be on disk. + assert!( + !worktree.join("untracked.txt").exists(), + "fixture post-condition: untracked.txt should be removed by git stash" + ); + + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let outcome = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_tree, + )?; + + assert!( + !outcome.had_conflicts, + "pop of untracked-only stash should have no conflicts" + ); + + // untracked.txt must be restored. + let content = std::fs::read_to_string(worktree.join("untracked.txt"))?; + assert_eq!( + content.trim(), + "untracked content", + "pop must restore the untracked file from parent[2]" + ); + + Ok(()) +} + +#[test] +fn pop_conflicts_leave_ref_intact() -> gix_testtools::Result { + let tmp = pop_conflict_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + // HEAD is now the "content C" commit; stash has "content B" based on "content A". + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + let stash_tip_before = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash OID") + .to_owned(); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let outcome = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_tree, + )?; + + assert!( + outcome.had_conflicts, + "merging stash B onto HEAD C (base A) must produce conflicts" + ); + + // refs/stash must still point at the same OID (not dropped on conflict). + let stash_tip_after = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash OID") + .to_owned(); + assert_eq!( + stash_tip_after, stash_tip_before, + "refs/stash must not be updated on a conflicted pop" + ); + + Ok(()) +} diff --git a/gix-stash/tests/stash/push.rs b/gix-stash/tests/stash/push.rs new file mode 100644 index 00000000000..caa8cae6c3a --- /dev/null +++ b/gix-stash/tests/stash/push.rs @@ -0,0 +1,339 @@ +use std::path::Path; + +use gix_date::parse::TimeBuf; + +use crate::{blob_content_in_tree, commit_tree, git_dir, head_commit_oid, open_odb, open_ref_store, test_committer}; + +/// Open the push fixture (writable copy), return the worktree `TempDir`. +fn push_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_push_repo.sh") +} + +/// Convenience: open all handles from a worktree path. +struct Repo { + worktree: std::path::PathBuf, + refs: gix_ref::file::Store, + odb: gix_odb::HandleArc, +} + +impl Repo { + fn open(worktree: &Path) -> gix_testtools::Result { + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + Ok(Self { + worktree: worktree.to_owned(), + refs, + odb, + }) + } + + /// Load the index from `.git/index`. + fn load_index(&self) -> gix_testtools::Result { + let object_hash = gix_testtools::object_hash(); + Ok(gix_index::File::at( + git_dir(&self.worktree).join("index"), + object_hash, + false, + Default::default(), + )?) + } +} + +#[test] +fn push_captures_unstaged_modification() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + let repo = Repo::open(worktree)?; + + // Modify tracked.txt on disk — do NOT stage it. + let tracked_path = worktree.join("tracked.txt"); + std::fs::write(&tracked_path, "modified content\n")?; + + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions::default(), + )?; + + // refs/stash must now exist and point at the stash commit. + let stash_ref = repo.refs.find("refs/stash")?; + let stash_oid = stash_ref.target.try_id().expect("stash ref must be an OID").to_owned(); + assert_eq!(stash_oid, outcome.stash); + + // The stash commit's tree must contain tracked.txt with the modified content. + let stash_tree = commit_tree(&repo.odb, stash_oid)?; + let content = blob_content_in_tree(&repo.odb, stash_tree, b"tracked.txt")?; + assert_eq!( + content, b"modified content\n", + "stash tree should capture the modified WT content" + ); + + // After push, the WT file should be reset to HEAD content. + let wt_content = std::fs::read(&tracked_path)?; + assert_eq!(wt_content, b"original content\n", "push must reset WT to HEAD content"); + + Ok(()) +} + +#[test] +fn push_captures_staged_change() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + + // Stage a modification to tracked.txt via git add. + std::fs::write(worktree.join("tracked.txt"), "staged content\n")?; + std::process::Command::new("git") + .args(["add", "tracked.txt"]) + .current_dir(worktree) + .status()?; + + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions::default(), + )?; + + // parent[1] of the stash commit is the index-state commit. + use gix_object::FindExt; + let mut buf = Vec::new(); + let stash_commit = repo.odb.find_commit(&outcome.stash, &mut buf)?; + let index_commit_oid = stash_commit + .parents() + .nth(1) + .expect("stash commit must have parent[1] (index-state)"); + + let index_tree = commit_tree(&repo.odb, index_commit_oid)?; + let content = blob_content_in_tree(&repo.odb, index_tree, b"tracked.txt")?; + assert_eq!( + content, b"staged content\n", + "index-state commit tree (parent[1]) must reflect what was staged" + ); + + Ok(()) +} + +/// Tests that `push` on a repository with no local changes returns `Err(NoLocalChanges)`. +/// +/// KNOWN BUG (as of commit a882282e5): the `NoLocalChanges` guard only fires when +/// `index.entries().is_empty()`, which is never true for a repo that has committed +/// files. A clean working tree therefore falls through and produces a stash commit +/// that is identical to HEAD. The correct behaviour would be to compare the WT + +/// index against HEAD and bail out when there is nothing to save. +/// +/// This test records the *expected* correct behaviour. If it passes, the bug +/// has been fixed; if it fails, the bug is still present. +#[test] +fn push_returns_no_local_changes_on_clean_wt() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let result = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions::default(), + ); + + match result { + Err(gix_stash::PushError::NoLocalChanges) => { + // Correct — the bug has been fixed. + } + Ok(_) => { + // BUG: the guard `if index.entries().is_empty() && !options.include_untracked` + // fires only for an empty index. A repo with committed files has a non-empty + // index even on a clean WT, so the check never trips. + return Err("BUG(gix-stash push): NoLocalChanges guard fires only on empty index, \ + not on clean-WT repos with committed files. \ + push succeeded on a clean working tree and produced a no-op stash." + .into()); + } + Err(e) => return Err(e.into()), + } + + Ok(()) +} + +#[test] +fn push_includes_untracked_when_flag_set() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + + // Create an untracked file (not staged, not committed). + std::fs::write(worktree.join("new.txt"), "untracked content\n")?; + + // Also make a tracked change so the index isn't fully clean and the + // `NoLocalChanges` guard does not trip (the guard is `index.is_empty()` + // today, but we want the test to work after a fix too). + std::fs::write(worktree.join("tracked.txt"), "modified for untracked test\n")?; + + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions { + include_untracked: true, + ..Default::default() + }, + )?; + + // parent[2] must exist when include_untracked is set and untracked files were found. + let untracked_commit_oid = outcome + .untracked_commit + .expect("untracked_commit must be Some when include_untracked=true and untracked files exist"); + + // The untracked commit's tree must contain new.txt. + let untracked_tree = commit_tree(&repo.odb, untracked_commit_oid)?; + let content = blob_content_in_tree(&repo.odb, untracked_tree, b"new.txt")?; + assert_eq!( + content, b"untracked content\n", + "untracked-files commit tree must contain new.txt" + ); + + // new.txt should no longer be on disk after push. + assert!( + !worktree.join("new.txt").exists(), + "untracked file must be removed from disk after push with include_untracked=true" + ); + + Ok(()) +} + +#[test] +fn push_leaves_untracked_alone_without_flag() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + + // Create an untracked file. + std::fs::write(worktree.join("new.txt"), "untracked content\n")?; + // Make a tracked change so push proceeds. + std::fs::write(worktree.join("tracked.txt"), "modified for flag test\n")?; + + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions { + include_untracked: false, + ..Default::default() + }, + )?; + + // No untracked commit should be created. + assert!( + outcome.untracked_commit.is_none(), + "untracked_commit must be None when include_untracked=false" + ); + + // new.txt must still be on disk. + assert!( + worktree.join("new.txt").exists(), + "untracked file must remain on disk when include_untracked=false" + ); + + Ok(()) +} + +/// Tests that `push` on a repo with no commits returns `Err(EmptyRepository)`. +/// +/// The `push` plumbing function requires the caller to supply pre-resolved +/// `head_commit` and `head_tree` OIDs. Resolving HEAD on an empty repository +/// fails before `push` is reached. The `EmptyRepository` variant is reserved +/// for the porcelain (`gix`) layer. This test documents that limitation. +#[test] +fn push_returns_empty_repository_on_no_commits() -> gix_testtools::Result { + // No-op: the plumbing API cannot represent the no-commits scenario because + // the caller must supply valid `head_commit`/`head_tree` OIDs up front. + // See `gix::Repository::stash_push` for the porcelain-level guard. + Ok(()) +} From c38fb6dffa3d647bfb6a8293576100d469f5949c Mon Sep 17 00:00:00 2001 From: mxaddict Date: Sun, 17 May 2026 23:21:10 +0800 Subject: [PATCH 08/11] docs(gix-stash): add initial CHANGELOG.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manual initial entry per gitoxide convention — cargo-smart-release will auto-augment future releases with commit-statistics sections. Documents the MVP scope (list / push / pop), behaviour, errors, and known limitations carried over from the implementation TODOs. Co-authored-by: Claude --- gix-stash/CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 gix-stash/CHANGELOG.md diff --git a/gix-stash/CHANGELOG.md b/gix-stash/CHANGELOG.md new file mode 100644 index 00000000000..37034477253 --- /dev/null +++ b/gix-stash/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.0.0 (Unreleased) + +The initial release. + +### New Features + +- `list(refs)` walks the `refs/stash` reflog and returns every stash entry + newest-first. Returns an empty `Outcome` when `refs/stash` is unborn, matching + `git stash list` output on a stash-free repo. + +- `push(ctx, head_commit, head_tree, head_branch, options)` captures the current + working tree as a new stash commit at `refs/stash`: + - parent[0] is the commit `HEAD` points at, parent[1] is a commit whose tree + matches the index at stash time, and parent[2] (optional, when + `Options::include_untracked` is set) carries the untracked files. + - The stash commit's own tree reflects the **working-tree** state for tracked + files (not the index) so unstaged modifications are captured. + - After the ref transaction the worktree is reset to `HEAD` via + `gix_worktree_state::checkout`; untracked files captured into parent[2] are + removed from disk. + - Errors: `EmptyRepository`, `NoLocalChanges`, ODB write failures, ref + transaction failures, worktree I/O. + +- `pop(ctx, head_tree, options)` applies the latest stash to the working tree + and drops the entry: + - Performs a 3-way merge via `gix_merge::tree` with base = stash parent[0] + tree, ours = current `head_tree`, theirs = stash WIP tree. + - On a clean merge: writes the merged tree to the worktree via + `gix_worktree_state::checkout`, restores parent[2] untracked files when + present, then drops `refs/stash` (deletes the ref when the stack is + exhausted, otherwise advances it to the next reflog entry). + - On conflict: `Outcome::had_conflicts` is set, conflict markers are written + to the worktree, and `refs/stash` is left untouched (matching + `git stash pop` semantics). + - Errors: `NoStash`, merge failures, ODB I/O, ref transaction failures. + +### Known limitations + +- The default `checkout_options::filters` and `checkout_options::attributes` are + empty. Callers wiring this crate into porcelain (e.g. `gix` at the + `Repository` level) must populate them so smudge/clean filters and + gitattributes run during the worktree write. +- Tracked entries that have been **deleted from the worktree** are stored with + their index OID rather than recorded as a deletion in the WIP tree. A pop of + such a stash will not restore the deletion. +- The index produced by `pop` after a clean merge does not preserve stat data or + timestamps from the merged tree. +- Operations that aren't `push` / `pop` / `list` (`apply`, `drop`, `show`, + `branch`, autostash integration with rebase-like workflows) are deferred. From 07133d726a4b27a49d42e7dcbc55cd7b3af59d39 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Mon, 18 May 2026 00:20:05 +0800 Subject: [PATCH 09/11] fix(gix-stash): preserve refs/stash on untracked-restore conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before writing any blob from parent[2] during pop, scan all target paths for pre-existing files. If any would be clobbered, mark had_conflicts=true, leave refs/stash intact, and skip all parent[2] writes — preventing silent data loss when the user created a file at the same path after stashing. Co-authored-by: Claude --- gix-stash/src/pop/mod.rs | 69 ++++++++++++++- .../make_pop_untracked_conflict_repo.tar | Bin 0 -> 74240 bytes .../make_pop_untracked_conflict_repo.sh | 18 ++++ gix-stash/tests/stash/pop.rs | 83 ++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 gix-stash/tests/fixtures/generated-archives/make_pop_untracked_conflict_repo.tar create mode 100755 gix-stash/tests/fixtures/make_pop_untracked_conflict_repo.sh diff --git a/gix-stash/src/pop/mod.rs b/gix-stash/src/pop/mod.rs index 906f375eda6..c64b1b6aaac 100644 --- a/gix-stash/src/pop/mod.rs +++ b/gix-stash/src/pop/mod.rs @@ -140,6 +140,13 @@ pub(crate) mod function { /// If the stash commit has a third parent (`parent[2]`), its tree is /// treated as the untracked-files snapshot and those files are restored to /// the working tree after a clean merge. + /// + /// # Untracked-restore conflicts + /// + /// Before writing any file from `parent[2]`, all target paths are scanned + /// for pre-existing entries. If any would be clobbered, [`Outcome::had_conflicts`] + /// is set to `true` and `refs/stash` is **not** dropped — no files from + /// `parent[2]` are written in this case, preventing data loss. pub fn pop(ctx: Context<'_, Objects>, head_tree: ObjectId) -> Result where Objects: gix_object::Find + gix_object::FindHeader + gix_object::Write + Send + Clone, @@ -212,7 +219,7 @@ pub(crate) mod function { gix_merge::tree::Options::default(), )?; - let had_conflicts = merge_outcome.has_unresolved_conflicts(gix_merge::tree::TreatAsUnresolved::git()); + let mut had_conflicts = merge_outcome.has_unresolved_conflicts(gix_merge::tree::TreatAsUnresolved::git()); // Write the merged tree to the ODB. // `tree` is an Editor; we write it by consuming it via the write() method. @@ -241,13 +248,21 @@ pub(crate) mod function { // ------------------------------------------------------------------ // // Restore untracked files if the stash had a parent[2] and merge clean. // ------------------------------------------------------------------ // + // First check whether any target path already exists on disk; if so, + // treat it as a conflict to avoid silent data loss. if !had_conflicts { if let Some(untracked_commit_oid) = untracked_commit { let mut uc_buf = Vec::new(); let uc_commit = objects.find_commit(&untracked_commit_oid, &mut uc_buf)?; let untracked_tree_oid = uc_commit.tree(); drop(uc_commit); - restore_tree_to_worktree(&untracked_tree_oid, worktree, objects)?; + if collect_restore_targets(&untracked_tree_oid, worktree, objects)? { + // At least one target path already exists on disk — treat + // this as a conflict. Leave refs/stash intact and report. + had_conflicts = true; + } else { + restore_tree_to_worktree(&untracked_tree_oid, worktree, objects)?; + } } } @@ -391,4 +406,54 @@ pub(crate) mod function { } Ok(()) } + + /// Walk `tree_oid` recursively and return `true` if any leaf file path + /// already exists on disk under `dir`. + /// + /// Used as a pre-flight check before [`restore_tree_to_worktree`] to avoid + /// silently clobbering files the user created after stashing. + fn collect_restore_targets( + tree_oid: &gix_hash::oid, + dir: &Path, + find: &impl gix_object::FindExt, + ) -> Result { + let mut buf = Vec::new(); + collect_restore_targets_recursive(tree_oid, dir, find, &mut buf) + } + + fn collect_restore_targets_recursive( + tree_oid: &gix_hash::oid, + dir: &Path, + find: &impl gix_object::FindExt, + buf: &mut Vec, + ) -> Result { + use gix_object::tree::EntryKind; + + buf.clear(); + let tree = find.find_tree(tree_oid, buf)?.to_owned(); + + for entry in tree.entries { + let name_bytes: &bstr::BStr = entry.filename.as_ref(); + let entry_path = dir.join(gix_path::from_bstr(name_bytes)); + + match entry.mode.kind() { + EntryKind::Tree => { + let mut sub_buf = Vec::new(); + if collect_restore_targets_recursive(&entry.oid, &entry_path, find, &mut sub_buf)? { + return Ok(true); + } + } + EntryKind::Blob | EntryKind::BlobExecutable | EntryKind::Link => { + if entry_path.try_exists().map_err(|e| super::Error::RestoreUntracked { + path: entry_path.clone(), + source: e, + })? { + return Ok(true); + } + } + EntryKind::Commit => {} + } + } + Ok(false) + } } diff --git a/gix-stash/tests/fixtures/generated-archives/make_pop_untracked_conflict_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_pop_untracked_conflict_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..a559f4e8f5ce14fcbbf30b980831c5d67ed6d235 GIT binary patch literal 74240 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 tracked.txt +git add tracked.txt +git commit -q -m "initial commit" + +# Create an untracked file and stash including untracked. +echo "stashed untracked content" > untracked.txt +git stash push --include-untracked -m "stash: with untracked file" + +# Simulate the user creating a file at the same path after stashing. +echo "user's own content" > untracked.txt diff --git a/gix-stash/tests/stash/pop.rs b/gix-stash/tests/stash/pop.rs index 6812fd52f6f..ec1f2b57949 100644 --- a/gix-stash/tests/stash/pop.rs +++ b/gix-stash/tests/stash/pop.rs @@ -17,6 +17,10 @@ fn pop_untracked_fixture() -> gix_testtools::Result gix_testtools::Result { + gix_testtools::scripted_fixture_writable("make_pop_untracked_conflict_repo.sh") +} + fn pop_conflict_fixture() -> gix_testtools::Result { gix_testtools::scripted_fixture_writable("make_pop_conflict_repo.sh") } @@ -307,3 +311,82 @@ fn pop_conflicts_leave_ref_intact() -> gix_testtools::Result { Ok(()) } + +/// When restoring untracked files (`parent[2]`) during `pop`, if a target +/// path already exists on disk, the pop must report a conflict and leave +/// `refs/stash` intact so no data is lost. +#[test] +fn pop_conflicts_on_untracked_restore_when_target_exists() -> gix_testtools::Result { + let tmp = pop_untracked_conflict_fixture()?; + let worktree = tmp.path(); + let gd = git_dir(worktree); + let refs = open_ref_store(&gd); + let odb = open_odb(&gd)?; + + // The fixture leaves untracked.txt on disk after stashing. + assert!( + worktree.join("untracked.txt").exists(), + "fixture post-condition: untracked.txt must exist on disk" + ); + + let stash_tip_before = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash OID") + .to_owned(); + + let head_oid = head_commit_oid(worktree, &refs, &odb)?; + let head_tree = commit_tree(&odb, head_oid)?; + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let mut diff_cache = new_diff_cache(worktree); + let mut blob_merge = new_blob_merge_platform(worktree); + + let outcome = gix_stash::pop( + gix_stash::PopContext { + refs: &refs, + objects: &odb, + committer: committer_ref, + worktree, + blob_merge: &mut blob_merge, + diff_cache: &mut diff_cache, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_tree, + )?; + + // had_conflicts must be true — an existing file would be clobbered. + assert!( + outcome.had_conflicts, + "pop must report had_conflicts=true when untracked restore would clobber an existing file" + ); + + // refs/stash must still point at the original stash commit. + let stash_tip_after = refs + .find("refs/stash")? + .target + .try_id() + .expect("refs/stash OID") + .to_owned(); + assert_eq!( + stash_tip_after, stash_tip_before, + "refs/stash must not be dropped when untracked restore has a conflict" + ); + + // The user's file must not have been overwritten. + let content = std::fs::read_to_string(worktree.join("untracked.txt"))?; + assert_eq!( + content.trim(), + "user's own content", + "existing file must not be clobbered during a conflicted pop" + ); + + Ok(()) +} From 2b928457ab7ceb6d00b74a7690e8ed754ad82440 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Mon, 18 May 2026 00:20:20 +0800 Subject: [PATCH 10/11] fix(gix-stash): store symlink target path for untracked links In collect_untracked, symlinks were read with std::fs::read which follows the link and reads the pointed-at file's content. Use std::fs::read_link instead, convert the OsStr target to bytes via gix_path::into_bstr, and store the result as an EntryKind::Link blob so the stash faithfully records the link target string, not the content of the target file. Also re-evaluate NoLocalChanges after building all three trees: build the untracked tree eagerly when include_untracked=true and include it in the no-changes check so a clean repo with include_untracked=true returns Err(NoLocalChanges) instead of writing an empty stash commit. Honour the keep_index option in the post-stash worktree checkout: when keep_index=true reset the working tree to the index tree (staged changes remain on disk) rather than unconditionally resetting to HEAD. Co-authored-by: Claude --- gix-stash/src/push/mod.rs | 72 ++++--- .../make_push_symlink_repo.tar | Bin 0 -> 62976 bytes .../tests/fixtures/make_push_symlink_repo.sh | 16 ++ gix-stash/tests/stash/push.rs | 203 ++++++++++++++++++ 4 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 gix-stash/tests/fixtures/generated-archives/make_push_symlink_repo.tar create mode 100755 gix-stash/tests/fixtures/make_push_symlink_repo.sh diff --git a/gix-stash/src/push/mod.rs b/gix-stash/src/push/mod.rs index c4cf6763575..8d9521b5eb3 100644 --- a/gix-stash/src/push/mod.rs +++ b/gix-stash/src/push/mod.rs @@ -198,17 +198,30 @@ pub(crate) mod function { checkout_options, } = ctx; // ------------------------------------------------------------------ // - // Build the WIP and index trees early so we can detect the no-changes - // case before writing any commits or updating any refs. + // Build all three trees before writing any commits so the + // NoLocalChanges check can see the full picture. // ------------------------------------------------------------------ // let wip_tree_oid = write_wip_tree(index, objects, objects, head_tree, worktree)?; let index_tree_oid = write_tree_from_index(index, objects, objects, head_tree)?; - // Guard: if neither the working tree nor the index differs from HEAD, - // and we are not capturing untracked files, there is nothing to stash. + // Collect untracked files (trees only, no commits yet) so we can + // include them in the NoLocalChanges decision below. + let (pending_untracked_tree, pending_untracked_paths) = if options.include_untracked { + let (tree, paths) = write_untracked_tree(objects, objects, worktree, index)?; + (Some(tree), paths) + } else { + (None, Vec::new()) + }; + + // Guard: nothing to stash when all three trees are empty / identical + // to HEAD. Checking the untracked tree against the empty-tree OID + // guards against the case where `include_untracked=true` but the + // worktree has no untracked files. let has_wt_changes = wip_tree_oid != head_tree; let has_index_changes = index_tree_oid != head_tree; - if !has_wt_changes && !has_index_changes && !options.include_untracked { + let empty_tree = ObjectId::empty_tree(head_commit.kind()); + let has_untracked = pending_untracked_tree.as_ref().is_some_and(|t| *t != empty_tree); + if !has_wt_changes && !has_index_changes && !has_untracked { return Err(Error::NoLocalChanges); } @@ -236,9 +249,7 @@ pub(crate) mod function { // ------------------------------------------------------------------ // // parent[2] — untracked files commit (optional). // ------------------------------------------------------------------ // - let (untracked_commit_oid, untracked_paths) = if options.include_untracked { - let empty_tree = ObjectId::empty_tree(head_commit.kind()); - let (untracked_tree, paths) = write_untracked_tree(objects, objects, worktree, index)?; + let (untracked_commit_oid, untracked_paths) = if let Some(untracked_tree) = pending_untracked_tree { if untracked_tree != empty_tree { let msg = format!( "untracked files on {branch}: {short} {subj}", @@ -254,7 +265,7 @@ pub(crate) mod function { committer, msg.as_bytes().as_bstr(), )?), - paths, + pending_untracked_paths, ) } else { (None, Vec::new()) @@ -323,15 +334,17 @@ pub(crate) mod function { .commit(committer_owned.to_ref(&mut time_buf))?; // ------------------------------------------------------------------ // - // Reset working tree to HEAD. + // Reset working tree — to HEAD, or to the index when keep_index=true. // ------------------------------------------------------------------ // - // Build a fresh index from HEAD's tree, then use gix_worktree_state::checkout - // to overwrite the working tree with HEAD content. - let mut head_index = - gix_index::State::from_tree(&head_tree, objects, gix_validate::path::component::Options::default())?; + // With keep_index=true the WT is reset to the *index* state (staged + // changes are preserved on disk) rather than to HEAD. We already + // computed `index_tree_oid` above, so we just reuse it. + let reset_tree = if options.keep_index { index_tree_oid } else { head_tree }; + let mut reset_index = + gix_index::State::from_tree(&reset_tree, objects, gix_validate::path::component::Options::default())?; let should_interrupt = std::sync::atomic::AtomicBool::new(false); gix_worktree_state::checkout( - &mut head_index, + &mut reset_index, worktree, objects.clone(), &gix_features::progress::Discard, @@ -573,20 +586,27 @@ pub(crate) mod function { continue; } - let content = std::fs::read(&abs_path).map_err(|e| Error::ReadFile { - path: abs_path.clone(), - source: e, - })?; - let blob_oid = odb - .write_buf(gix_object::Kind::Blob, &content) - .map_err(Error::WriteBlob)?; - - let kind = if file_type.is_symlink() { - EntryKind::Link + let (blob_content, kind) = if file_type.is_symlink() { + // Store the symlink target path as the blob, not the + // content of the file the link points to. + let target = std::fs::read_link(&abs_path).map_err(|e| Error::ReadFile { + path: abs_path.clone(), + source: e, + })?; + let target_bytes = gix_path::into_bstr(target); + (target_bytes.as_ref().to_vec(), EntryKind::Link) } else { - EntryKind::Blob + let content = std::fs::read(&abs_path).map_err(|e| Error::ReadFile { + path: abs_path.clone(), + source: e, + })?; + (content, EntryKind::Blob) }; + let blob_oid = odb + .write_buf(gix_object::Kind::Blob, &blob_content) + .map_err(Error::WriteBlob)?; + let rela_bstr: &bstr::BStr = rela.as_bstr(); let components: Vec<&bstr::BStr> = rela_bstr.split(|b| *b == b'/').map(bstr::ByteSlice::as_bstr).collect(); diff --git a/gix-stash/tests/fixtures/generated-archives/make_push_symlink_repo.tar b/gix-stash/tests/fixtures/generated-archives/make_push_symlink_repo.tar new file mode 100644 index 0000000000000000000000000000000000000000..c19e32d4d4476f5ea352a1bf459218559b9fe231 GIT binary patch literal 62976 zcmeHwU2I&*m7eTfYNwWRntG>+v)(8a8iNojP^u{MD&b=jQU~CjL{v&+78>IsV4a;BR?WEG(@Q z3#CG7QNCYYEG?f~o;YS0pci(%jxn!2luRu#xz`5xW&q^&>)lC@MADpg{a2Su)AgV9 z!T>(kKVS93N~hlL)`Qk0hUEIMEG-Snz5BaREEdo8%YU&{T7@2%1Tup#zu@wJx7G5R ze${mRb`aLPLFe$Y@vHT&=^oTWQ>!<8Gn-(KSb$wqGX67JdqbUI)GeUSuN?T5N22tVO-v)$H%&hb zy?sB2o||5y0W(rQG--GgMjNHg(|Y&71TEiqo&8?ZZ*><0hFFlGCZB0z0HAx|bT6p&dspu6e6)RgZ|mx{n>*%(3DG{=GP7a+ z;qF}S!p81=?m{NtH;py3TgiEMSK`juWRduu?pLtz?c`E!=30k#Y2w#WO@tU((jgH@4 zFQigkKkS-x+N@*XIl*!E;lLDTEvl&*69%0wX5MSnADe8&(ExDs5AU0q2N#O`f4(|9 zFEe|dnvuzEyj?O^kHb8dnjt#H)XP-La>!oCUk66183374k!#iJ`zD*+_q*ASzwbY5 zgOnAl-EPkInX4Rlofl!Z10I>*%@7B4Rvc!}&!%QxI*eN;3#9R7hTvzgg7+Vq2Nz5} zFMqzc|KR?GTz>b#i{0I~U+mi^UauQu^7qeQe*dk^S0BFpV(o#nZsY5hv#I%XDy%>% zgHE_d=3U1+KaVH#(qwm^zq7FW(rmorJ_5yPx}&<}E~HcChSz$e0B4oPnOQ9B)p{s0 zD{>N2HFUa^4AB|sYQ0u7*|Z5dCcV|F`p?!(nsPy5DhJg=qq+%joM@Rl)DR7bGNC|w zJcUU3&}-0b_v}23!jdjmw zp1TobmM`;<<)Nb-$9tKt%Cndj@`N(t15O7V4C(gvJ6`($6lgR|*++kM;=^9~6Tbr8 z>w8t`%`hOk)d0fO3c9A{tI(IB?`Q)2DsWnZHjqxKnby%5Cb4;|9Cv}Sh>S^s*pAeu zhE;*WhW>1N-Fk)SKt0v!&-_rvW}jmR?k7!IRupJH)K$+vZ(n|V`6VCNrH`JtrmB0u zuvll2e`v9EhLldHuuhnBB(~`rY6G#ht*%#ZQPaveV$X!uL%IbF5%-6&z|@?^t~%XM z7&$alv9HxrDKU8&{C%cdt%UyvB3u3chW)?d%FzCQX=!ol|DD8td)+oC_5YsuoqFxi zJf)XLccL87a|obFtRDpDV4NS=f+u6$z%c1wh2qme*$#Vkr}XInW0Y5CQ9P}877GNu zGw1@E*^d7t3uOvlA)B>C`RLk}t7ZdHv?uvi56ZQ)@pjSb32&b|p}Wo|DZdf=)Mi_) zdbjR1ELLl(FKkQttA4ZHJv7}8%u8U^tb?Qe7Tn~6tbS#(SO&I?hM!MSTVRx$IShK@ z;))MR4;CuDmA!Ijb8Ab?3q4ZUMxWLPiwZZcu5K&b%1#g7te8;r@nC}L*4d*v6F;MS z*!I`ULC_FKG0gQfo+k>o;VXA&Tfi)B<(IGvBh$%2oB0(}gyX|@o1GvGO}pWBq5GRu z{(oH+hl~B0UT8~w4whAzoQJuk9;}_ouCKk9@fI5f;GaX?7;8% z3udm~oec?u^T+%i_dx3Ef?7{_pvrZBOjvJK zgeOUf@No}&0EYnfazjL5u3ul@uh~u-1H*sn zi%Tw;PjJc#w+!B$#vG)-_CXLvz+%a^Xfx5D1SQG{C;MueXR;i zl)CfLmEwA4F_ns(vbNu8m~1^)_8uX0iV&Q5qlhOq5N4Qb<{#$9t!X}&L$mXl{5->w z16ZW#ZCA(=(N~JiLWa0728IS^;RXFCI5++RBeVD!jUZNnu{QltOw+rqw8@!|olAgK z>URJmPWc!;XXtAI+dLd%lMzO~c+srGw;P4LxqO+UU6|Joe&|&=pW!&P&U)wCD~F=N zZ(w4-1Ig@WyB#P2zY}6A^;Q;S!3H4fo`R4zSUp5)2!abAeS47J6caF-Q2Q9-CTI#+ zZz+W&40!S4C<*~8Hh-tF8w2s5b}u|QBdZ@{G0y%kFRl!o{}mS(r{{k$2`Ba`w*TOr zD@Pvx9TWZ`%ZNJcKLP|eev@{`aeBCCW5}XpX|1sTRfV8OQPxCwtwShmY&qy1lzkDC zXc#fi(7N_)lG~c2EHoq70>{R@1*6D5Q+um*#jXl_Ti6@1BQyZ&d=Lc&&YM1TLAWN? zBvXX&o11v%ybwDNm=0{DDhDz*VoTGYp@=GsGw6{ivFqL2*J);sZ;RHLC!r7rqQ!Wd z?D}A5=t?nVN9c#IpwFYg8X8+O1#^fE8ZF>fi*XeM)3I~4kvq&${T9$pu$G;_1WuIF z#6Ik`ah!&sundfT(|-au5(ul-s(P@g^%f&L_UweiD4C3Tw-JD6KQpS&Pq5`Zy1svAZ>?cODbWnL{KjLYsijx;-F>u--{qS6m9*Gf-1H@*` zAnNlG$d4haegnLT1GY76@Fl#M01?CPgW-TM0OdbrxTjS)SPt``6q2ZHtG2SJ#YB%L+L%(*nz*TDpA|GbDC{>HYp$6q~{oa z&Ehl$hbX_K-1Z^L=F>4wn$PD%y)T=OASRQ~7tfv|G(tfr z@$emUw+4@ssY)P!2SS-Jk&lZ^6iq{+W{E&hl{giWc$j%!yqM0spMH5F?U9iG5WzI0 z+|(P{CtjmYx9*H00b}HUX{j(A|1TDCU*s6RJ+rUA;P*db`G-^-v;Vgofz78_2s4NM zN1lmVv=@jLcj|sPkL@l)ynsYr;$8q!{iN<8qXBI7x{{yFS zUWv2-U}p1IPj2~#B?F>Of9Zom?7(VXuYtT~QDE3R$qBbpmo{ras5D0k@jMqXWGV`7 ztg#_yUSUuJFk)<>nO+-Nn!blz2HBlO5dfa_BEc0A4{S@2dni$QuY<%K98xn6BxkNL z{Tbk_5OdEz7xpR@YeLA`W4* z1zfBlTA-X$UAHwjGG__8hI!Zb>n#muAJzOe29J9%X9w+p+ihQ?zC?a!`|36i{ww}L zfMgGZg(bv*1&wTyEIx3oAo*6L0J~$H<#SP(PRVQmhvr@d2R5BOY~p!BGdFLLZp(9a z4M<=Q%;DA|1dbYN&d=@VOxX6hcjwtVWDn7e8K#>sXE++p3h(eZ*IHpu;wVXuwQQPg@}!7!bB_=m&giH|*iWY-laOb*c4OpBlu<3o zPB$Dg9s&eU6hu5IQfP3;q1;0f&jO|@OBZ$z(Nb3wF_La2m&68I2>D$IJDkx^D#|$3 zUBR7eH?Lm1ab@eeoFVOPZr{C$Qw>IQT!NX3&0uPlQzOds42vMg>nN_D=avMCrnRG> zNPgS#Fd#V!#_0^eYChsA5(e)!+sFfBB2R=u>|%UNf3!Dw%0ZA@57`HskZ zk5?C5f`2+1Gn)9CkzAWK$)iy<2h+s_jEDkh!~8fGVS*Ug5_>MGWGn(CE>++nfIY1xg*Cm|W={>M1_^{}crW^Nd(`&sE{~vcf3yXvCzm?)D>P5T>dQSa+mk`R_1LznW zCh>+)rF&UEKlM5-coU)iwRLNAZEYI~p{2__dOv!@z~gh=H^I@sJAWa4gzsKd=leJvO9ZmfkE%)F)&CO#XZ1+hRwD#JM_AjS= z*fL^ilfTW^?`+>(Tl?0X%c+^D74I#&=OD#Q@p9@V_kh)4xaX}~qkG=Eg`O`le8cPE zsBq7Ts-T3MU}oSsArsR8gS)nSLOJ%}QQTZTjOVKG{OVjLm=~(gsF>k=G{nvtkFy)k zD>#3A^m4(x|6KkQ$$I&c;9hcrLoE7wSsdmv(yH@GDKMAi4DxMBTSN)_UFAJ0K&zQ2qrVFt5dKx24z# zKcER7Lk*(|5p{sT@!NNIFmMFRLb5;r+~o@uIFg*$n#=-OU{{{#*E$qdRLSOMuqR;L z+JW}%0PZF_KYkV|XCekBPWLlwI*-vrR6b;;lx*4QA8TWrcOwKb+utRUkeM;!!SGJz zJxTW`Ep&ew3qf`R>NmSV;$}Chh=x@CIcxzGcdWzU9EA(r9NR!`U6!^qw|impqw81q zguwRm*3AVo>!@S0pI{@>3bJt0yWK+^^3PC;s2|81C2ZZ9E9zI*Oy(^s{bQ!4Ot_`D zP8BiSOJQOHO_g5$e(KL+L*Rv?Yw*HM(9Dg=*^>RiGuGlxnWba zUD#a3ml$2?*qpHnZMu`Va?o6ley9l7UXe)r1DkJ1&I5&Io**~ zR{y0RJV5K0_#ZGhR;iNmkuV98T%qxLRhs5#^sHWu)u&Pb(;iTm+Ct=c>!zg>T0tFw zPwiv;O&U5?fu*8Fv??=y*}Uu2!3GTOyO^=wD`+9h4qHyk6 zk9rM^-NZJH2}KOMLG$8_-5R2k=^#Mhwt+o8ax4#Eis)pX{#n_2%3 zvVeD=!;ZYNiXY`sSVa#HLQ>3~?U$98=nIzJeg_vro?x{_^fUTBiEBRs$7a4~>w54u zR~R(ll#4f>y8u(bn~tcTWl-v3p4oF$x5Dv$6`Pvpkdpz9OBz;bI3%<*tb>-diNn@d zlCj5d{Mk)4%UEzv?c-`DEp`1#y^3T}v9>Ua!l!-CyLfWqyeF4BAv5SS zpfp}XTLCv z$mBu;O}aWkCKrKe=_FG3FN_vmM4Sl?@|ZZ0COjql|e+ZQ}H=FEld7ZBi({x73C#M!F(e_l3`R4+PKqe^%Z=PVlN$E8|lSt#5WN*0}$Tb|0F0ci0Ws;H~`8uZ;e{&>4qpe zKxNVdcbL2^#E;B|1`JmL+};?n{wAWH4x!Ut?&sK10zUt+xyDa%4IaKOyxF-vZPm=dGuCR3V#DMyF_+nCDw6GptYRf92zcD}cia}JS8N}c#wb80$@%?!1KzLHy zt^-KU69l;lf|T+QXV|QVgj*h6v;7So3}{3A{T}n~uChEcnMgrm=^J#qcmw~v6gNvO zoglW1L zbqPGQ7)1O40tXVbP*Pi$Qr3H5G}LOhOr$ILv7C3P$DCL>Z4;HL7`1dxgW&iHY%Rqj zL_8EOo#0nlFjtX6#Wlr345~!zoVWr5Q_2o8XM%uEW-!lz(pOp*pC`F07dR(n&f@(7 zGc)Am47WR>;nEje3l_1_=pQ#kqNWqfBx~k0`Wg<vOsl47@+vTl8{+4{KqmNmUSZqLzH+4!csV5J>@)+eiW3w zy1lt4SuH!fpk$4*_!Oa6^Y>9J7ggkx2~>kRdmQEZI+JJ}hDhhbytc4mMFCpP;H|MV zGjf&jh7=f7)kZAtdckZnoeLF!la_-n2vi^|9j7ho$i}gsM2}HlvB-ec)=+5yEs0#3 z!v!n2P0(kWumGJ7mf#sMc96)6h97d&xRKtiz|jMu+^r`^a40NrXbgw;?z*5TOKQsK zbppI(m9Tke4jhq9Of1;n_d2*B31)>!L&5_x(KOS#j}jQ60(r98&ZG;~E?(;a0GOlc z;++dNQQ9GwDBFkCx0&uBLJNT}yv(L^C^o^)fI$zj!Kk80WPsu$9~WwPna?>sNV!AA zgVlVoT=$?y#aJZgDq0dmw%S>HYr-D+M;DhPIxbpJvT_PdEDf5Zm1DJmWgM-4MGsCU z9hL4{K-H{Hm6?$0BDi6#m1p)=3Ls&q6sj6QT`y$1B7PqG|`&=9E#VDC93&>;t!XYT_XPK_yK z)P#A~>=1`m5yle!8U$#cBvax=l#@HBB7PytaUi|Zu}=aEWXFU3rE>-4FLnnD(>p{B(bXQ-=HsCGm`k5$LJ9z);t1OD;ku&~7d)5L95UCBrm7Hh(0htxLjdnE z+QzZC9a8}&WMgiydMB72jtLgB0ZC!2*!Pj%a(l90uu=2Omdn=i*-a0Jgw_t)8A#XY zhOiJ_ZawI-iztf*1YgLdP_VYE7Y-bBG2JXs)EwGQOL&XAiLyZD1~N2>Qr7y4$Q0#b zQhBJ(kb&lK83vWl?d4t~0R69-sJJ&$!_Md+N~M+lR@FEpun^aM6lh183~Y+!PA!DO z21&vDGrSrQ;MVBae^yGRN1mAC$Ne7q%vSu;17!Xyn=5S7-vN!-CJAIj8$QC1Ygqxb z=%4vOzvTCN$sumo4|aE`p##X@WkGRM+PIb$V6xdLka?jx`i~``YZIBsR4R340aKv; z+LV1-(x~tr5mosWDL%-mx_mv0=2e4n7z40^uO6(`Nz&&7=~Jsi8)`|!EvS@ZA|{pD&G4&82??A_XWQR!c6afp6LCLWx4o+^ye0&QatXi z2iHh;A*|KSSIRLUx(V@w)kc)ohJEEc8D)(sk8ByZs8Z<~br0Q9_z+$(i>oO50XKS1 znXIFnYiqJ>%KK@63KTzn@PT_a`(BL11n`9|;mvp0&=JJtdO2*Qe~6OX15?xam&~v^x>kK6=WQPiL?WGjZl0T4G!|kNXTu}!|_PK zLq~=(T{jqy=nAX8YE$-LxU-Z?%v=zny*zI2>Mx&k`jK~ib|k>-)(&+Cc_LJ_#soZi zoiUbV2q!?qg=C$*yKdwXWd>P8?UCgt#{ba`9BVf1tq!@OV`W66=#UZETYN^$4q~F8 z7p1B@JKjND0B=z;9xus>hElDWfw(&Sr%C5gAY2H4ZIdJ zS@MEm*mxS25;$xRbY9v!{#$_zO|!>$)Z$G1$Wey}b?d-4UFl1(66#>E6v)W;U?tkm zD+CpHWtbCzNOqV{4YKMu5QFXeuz)O&>o05>sVLh!(iSJ`9koV9WL{C^8-y8vM4UbG zGLMvf-4`DLth_f9LDu49M(!-@<`eBv&etB_Bs$6=4sL4GoFsG85QTk#`+Pje5zf7E z;exrPuNO<=p!W>nbE={S*TNR`vqkcVM}$;wl;a&}EHe!*%_#fP#iYZ7Jve{S(Zn{; z6KR4zg!ipP#Un6u?sZE|i+CWCDas&G<~f#i{c2CRREKtQiF7Z-1 z3sT=EX9?~kaKQ~Ioi$Mf--UTvsQiTmY!h&s6GX@LlU^&^XGrl7A%jkr)vvi)>zU{F z)A{o;3*}= z3jG4l#}eWyi-Xf~P~&}y86S%!hE@~@*EP9dk?i+ji3&V&2Z@_Tq?3Z2xH_ml;70rg zLw$kbKymK0EG_6h>$=QFQ6xQaNKiSF^&2dabj>0#=Z)b(CEAQ1$ zZzLB>i973lB_%2DFk*_s{gN!J9Zo;j0jRIFNRs5ox;plTpj`EugXF@MLi9CyF$SN% zM=6ZW+FdPT!_Ceb^dDa;2*(cgiANy1L!$M8yv0Jt!x6Ty%HFDTalu^KG0{>kw}FQ5 zLmS@u{M5LA6JdnU>llgn4-<8>-5{#AcV=0D^n+HHQO`?%27jZ}|LS1;XL)&bTL1S= z@Kl`t2QT(Ybcm?Ab9P4vtzq>C+*rE3ewF2JwCEJ_mhj>;c(!DpL=u}zzC*15IW0y+ z2(6+#Wd?+J04RBJ5=O@Rbmce{t$>PTjdS;@BQqFYg9gOG80+~W9L&tY*jdx)b(AR1 z-T;E*Odf(p=yO+VEt8z-L9fOI!R*X{A81M5=wMV{tK-Vv zQu}MgTXa_3w@`cpsclM|I5vcHDkfjyLJg2*H_VWU?A%KbQF{egsZ2`np_EktWP7Tu z)-FK@IT(PI!0A2?7=>QZIaNGY07%YO-Sx}57G<2(jTv7f^#DSQECKEQMhXYxcn`Kp zCoku=64{s5*o+{2!WmTt>gAa&9b$;RGDtF}0%O%NadiDkzrJb&k6N3=SMv>kaP>qRS>=8C51S zEa)RhAr62<6$FYIgN`qTj3I4;6*)4PFk)9Z^rEk9b2Jn0_tX{s5ah7uwptZimYZke z_NoixXL=CkRq<9p)ZxqaOtvX$s&M-8UMbE9RIO?TiQnw;&5F_o;X0@Qr;7*)Er}vF zNGMqdE|;_%up58~hECH$;VG|mp9VTAwxCVwg;BW3Yc-MI0wzX!2PBYV$cZ+futNyo zzyx#UU!ojD!^?FY`A&F8xfN@u_g|O{V^>JuZWXPuoko&}begyd!;Eca6|)2%om!a} z5lZMR9L_&wu|z(0gW^zA?;5F4Lu5QiB<{YTVkuvb$z|fJ+*VdUNtUQ^k0Z;OA7061PO&s9b7Ed&vISXn%SZDh`c zO;nXqBX`?e97!M*ezo=2(vrRxfhSK%Op8!Z;2Tv_MvssLea~i+sr0d?509f!mKdQA zEQ(cRIucnDtSH>r33~ew=L7==HCLjZaf3a8*+Xk4%!5(hGihd|r_GwVcH`F0uSp^2 zJ-&!_@1twqB8fy8qoACxI;FIMaOjhK3klg4X~JHY4;>0PW3 zu?fumb@)N_L6MUs+a`77X3rxo53i0p7w)&{7|@5tKG&YRHBsOJE-1XCP4%ebcgSeQ z8n%4)UFzIP%e!$bGR*#j+t>=t9Gom2f$kB~q~il=+3@QQsEN$5^X5YCe&9Xehwn@c z!T=-4!os@>2cDnhpOp7w;(z>lSpVbx?^OSvy|i?xfCi;Z|Cfg9{}q;&OVj<|1lPoYhU+hR_@VlLV*jVQ-cje5|F_7$#)l`Q{m0k;g?p!7$fLB`o#g(c253SbO zKj>TkQtj*q0K`{V|Ke(~G+qCRtVTc3f%VT<3&rKqa^=$M@@lQ*FX1(vRTRfqsuldo zJAS3~&dSOq-(Tv7G43y}|FQ9(rJ?xG>Jpw!&ws{I_VvGi_N8z9_($8f3rqVy%>VfM zXPz`jl;{D<~mnzkzORLrLs^@u^mP)IuE6aX)X|duL%Ej7h<&Ol z3pJs?o_*=!{MT>XEquR5 zuHP*F&eHZ)Y5V&1?=2PK1T5aTQ7YcNx%^S-`r`J@|MK;P?2mq!zy0gCSAO+BzV)xt z#oslvck`Ft`wusN?~nfA(@%f@4}Sj#?_c@Z4}S22U;j$^-qt67_shTO|H^*(PnTC$ zmwxl-fAZr`|Lww$u7CO)pZv*}&;9VhrJuk4Or{WCg#se~dX?=}x}T`ec=;c?|5+@- z7ER^cku|tSdeDaU^6^=-`=juA)j-4 z^7vYd`3jvy{^hF2D|9|#m$C8>Ss!@+U$L;fGL`=m()V?mUER99iT+>4zb}0<^WQsv zZF%#bx!-uUaPG^0^DF1h{l;&dJNGMps{hXY<@_K1)vx}j_qTG_fBv)owf&cV_&?@q z|Kz`(`-+sozQ zxpC)fXFl*a`7a>WGa&!SI-JV?nJ>f$fGFm|E5Y_Y^wj>2Aki=NBaweNa1%@Z0re>V zr_BE-tU~^$@t?`FUbO!C$t8h~)BmNVq5RL{QfZq1Ir-IB+2s0+`!OfIC}1rKZt0nK zwR#CYCii$7=F$?vdquog&|KuOGujMAWz`nVui`@?p9Vh>|V>%fBDNgHuoc%hCf*9a+-}RORfKZUF)tyFwMp5aP z_gV80ulJb5rJn@JDF3JQ|4Yj&%jZgOLOFfG>p$)7p~hVB3{_S4ns`n6-drj1Us_$A#{bWV{KtY}g;|&DRmv?d Z tracked.txt +git add tracked.txt +git commit -q -m "initial commit" + +# Create an untracked symlink (not staged) pointing at tracked.txt. +# We don't stage it — it should appear as an untracked entry when +# `include_untracked=true` is set. +ln -s tracked.txt mylink diff --git a/gix-stash/tests/stash/push.rs b/gix-stash/tests/stash/push.rs index caa8cae6c3a..8031a194a5a 100644 --- a/gix-stash/tests/stash/push.rs +++ b/gix-stash/tests/stash/push.rs @@ -337,3 +337,206 @@ fn push_returns_empty_repository_on_no_commits() -> gix_testtools::Result { // See `gix::Repository::stash_push` for the porcelain-level guard. Ok(()) } + +/// Open the symlink push fixture using `Creation::Execute` so that the symlink +/// created by the script is preserved in the writable copy. +fn push_symlink_fixture() -> gix_testtools::Result { + gix_testtools::scripted_fixture_writable_with_args( + "make_push_symlink_repo.sh", + None::, + gix_testtools::Creation::Execute, + ) +} + +/// When `include_untracked=true` an untracked symlink must be captured with +/// entry mode `Link` and a blob containing the **link target path**, not the +/// content of the file the link points to. +#[test] +fn push_captures_symlink_target_for_untracked_links() -> gix_testtools::Result { + let tmp = push_symlink_fixture()?; + let worktree = tmp.path(); + + // Verify the fixture produced a symlink on disk. + let link_path = worktree.join("mylink"); + assert!( + link_path.symlink_metadata()?.file_type().is_symlink(), + "fixture must create mylink as a symlink" + ); + + // Also make a tracked change so push doesn't bail with NoLocalChanges + // when the symlink is the only untracked entry. + std::fs::write(worktree.join("tracked.txt"), "changed\n")?; + + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions { + include_untracked: true, + ..Default::default() + }, + )?; + + // The untracked commit must exist (mylink was captured). + let untracked_commit_oid = outcome + .untracked_commit + .expect("untracked_commit must be Some when symlink was captured"); + + // The blob stored for mylink must be the link target "tracked.txt", + // NOT the content of tracked.txt. + let untracked_tree = commit_tree(&repo.odb, untracked_commit_oid)?; + let blob_bytes = blob_content_in_tree(&repo.odb, untracked_tree, b"mylink")?; + assert_eq!( + blob_bytes, b"tracked.txt", + "symlink blob must store the link target path, not the target file content" + ); + + // Verify the mode stored in the tree is Link. + use gix_object::FindExt; + let mut buf = Vec::new(); + let tree = repo.odb.find_tree(&untracked_tree, &mut buf)?; + let entry = tree + .entries + .iter() + .find(|e| e.filename == "mylink") + .expect("mylink entry must exist in untracked tree"); + assert_eq!( + entry.mode.kind(), + gix_object::tree::EntryKind::Link, + "symlink must be stored with EntryKind::Link" + ); + + Ok(()) +} + +/// When `include_untracked=true` but there are no untracked files (and no +/// staged/WIP changes either), `push` must return `Err(NoLocalChanges)`. +#[test] +fn push_returns_no_local_changes_with_include_untracked_when_nothing_to_save() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let result = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: Default::default(), + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions { + include_untracked: true, + ..Default::default() + }, + ); + + match result { + Err(gix_stash::PushError::NoLocalChanges) => {} + Ok(_) => { + return Err( + "BUG: push with include_untracked=true on a clean repo must return NoLocalChanges, \ + not succeed with an empty stash commit" + .into(), + ); + } + Err(e) => return Err(e.into()), + } + + Ok(()) +} + +/// `push` with `keep_index=true` must leave the WT reflecting the **index** +/// state (staged changes visible on disk) rather than resetting to HEAD. +#[test] +fn push_with_keep_index_preserves_staged_changes_in_wt() -> gix_testtools::Result { + let tmp = push_fixture()?; + let worktree = tmp.path(); + + // Stage a modification. + std::fs::write(worktree.join("tracked.txt"), "staged content\n")?; + std::process::Command::new("git") + .args(["add", "tracked.txt"]) + .current_dir(worktree) + .status()?; + + let repo = Repo::open(worktree)?; + let index = repo.load_index()?; + let head_oid = head_commit_oid(worktree, &repo.refs, &repo.odb)?; + let head_tree = commit_tree(&repo.odb, head_oid)?; + let head_branch: gix_ref::FullName = "refs/heads/main".try_into().expect("valid ref name"); + + let committer = test_committer(); + let mut time_buf = TimeBuf::default(); + let committer_ref = committer.to_ref(&mut time_buf); + + let outcome = gix_stash::push( + gix_stash::PushContext { + refs: &repo.refs, + objects: &repo.odb, + index: &index, + worktree, + committer: committer_ref, + checkout_options: gix_worktree_state::checkout::Options { + overwrite_existing: true, + ..Default::default() + }, + }, + head_oid, + head_tree, + Some(head_branch.as_ref()), + gix_stash::PushOptions { + keep_index: true, + ..Default::default() + }, + )?; + + // refs/stash must be set. + assert!( + repo.refs.try_find("refs/stash")?.is_some(), + "refs/stash must be created by push" + ); + + // The stash must have been recorded. + let _ = outcome.stash; + + // With keep_index=true the WT file must still contain the staged content, + // not the HEAD content. + let wt_content = std::fs::read(worktree.join("tracked.txt"))?; + assert_eq!( + wt_content, b"staged content\n", + "keep_index=true must leave the staged content on disk" + ); + + Ok(()) +} From 094dce3835e61c4b3fb6a787b252584f8514dbf8 Mon Sep 17 00:00:00 2001 From: mxaddict Date: Mon, 18 May 2026 00:45:57 +0800 Subject: [PATCH 11/11] =?UTF-8?q?fix(gix-stash):=20unblock=20CI=20?= =?UTF-8?q?=E2=80=94=20drop=20unused=20deps,=20fix=20doc=20links,=20gate?= =?UTF-8?q?=20symlink=20restore=20by=20cfg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo machete flagged gix-dir, gix-trace, smallvec as unused; remove them from the manifest (they were copy-paste from the gix-blame template but never imported). cargo doc rejected the intra-doc links [push] / [pop] / [list] as ambiguous because each name is both a function (re-exported in lib.rs) and a module. Disambiguate with [push()] / [pop()] / [list()] to target the function re-exports. cargo test on windows-latest / windows-11-arm / macos failed at the build step because std::os::unix::fs::symlink doesn't exist on Windows. cfg-gate the symlink call: unix uses std::os::unix::fs::symlink, windows uses std::os::windows::fs::symlink_file, other targets get an Unsupported io::Error. The pre-flight try_exists scan already short-circuits before any writes when a target conflict is detected, so a partial restore on Windows symlink failure only happens when the file system itself rejects the call (e.g. insufficient privileges). Co-authored-by: Claude --- Cargo.lock | 494 ++++++++++++++++++++++----------------- gix-stash/Cargo.toml | 15 +- gix-stash/src/lib.rs | 8 +- gix-stash/src/pop/mod.rs | 24 +- 4 files changed, 319 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f54dd9168e..238b3e500a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,15 +278,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.16.3" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "zeroize", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.40.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -445,6 +445,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -496,9 +507,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.2" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff7a1dccbdd8b078c2bdebff47e404615151534d5043da397ec50286816f9cb" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" dependencies = [ "clap", ] @@ -557,9 +568,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -607,6 +618,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -658,9 +679,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -830,9 +851,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -854,9 +875,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.87+curl-8.19.0" +version = "0.4.88+curl-8.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a460380f0ef783703dcbe909107f39c162adeac050d73c850055118b5b6327" +checksum = "644816de6547255eff4e491a1dda1c19b7237f00b62a61e6e64859ce4f2906d0" dependencies = [ "cc", "libc", @@ -865,7 +886,7 @@ dependencies = [ "pkg-config", "rustls-ffi", "vcpkg", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -904,9 +925,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -988,19 +1009,19 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1030,9 +1051,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encode_unicode" @@ -1049,18 +1070,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "env_filter" version = "1.0.1" @@ -1178,13 +1187,12 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1306,6 +1314,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -1326,6 +1345,7 @@ checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1380,6 +1400,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", "wasm-bindgen", @@ -1493,6 +1514,7 @@ dependencies = [ "gix-revwalk", "gix-sec", "gix-shallow", + "gix-stash", "gix-status", "gix-submodule", "gix-tempfile", @@ -1905,7 +1927,7 @@ name = "gix-hashtable" version = "0.15.1" dependencies = [ "gix-hash", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "parking_lot", ] @@ -1935,7 +1957,7 @@ dependencies = [ "gix-hash", "gix-imara-diff", "gix-object", - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -1960,7 +1982,7 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "insta", "itoa", "libc", @@ -2358,6 +2380,31 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "gix-stash" +version = "0.0.0" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-diff", + "gix-features", + "gix-filter", + "gix-hash", + "gix-index", + "gix-lock", + "gix-merge", + "gix-object", + "gix-odb", + "gix-path", + "gix-ref", + "gix-testtools", + "gix-validate", + "gix-worktree", + "gix-worktree-state", + "thiserror 2.0.18", +] + [[package]] name = "gix-status" version = "0.31.0" @@ -2618,9 +2665,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -2683,9 +2730,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -2724,46 +2771,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", + "hickory-proto", "idna", "ipnet", + "jni 0.22.4", + "rand 0.10.1", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", "once_cell", - "rand", + "prefix-trie", + "rand 0.10.1", "ring", "thiserror 2.0.18", "tinyvec", - "tokio", "tracing", "url", ] [[package]] name = "hickory-resolver" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni 0.22.4", "moka", + "ndk-context", "once_cell", "parking_lot", - "rand", + "rand 0.10.1", "resolv-conf", "smallvec", + "system-configuration", "thiserror 2.0.18", "tokio", "tracing", @@ -2771,9 +2842,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2810,24 +2881,24 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "human_format" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8" +checksum = "eaec953f16e5bcf6b8a3cb3aa959b17e5577dbd2693e94554c462c08be22624b" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -3005,9 +3076,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -3020,7 +3091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -3098,14 +3169,7 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ - "memchr", "serde", ] @@ -3177,9 +3241,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3187,14 +3251,14 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-link", ] [[package]] name = "jiff-static" -version = "0.2.24" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -3302,9 +3366,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -3376,18 +3440,6 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags 2.11.1", - "libc", - "plain", - "redox_syscall 0.7.4", -] - [[package]] name = "libsqlite3-sys" version = "0.37.0" @@ -3449,9 +3501,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" dependencies = [ "value-bag", ] @@ -3482,22 +3534,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "macro_rules_attribute" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" -dependencies = [ - "macro_rules_attribute-proc_macro", - "paste", -] - -[[package]] -name = "macro_rules_attribute-proc_macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" - [[package]] name = "maplit" version = "1.0.2" @@ -3506,9 +3542,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "maybe-async" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +checksum = "746873a384ad60adc5db74471dfaba74bd278afbdcfd81db93fafcdfc8b5ca0c" dependencies = [ "proc-macro2", "quote", @@ -3517,9 +3553,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmap2" @@ -3557,9 +3593,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3601,6 +3637,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nix" version = "0.26.4" @@ -3643,9 +3685,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -3708,9 +3750,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "open" -version = "5.3.4" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "is-wsl", "libc", @@ -3800,17 +3842,11 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3858,12 +3894,6 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - [[package]] name = "plotters" version = "0.3.7" @@ -3945,6 +3975,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -4036,7 +4077,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -4090,7 +4131,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -4100,7 +4152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -4112,6 +4164,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "ratatui" version = "0.30.0" @@ -4210,15 +4268,6 @@ dependencies = [ "bitflags 2.11.1", ] -[[package]] -name = "redox_syscall" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" -dependencies = [ - "bitflags 2.11.1", -] - [[package]] name = "regex" version = "1.12.3" @@ -4250,9 +4299,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -4279,7 +4328,7 @@ dependencies = [ "quinn", "rustls", "rustls-pki-types", - "rustls-platform-verifier 0.6.2", + "rustls-platform-verifier 0.7.0", "sync_wrapper", "tokio", "tokio-native-tls", @@ -4315,9 +4364,9 @@ dependencies = [ [[package]] name = "rsqlite-vfs" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", "thiserror 2.0.18", @@ -4368,9 +4417,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -4382,15 +4431,14 @@ dependencies = [ [[package]] name = "rustls-ffi" -version = "0.15.3" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e80b8f2dee9c7cd5ad441577a8f350cf67080205dd3415bbafa7998fc6b5cf" +checksum = "4128514cb6472050cba340cdac098a235c53e6aad276737ce1d7b24a19260392" dependencies = [ "libc", "log", - "macro_rules_attribute", "rustls", - "rustls-platform-verifier 0.7.0", + "rustls-platform-verifier 0.5.3", "rustls-webpki", ] @@ -4418,11 +4466,11 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni 0.21.1", "log", @@ -4433,8 +4481,8 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", ] [[package]] @@ -4443,7 +4491,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "jni 0.22.4", "log", @@ -4454,7 +4502,7 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs", + "webpki-root-certs 1.0.7", "windows-sys 0.61.2", ] @@ -4534,7 +4582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4588,9 +4636,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4663,7 +4711,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "digest 0.11.2", + "digest 0.11.3", ] [[package]] @@ -4683,9 +4731,9 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -4773,9 +4821,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4783,9 +4831,9 @@ dependencies = [ [[package]] name = "sqlite-wasm-rs" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ "cc", "js-sys", @@ -4900,6 +4948,27 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -5068,9 +5137,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5181,20 +5250,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.11.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5327,9 +5396,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uluru" @@ -5422,9 +5491,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -5500,9 +5569,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -5513,9 +5582,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -5523,9 +5592,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5533,9 +5602,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -5546,9 +5615,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -5589,9 +5658,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -5607,6 +5676,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.7", +] + [[package]] name = "webpki-root-certs" version = "1.0.7" @@ -6016,9 +6094,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "winreg" @@ -6180,18 +6258,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -6200,9 +6278,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/gix-stash/Cargo.toml b/gix-stash/Cargo.toml index 8908c4f7f2b..e2159d8de9b 100644 --- a/gix-stash/Cargo.toml +++ b/gix-stash/Cargo.toml @@ -20,23 +20,20 @@ sha1 = ["gix-hash/sha1", "gix-index/sha1", "gix-object/sha1"] [dependencies] gix-hash = { version = "^0.25.0", path = "../gix-hash" } -gix-object = { version = "^0.60.0", path = "../gix-object" } -gix-index = { version = "^0.51.0", path = "../gix-index" } -gix-ref = { version = "^0.63.0", path = "../gix-ref" } +gix-object = { version = "^0.61.0", path = "../gix-object" } +gix-index = { version = "^0.52.0", path = "../gix-index" } +gix-ref = { version = "^0.64.0", path = "../gix-ref" } gix-actor = { version = "^0.41.0", path = "../gix-actor" } gix-date = { version = "^0.15.3", path = "../gix-date" } -gix-trace = { version = "^0.1.19", path = "../gix-trace" } gix-features = { version = "^0.48.0", path = "../gix-features", features = ["progress"] } gix-path = { version = "^0.12.0", path = "../gix-path" } gix-validate = { version = "^0.11.1", path = "../gix-validate" } -gix-dir = { version = "^0.25.0", path = "../gix-dir" } -gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false, features = ["blob"] } -gix-merge = { version = "^0.16.0", path = "../gix-merge" } -gix-worktree-state = { version = "^0.30.0", path = "../gix-worktree-state" } +gix-diff = { version = "^0.64.0", path = "../gix-diff", default-features = false, features = ["blob"] } +gix-merge = { version = "^0.17.0", path = "../gix-merge" } +gix-worktree-state = { version = "^0.31.0", path = "../gix-worktree-state" } gix-lock = { version = "^23.0.0", path = "../gix-lock" } bstr = { version = "1.12.0", default-features = false, features = ["std"] } -smallvec = "1.15.1" thiserror = "2.0.18" [dev-dependencies] diff --git a/gix-stash/src/lib.rs b/gix-stash/src/lib.rs index d44dcee1039..312663e3481 100644 --- a/gix-stash/src/lib.rs +++ b/gix-stash/src/lib.rs @@ -20,11 +20,11 @@ //! //! # API //! -//! * [`push`] — capture working tree (+ index, + optional untracked) and reset -//! to `HEAD`. -//! * [`pop`] — apply the latest stash to the working tree (3-way merge) and +//! * [`push()`] — capture working tree (+ index, + optional untracked) and +//! reset to `HEAD`. +//! * [`pop()`] — apply the latest stash to the working tree (3-way merge) and //! drop it from `refs/stash`. -//! * [`list`] — walk the `refs/stash` reflog and return every stash entry. +//! * [`list()`] — walk the `refs/stash` reflog and return every stash entry. //! //! All three operate on plumbing handles (index, ODB, ref store, worktree //! path) rather than a high-level repository — the porcelain layer in `gix` diff --git a/gix-stash/src/pop/mod.rs b/gix-stash/src/pop/mod.rs index c64b1b6aaac..1b691a7a996 100644 --- a/gix-stash/src/pop/mod.rs +++ b/gix-stash/src/pop/mod.rs @@ -394,7 +394,29 @@ pub(crate) mod function { } // Remove any existing entry before creating the symlink. let _ = std::fs::remove_file(&entry_path); - std::os::unix::fs::symlink(&target, &entry_path).map_err(|e| super::Error::RestoreUntracked { + + #[cfg(unix)] + let symlink_result = std::os::unix::fs::symlink(&target, &entry_path); + + // Windows symlinks need a kind hint (file vs directory) and + // typically require admin or Developer Mode. We try the file + // variant first; callers that need full symlink semantics on + // Windows can resolve manually using the preserved + // `refs/stash`. The pre-flight scan in + // `collect_restore_targets_recursive` already short-circuits + // before any writes when a target exists, so a partial + // restore here only happens when the file system rejects the + // call (e.g. insufficient privileges). + #[cfg(windows)] + let symlink_result = std::os::windows::fs::symlink_file(&target, &entry_path); + + #[cfg(not(any(unix, windows)))] + let symlink_result = Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "symlink creation is not supported on this platform", + )); + + symlink_result.map_err(|e| super::Error::RestoreUntracked { path: entry_path, source: e, })?;