diff --git a/Cargo.lock b/Cargo.lock index 37581dae9a..bb6940c8c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,6 +675,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.9" @@ -816,6 +830,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -1212,16 +1235,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a61e71ec6817fc3c9f12f812682cfe51ee6ea0d2e27e02fc3849c35524617435" dependencies = [ "gix-actor", + "gix-attributes", + "gix-command", "gix-commitgraph", "gix-config", "gix-date", "gix-diff", + "gix-dir", "gix-discover", "gix-features", + "gix-filter", "gix-fs", "gix-glob", "gix-hash", "gix-hashtable", + "gix-ignore", "gix-index", "gix-lock", "gix-mailmap", @@ -1229,6 +1257,7 @@ dependencies = [ "gix-odb", "gix-pack", "gix-path", + "gix-pathspec", "gix-protocol", "gix-ref", "gix-refspec", @@ -1236,12 +1265,15 @@ dependencies = [ "gix-revwalk", "gix-sec", "gix-shallow", + "gix-status", + "gix-submodule", "gix-tempfile", "gix-trace", "gix-traverse", "gix-url", "gix-utils", "gix-validate", + "gix-worktree", "once_cell", "smallvec", "thiserror 2.0.12", @@ -1261,6 +1293,23 @@ dependencies = [ "winnow", ] +[[package]] +name = "gix-attributes" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e25825e0430aa11096f8b65ced6780d4a96a133f81904edceebb5344c8dd7f" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-quote", + "gix-trace", + "kstring", + "smallvec", + "thiserror 2.0.12", + "unicode-bom", +] + [[package]] name = "gix-bitmap" version = "0.2.14" @@ -1358,8 +1407,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c975dad2afc85e4e233f444d1efbe436c3cdcf3a07173984509c436d00a3f8" dependencies = [ "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", "gix-hash", + "gix-index", "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-dir" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5879497bd3815d8277ed864ec8975290a70de5b62bb92d2d666a4cefc5d4793b" +dependencies = [ + "bstr", + "gix-discover", + "gix-fs", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-trace", + "gix-utils", + "gix-worktree", "thiserror 2.0.12", ] @@ -1399,6 +1480,27 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gix-filter" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2b2bbffdc5cc9b2b82fc82da1b98163c9b423ac2b45348baa83a947ac9ab89" +dependencies = [ + "bstr", + "encoding_rs", + "gix-attributes", + "gix-command", + "gix-hash", + "gix-object", + "gix-packetline-blocking", + "gix-path", + "gix-quote", + "gix-trace", + "gix-utils", + "smallvec", + "thiserror 2.0.12", +] + [[package]] name = "gix-fs" version = "0.14.0" @@ -1448,6 +1550,19 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "gix-ignore" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a27c8380f493a10d1457f756a3f81924d578fc08d6535e304dfcafbf0261d18" +dependencies = [ + "bstr", + "gix-glob", + "gix-path", + "gix-trace", + "unicode-bom", +] + [[package]] name = "gix-index" version = "0.39.0" @@ -1572,6 +1687,18 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "gix-packetline-blocking" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecf3ea2e105c7e45587bac04099824301262a6c43357fad5205da36dbb233b3" +dependencies = [ + "bstr", + "faster-hex", + "gix-trace", + "thiserror 2.0.12", +] + [[package]] name = "gix-path" version = "0.10.15" @@ -1585,6 +1712,21 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "gix-pathspec" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8422c3c9066d649074b24025125963f85232bfad32d6d16aea9453b82ec14" +dependencies = [ + "bitflags 2.9.1", + "bstr", + "gix-attributes", + "gix-config-value", + "gix-glob", + "gix-path", + "thiserror 2.0.12", +] + [[package]] name = "gix-protocol" version = "0.49.0" @@ -1707,12 +1849,51 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "gix-status" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "605a6d0eb5891680c46e24b2ee7a63ef7bd39cb136dc7c7e55172960cf68b2f5" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-submodule" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c7390c2059505c365e9548016d4edc9f35749c6a9112b7b1214400bbc68da2" +dependencies = [ + "bstr", + "gix-config", + "gix-path", + "gix-pathspec", + "gix-refspec", + "gix-url", + "thiserror 2.0.12", +] + [[package]] name = "gix-tempfile" version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6de439bbb9a5d3550c9c7fab0e16d2d637d120fcbe0dfbc538772a187f099b" dependencies = [ + "dashmap", "gix-fs", "libc", "once_cell", @@ -1779,6 +1960,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "189f8724cf903e7fd57cfe0b7bc209db255cacdcb22c781a022f52c3a774f8d0" dependencies = [ + "bstr", "fastrand", "unicode-normalization", ] @@ -1793,6 +1975,25 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "gix-worktree" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7760dbc4b79aa274fed30adc0d41dca6b917641f26e7867c4071b1fb4dc727b" +dependencies = [ + "bstr", + "gix-attributes", + "gix-features", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-ignore", + "gix-index", + "gix-object", + "gix-path", + "gix-validate", +] + [[package]] name = "group" version = "0.13.0" @@ -2017,6 +2218,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imara-diff" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" +dependencies = [ + "hashbrown 0.15.2", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -2190,6 +2400,15 @@ dependencies = [ "libc", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index e0eaa7f560..5ca1f2ad73 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -28,6 +28,7 @@ gix = { version = "0.71.0", default-features = false, features = [ "max-performance", "revision", "mailmap", + "status", ] } log = "0.4" # git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]} diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index 1578ed1e50..ffd0bd899b 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -123,6 +123,44 @@ pub enum Error { #[from] gix::object::find::existing::with_conversion::Error, ), + /// + #[error("gix::pathspec::init::Error error: {0}")] + GixPathspecInit(#[from] Box), + + /// + #[error("gix::reference::head_tree_id::Error error: {0}")] + GixReferenceHeadTreeId( + #[from] gix::reference::head_tree_id::Error, + ), + + /// + #[error("gix::status::Error error: {0}")] + GixStatus(#[from] Box), + + /// + #[error("gix::status::iter::Error error: {0}")] + GixStatusIter(#[from] Box), + + /// + #[error("gix::status::into_iter::Error error: {0}")] + GixStatusIntoIter(#[from] Box), + + /// + #[error("gix::status::index_worktree::Error error: {0}")] + GixStatusIndexWorktree( + #[from] Box, + ), + + /// + #[error("gix::status::tree_index::Error error: {0}")] + GixStatusTreeIndex(#[from] Box), + + /// + #[error("gix::worktree::open_index::Error error: {0}")] + GixWorktreeOpenIndex( + #[from] Box, + ), + /// #[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")] SignAmendNonLastCommit, @@ -156,3 +194,45 @@ impl From for Error { Self::GixDiscover(Box::new(error)) } } + +impl From for Error { + fn from(error: gix::pathspec::init::Error) -> Self { + Self::GixPathspecInit(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::status::Error) -> Self { + Self::GixStatus(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::status::iter::Error) -> Self { + Self::GixStatusIter(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::status::into_iter::Error) -> Self { + Self::GixStatusIntoIter(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::status::index_worktree::Error) -> Self { + Self::GixStatusIndexWorktree(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::status::tree_index::Error) -> Self { + Self::GixStatusTreeIndex(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::worktree::open_index::Error) -> Self { + Self::GixWorktreeOpenIndex(Box::new(error)) + } +} diff --git a/asyncgit/src/sync/config.rs b/asyncgit/src/sync/config.rs index 9b5d85d823..f15e9cf937 100644 --- a/asyncgit/src/sync/config.rs +++ b/asyncgit/src/sync/config.rs @@ -56,6 +56,13 @@ pub fn untracked_files_config_repo( } } + // This does not reflect how git works according to its docs that say: "If this variable is not + // specified, it defaults to `normal`." + // + // https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles + // + // Note that this might become less relevant over time as more code gets migrated to `gitoxide` + // because `gitoxide` respects `status.showUntrackedFiles` by default. Ok(ShowUntrackedFilesConfig::All) } diff --git a/asyncgit/src/sync/status.rs b/asyncgit/src/sync/status.rs index 1cd5bcc847..9a2f57a421 100644 --- a/asyncgit/src/sync/status.rs +++ b/asyncgit/src/sync/status.rs @@ -1,7 +1,6 @@ //! sync git api for fetching a status use crate::{ - error::Error, error::Result, sync::{config::untracked_files_config_repo, repository::repo}, }; @@ -28,6 +27,40 @@ pub enum StatusItemType { Conflicted, } +impl From + for StatusItemType +{ + fn from( + summary: gix::status::index_worktree::iter::Summary, + ) -> Self { + use gix::status::index_worktree::iter::Summary; + + match summary { + Summary::Removed => Self::Deleted, + Summary::Added + | Summary::Copied + | Summary::IntentToAdd => Self::New, + Summary::Modified => Self::Modified, + Summary::TypeChange => Self::Typechange, + Summary::Renamed => Self::Renamed, + Summary::Conflict => Self::Conflicted, + } + } +} + +impl From> for StatusItemType { + fn from(change_ref: gix::diff::index::ChangeRef) -> Self { + use gix::diff::index::ChangeRef; + + match change_ref { + ChangeRef::Addition { .. } => Self::New, + ChangeRef::Deletion { .. } => Self::Deleted, + ChangeRef::Modification { .. } + | ChangeRef::Rewrite { .. } => Self::Modified, + } + } +} + impl From for StatusItemType { fn from(s: Status) -> Self { if s.is_index_new() || s.is_wt_new() { @@ -126,6 +159,16 @@ pub fn is_workdir_clean( Ok(statuses.is_empty()) } +impl From for gix::status::UntrackedFiles { + fn from(value: ShowUntrackedFilesConfig) -> Self { + match value { + ShowUntrackedFilesConfig::All => Self::Files, + ShowUntrackedFilesConfig::Normal => Self::Collapsed, + ShowUntrackedFilesConfig::No => Self::None, + } + } +} + /// guarantees sorting pub fn get_status( repo_path: &RepoPath, @@ -134,59 +177,93 @@ pub fn get_status( ) -> Result> { scope_time!("get_status"); - let repo = repo(repo_path)?; + let repo: gix::Repository = + gix::ThreadSafeRepository::discover_with_environment_overrides(repo_path.gitpath()) + .map(Into::into)?; - if repo.is_bare() && !repo.is_worktree() { - return Ok(Vec::new()); + let mut status = repo.status(gix::progress::Discard)?; + + if let Some(config) = show_untracked { + status = status.untracked_files(config.into()); } - let show_untracked = if let Some(config) = show_untracked { - config - } else { - untracked_files_config_repo(&repo)? - }; + let mut res = Vec::new(); - let mut options = StatusOptions::default(); - options - .show(status_type.into()) - .update_index(true) - .include_untracked(show_untracked.include_untracked()) - .renames_head_to_index(true) - .recurse_untracked_dirs( - show_untracked.recurse_untracked_dirs(), - ); + match status_type { + StatusType::WorkingDir => { + let iter = status.into_index_worktree_iter(Vec::new())?; - let statuses = repo.statuses(Some(&mut options))?; + for item in iter { + let item = item?; + + let status = item.summary().map(Into::into); + + if let Some(status) = status { + let path = item.rela_path().to_string(); + + res.push(StatusItem { path, status }); + } + } + } + StatusType::Stage => { + let tree_id: gix::ObjectId = + repo.head_tree_id_or_empty()?.into(); + let worktree_index = + gix::worktree::IndexPersistedOrInMemory::Persisted( + repo.index_or_empty()?, + ); + + let mut pathspec = repo.pathspec( + false, /* empty patterns match prefix */ + None::<&str>, + true, /* inherit ignore case */ + &gix::index::State::new(repo.object_hash()), + gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping + )?; + + let cb = + |change_ref: gix::diff::index::ChangeRef<'_, '_>, + _: &gix::index::State, + _: &gix::index::State| + -> Result { + let path = change_ref.fields().0.to_string(); + let status = change_ref.into(); - let mut res = Vec::with_capacity(statuses.len()); - - for e in statuses.iter() { - let status: Status = e.status(); - - let path = match e.head_to_index() { - Some(diff) => diff - .new_file() - .path() - .and_then(Path::to_str) - .map(String::from) - .ok_or_else(|| { - Error::Generic( - "failed to get path to diff's new file." - .to_string(), - ) - })?, - None => e.path().map(String::from).ok_or_else(|| { - Error::Generic( - "failed to get the path to indexed file." - .to_string(), - ) - })?, - }; - - res.push(StatusItem { - path, - status: StatusItemType::from(status), - }); + res.push(StatusItem { path, status }); + + Ok(gix::diff::index::Action::Continue) + }; + + repo.tree_index_status( + &tree_id, + &worktree_index, + Some(&mut pathspec), + gix::status::tree_index::TrackRenames::default(), + cb, + )?; + } + StatusType::Both => { + let iter = status.into_iter(Vec::new())?; + + for item in iter { + let item = item?; + + let path = item.location().to_string(); + + let status = match item { + gix::status::Item::IndexWorktree(item) => { + item.summary().map(Into::into) + } + gix::status::Item::TreeIndex(change_ref) => { + Some(change_ref.into()) + } + }; + + if let Some(status) = status { + res.push(StatusItem { path, status }); + } + } + } } res.sort_by(|a, b| { diff --git a/asyncgit/src/sync/utils.rs b/asyncgit/src/sync/utils.rs index ebae31beb0..3250de69e9 100644 --- a/asyncgit/src/sync/utils.rs +++ b/asyncgit/src/sync/utils.rs @@ -230,6 +230,7 @@ mod tests { }, }; use std::{ + env, fs::{self, remove_file, File}, io::Write, path::Path, @@ -270,6 +271,44 @@ mod tests { assert_eq!(get_statuses(repo_path), (1, 1)); } + #[test] + fn test_staging_one_file_from_different_sub_directory() { + // This test case covers an interaction between current working directory and the way + // `gitoxide` handles pathspecs. + // + // When staging a new file in one sub-directory, then running running `get_status` in a + // different sub-directory, `repo.pathspec` in `get_status` has to initialized with + // `empty_patterns_match_prefix` set to `false` for `get_status` to report the staged file’s + // status. + let file_path = Path::new("untracked/file1.txt"); + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + fs::create_dir(root.join("untracked")).unwrap(); + + File::create(root.join(file_path)) + .unwrap() + .write_all(b"test file1 content") + .unwrap(); + + let sub_dir_path = root.join("unrelated"); + + fs::create_dir(root.join("unrelated")).unwrap(); + + let current_dir = env::current_dir().unwrap(); + env::set_current_dir(sub_dir_path).unwrap(); + + assert_eq!(get_statuses(repo_path), (1, 0)); + + stage_add_file(repo_path, file_path).unwrap(); + + assert_eq!(get_statuses(repo_path), (0, 1)); + + env::set_current_dir(current_dir).unwrap(); + } + #[test] fn test_staging_folder() -> Result<()> { let (_td, repo) = repo_init().unwrap(); @@ -289,6 +328,8 @@ mod tests { File::create(root.join(Path::new("a/f3.txt")))? .write_all(b"foo")?; + repo.config()?.set_str("status.showUntrackedFiles", "all")?; + assert_eq!(status_count(StatusType::WorkingDir), 3); stage_add_all(repo_path, "a/d", None).unwrap(); @@ -351,6 +392,8 @@ mod tests { File::create(root.join(Path::new("f3.txt")))? .write_all(b"foo")?; + repo.config()?.set_str("status.showUntrackedFiles", "all")?; + assert_eq!(get_statuses(repo_path), (3, 0)); repo.config()?.set_str("status.showUntrackedFiles", "no")?;