From 5783df24df627cf6993a59e5dbaedef4e31a2d0b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 14:27:14 +0200 Subject: [PATCH 01/18] Add remaining docs to get `gix-fs` into 'early' mode. --- README.md | 2 +- gix-fs/src/lib.rs | 3 ++- gix-fs/src/symlink.rs | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b143b1097b9..df862b6c8fa 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ is usable to some extent. * [gix-command](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-command) * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt) * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) + * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) * `gitoxide-core` * **very early** _(possibly without any documentation and many rough edges)_ * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) - * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) diff --git a/gix-fs/src/lib.rs b/gix-fs/src/lib.rs index 19128b7d4e1..aa576c24062 100644 --- a/gix-fs/src/lib.rs +++ b/gix-fs/src/lib.rs @@ -1,5 +1,5 @@ //! A crate with file-system specific utilities. -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] /// Common knowledge about the worktree that is needed across most interactions with the work tree @@ -51,4 +51,5 @@ pub struct Stack { current_is_directory: bool, } +/// pub mod stack; diff --git a/gix-fs/src/symlink.rs b/gix-fs/src/symlink.rs index e5bf5371d8e..55798daa3e9 100644 --- a/gix-fs/src/symlink.rs +++ b/gix-fs/src/symlink.rs @@ -1,16 +1,21 @@ use std::{io, io::ErrorKind::AlreadyExists, path::Path}; #[cfg(not(windows))] +/// Create a new symlink at `link` which points to `original`. pub fn create(original: &Path, link: &Path) -> io::Result<()> { std::os::unix::fs::symlink(original, link) } #[cfg(not(windows))] +/// Remove a symlink. +/// +/// Note that on only on windows this is special. pub fn remove(path: &Path) -> io::Result<()> { std::fs::remove_file(path) } // TODO: use the `symlink` crate once it can delete directory symlinks +/// Remove a symlink. #[cfg(windows)] pub fn remove(path: &Path) -> io::Result<()> { if let Ok(meta) = std::fs::metadata(path) { @@ -25,6 +30,7 @@ pub fn remove(path: &Path) -> io::Result<()> { } #[cfg(windows)] +/// Create a new symlink at `link` which points to `original`. pub fn create(original: &Path, link: &Path) -> io::Result<()> { use std::os::windows::fs::{symlink_dir, symlink_file}; // TODO: figure out if links to links count as files or whatever they point at @@ -36,6 +42,8 @@ pub fn create(original: &Path, link: &Path) -> io::Result<()> { } #[cfg(not(windows))] +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` +/// already exists as filesystem object. pub fn is_collision_error(err: &std::io::Error) -> bool { // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively err.kind() == AlreadyExists @@ -45,6 +53,8 @@ pub fn is_collision_error(err: &std::io::Error) -> bool { } #[cfg(windows)] +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` +/// already exists as filesystem object. pub fn is_collision_error(err: &std::io::Error) -> bool { err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied } From 0f3b65fdc210aded0a4e4ab72267e81141509122 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 14:30:47 +0200 Subject: [PATCH 02/18] Indicate that `gix-utils` is in early mode. --- README.md | 2 +- gix-utils/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df862b6c8fa..cbe2d26f0a4 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,9 @@ is usable to some extent. * [gix-prompt](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-prompt) * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) + * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) * `gitoxide-core` * **very early** _(possibly without any documentation and many rough edges)_ - * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) diff --git a/gix-utils/src/lib.rs b/gix-utils/src/lib.rs index 47c02cd91fe..4c9d99fa423 100644 --- a/gix-utils/src/lib.rs +++ b/gix-utils/src/lib.rs @@ -1,7 +1,7 @@ //! A crate with utilities that don't need feature toggles. //! //! If they would need feature toggles, they should be in `gix-features` instead. -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] /// From 1f6592759ce88082dfc5c668ac2a71e280c65bfa Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 14:35:27 +0200 Subject: [PATCH 03/18] Advance `gix-hashtable` to early mode --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbe2d26f0a4..903bce8f48e 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,12 @@ is usable to some extent. * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) + * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable) * `gitoxide-core` * **very early** _(possibly without any documentation and many rough edges)_ * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) - * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable) * **idea** _(just a name placeholder)_ * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive) * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note) From bdd505a289fc4bf29563cb8622eae577caa30b41 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 14:39:37 +0200 Subject: [PATCH 04/18] Advance `gix-bitmap` to 'early' stage --- README.md | 6 +++--- gix-bitmap/src/ewah.rs | 4 ++++ gix-bitmap/src/lib.rs | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 903bce8f48e..93813b1a6c5 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,13 @@ is usable to some extent. * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) * [gix-hashtable](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-hashtable) - * `gitoxide-core` -* **very early** _(possibly without any documentation and many rough edges)_ * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) + * `gitoxide-core` +* **very early** _(possibly without any documentation and many rough edges)_ * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) -* **idea** _(just a name placeholder)_ * [gix-archive](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-archive) +* **idea** _(just a name placeholder)_ * [gix-note](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-note) * [gix-fetchhead](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fetchhead) * [gix-filter](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-filter) diff --git a/gix-bitmap/src/ewah.rs b/gix-bitmap/src/ewah.rs index 6958f044146..e2575f90500 100644 --- a/gix-bitmap/src/ewah.rs +++ b/gix-bitmap/src/ewah.rs @@ -1,13 +1,17 @@ use std::convert::TryInto; +/// pub mod decode { + /// The error returned by [`decode()`][super::decode()]. #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] pub enum Error { #[error("{}", message)] Corrupt { message: &'static str }, } } +/// Decode `data` as EWAH bitmap. pub fn decode(data: &[u8]) -> Result<(Vec, &[u8]), decode::Error> { use self::decode::Error; use crate::decode; diff --git a/gix-bitmap/src/lib.rs b/gix-bitmap/src/lib.rs index 9148b3f1bad..83756d98897 100644 --- a/gix-bitmap/src/lib.rs +++ b/gix-bitmap/src/lib.rs @@ -1,8 +1,7 @@ //! An implementation of the shared parts of git bitmaps used in `gix-pack`, `gix-index` and `gix-worktree`. //! //! Note that many tests are performed indirectly by tests in the aforementioned consumer crates. -#![deny(rust_2018_idioms, unsafe_code)] -#![allow(missing_docs)] +#![deny(rust_2018_idioms, unsafe_code, missing_docs)] /// Bitmap utilities for the advanced word-aligned hybrid bitmap pub mod ewah; From 21b4e676ab094850ba808e085e2e1c3b1eb2eb61 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 08:33:33 +0200 Subject: [PATCH 05/18] Remove duplicate usage of `case` in `cache::state::Attributes|Ignore` types. It was mainly added for testing, even though there are better ways to test this without introducing redundancy for everyone. --- gix-worktree/src/cache/delegate.rs | 5 +++++ gix-worktree/src/cache/mod.rs | 1 + gix-worktree/src/cache/state/attributes.rs | 14 ++------------ gix-worktree/src/cache/state/ignore.rs | 19 ++++++++----------- gix-worktree/src/cache/state/mod.rs | 3 --- gix-worktree/src/checkout/function.rs | 2 +- .../tests/worktree/cache/attributes.rs | 1 - gix-worktree/tests/worktree/cache/ignore.rs | 2 -- 8 files changed, 17 insertions(+), 30 deletions(-) diff --git a/gix-worktree/src/cache/delegate.rs b/gix-worktree/src/cache/delegate.rs index 7b48eaa9dee..272b537476e 100644 --- a/gix-worktree/src/cache/delegate.rs +++ b/gix-worktree/src/cache/delegate.rs @@ -7,6 +7,7 @@ pub struct StackDelegate<'a, Find> { pub is_dir: bool, pub id_mappings: &'a Vec, pub find: Find, + pub case: gix_glob::pattern::Case, } impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find> @@ -23,6 +24,7 @@ where self.buf, self.id_mappings, &mut self.find, + self.case, )?; } State::AttributesAndIgnoreStack { ignore, attributes } => { @@ -32,6 +34,7 @@ where self.buf, self.id_mappings, &mut self.find, + self.case, )?; ignore.push_directory( stack.root(), @@ -39,6 +42,7 @@ where self.buf, self.id_mappings, &mut self.find, + self.case, )? } State::IgnoreStack(ignore) => ignore.push_directory( @@ -47,6 +51,7 @@ where self.buf, self.id_mappings, &mut self.find, + self.case, )?, } Ok(()) diff --git a/gix-worktree/src/cache/mod.rs b/gix-worktree/src/cache/mod.rs index 469468dd543..829f2909e64 100644 --- a/gix-worktree/src/cache/mod.rs +++ b/gix-worktree/src/cache/mod.rs @@ -116,6 +116,7 @@ impl Cache { is_dir: is_dir.unwrap_or(false), id_mappings: &self.id_mappings, find, + case: self.case, }; self.stack.make_relative_path_current(relative, &mut delegate)?; Ok(Platform { parent: self, is_dir }) diff --git a/gix-worktree/src/cache/state/attributes.rs b/gix-worktree/src/cache/state/attributes.rs index 5914c8404bc..edc902d45cd 100644 --- a/gix-worktree/src/cache/state/attributes.rs +++ b/gix-worktree/src/cache/state/attributes.rs @@ -42,7 +42,6 @@ impl Attributes { pub fn new( globals: AttributeMatchGroup, info_attributes: Option, - case: Case, source: Source, collection: gix_attributes::search::MetadataCollection, ) -> Self { @@ -50,7 +49,6 @@ impl Attributes { globals, stack: Default::default(), info_attributes, - case, source, collection, } @@ -69,6 +67,7 @@ impl Attributes { buf: &mut Vec, id_mappings: &[PathIdMapping], mut find: Find, + case: Case, ) -> std::io::Result<()> where Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec) -> Result, E>, @@ -79,7 +78,7 @@ impl Attributes { gix_path::into_bstr(root).as_ref(), dir_bstr.as_ref(), None, - self.case, + case, ) .expect("dir in root") .0; @@ -168,15 +167,6 @@ impl Attributes { } } -/// Builder -impl Attributes { - /// Set the case to use when matching attributes to paths. - pub fn with_case(mut self, case: gix_glob::pattern::Case) -> Self { - self.case = case; - self - } -} - /// Attribute matching specific methods impl Cache { /// Creates a new container to store match outcomes for all attribute matches. diff --git a/gix-worktree/src/cache/state/ignore.rs b/gix-worktree/src/cache/state/ignore.rs index ce6fb65adc9..9013a82a734 100644 --- a/gix-worktree/src/cache/state/ignore.rs +++ b/gix-worktree/src/cache/state/ignore.rs @@ -4,7 +4,8 @@ use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; use bstr::{BStr, BString, ByteSlice}; use gix_glob::pattern::Case; -/// State related to the exclusion of files. +/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read +/// ignore files from disk or from the index each time the directory changes. #[derive(Default, Clone)] #[allow(unused)] pub struct Ignore { @@ -21,25 +22,20 @@ pub struct Ignore { matched_directory_patterns_stack: Vec>, /// The name of the file to look for in directories. pub(crate) exclude_file_name_for_directories: BString, - /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine - /// to know if an entire path can be ignored as a parent directory is ignored. - pub(crate) case: Case, } impl Ignore { + /// Configure gitignore file matching by providing the immutable groups being `overrides` and `globals`, while letting the directory + /// stack be dynamic. + /// /// The `exclude_file_name_for_directories` is an optional override for the filename to use when checking per-directory /// ignore files within the repository, defaults to`.gitignore`. - /// - // This is what it should be able represent: https://github.com/git/git/blob/140b9478dad5d19543c1cb4fd293ccec228f1240/dir.c#L3354 - // TODO: more docs pub fn new( overrides: IgnoreMatchGroup, globals: IgnoreMatchGroup, exclude_file_name_for_directories: Option<&BStr>, - case: Case, ) -> Self { Ignore { - case, overrides, globals, stack: Default::default(), @@ -142,6 +138,7 @@ impl Ignore { buf: &mut Vec, id_mappings: &[PathIdMapping], mut find: Find, + case: Case, ) -> std::io::Result<()> where Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec) -> Result, E>, @@ -152,7 +149,7 @@ impl Ignore { gix_path::into_bstr(root).as_ref(), dir_bstr.as_ref(), None, - self.case, + case, ) .expect("dir in root") .0; @@ -160,7 +157,7 @@ impl Ignore { rela_dir = &rela_dir[1..]; } self.matched_directory_patterns_stack - .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), self.case)); + .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case)); let ignore_path_relative = gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); diff --git a/gix-worktree/src/cache/state/mod.rs b/gix-worktree/src/cache/state/mod.rs index f2d7c66e9e6..779b5414742 100644 --- a/gix-worktree/src/cache/state/mod.rs +++ b/gix-worktree/src/cache/state/mod.rs @@ -23,9 +23,6 @@ pub struct Attributes { info_attributes: Option, /// A lookup table to accelerate searches. collection: gix_attributes::search::MetadataCollection, - /// The case to use when matching directories as they are pushed onto the stack. We run them against the exclude engine - /// to know if an entire path can be ignored as a parent directory is ignored. - case: Case, /// Where to read `.gitattributes` data from. source: attributes::Source, } diff --git a/gix-worktree/src/checkout/function.rs b/gix-worktree/src/checkout/function.rs index 1e6ebdae585..089938795e1 100644 --- a/gix-worktree/src/checkout/function.rs +++ b/gix-worktree/src/checkout/function.rs @@ -57,7 +57,7 @@ where None, ); - let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone().with_case(case)); + let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone()); let attribute_files = state.id_mappings_from_index(index, paths, case); let mut ctx = chunk::Context { buf: Vec::new(), diff --git a/gix-worktree/tests/worktree/cache/attributes.rs b/gix-worktree/tests/worktree/cache/attributes.rs index 8601f573c17..b7d9f55d8f6 100644 --- a/gix-worktree/tests/worktree/cache/attributes.rs +++ b/gix-worktree/tests/worktree/cache/attributes.rs @@ -24,7 +24,6 @@ fn baseline() -> crate::Result { state::Attributes::new( gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?, Some(git_dir.join("info").join("attributes")), - case, gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, collection, ), diff --git a/gix-worktree/tests/worktree/cache/ignore.rs b/gix-worktree/tests/worktree/cache/ignore.rs index 8dbf80907cb..393a86ad776 100644 --- a/gix-worktree/tests/worktree/cache/ignore.rs +++ b/gix-worktree/tests/worktree/cache/ignore.rs @@ -43,7 +43,6 @@ fn special_exclude_cases_we_handle_differently() { Default::default(), gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(), None, - case, ), ); let mut cache = Cache::new(&dir, state, case, buf, Default::default()); @@ -104,7 +103,6 @@ fn check_against_baseline() -> crate::Result { gix_ignore::Search::from_overrides(vec!["!force-include"]), gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, None, - case, ), ); let paths_storage = index.take_path_backing(); From f722d6bebcd215b6e270261a3ed032a5f7e7b72f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 08:45:33 +0200 Subject: [PATCH 06/18] adjust to changes in `gix-worktree` --- gix/src/config/cache/access.rs | 9 --------- gix/src/worktree/mod.rs | 1 - 2 files changed, 10 deletions(-) diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index b92b5289c21..3b23e2821ba 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -2,7 +2,6 @@ use std::{borrow::Cow, path::PathBuf, time::Duration}; use gix_attributes::Source; -use gix_glob::pattern::Case; use gix_lock::acquire::Fail; use crate::{ @@ -167,15 +166,9 @@ impl Cache { executable_bit: boolean(self, "core.fileMode", &Core::FILE_MODE, true)?, symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?, }; - let case = if capabilities.ignore_case { - Case::Fold - } else { - Case::Sensitive - }; Ok(gix_worktree::checkout::Options { attributes: self.assemble_attribute_globals( git_dir, - case, gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, self.attributes, )?, @@ -203,7 +196,6 @@ impl Cache { fn assemble_attribute_globals( &self, git_dir: &std::path::Path, - case: gix_glob::pattern::Case, source: gix_worktree::cache::state::attributes::Source, attributes: crate::permissions::Attributes, ) -> Result { @@ -235,7 +227,6 @@ impl Cache { Ok(gix_worktree::cache::state::Attributes::new( gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, Some(info_attributes_path), - case, source, collection, )) diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index b32aa1661ce..b73f3be6130 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -149,7 +149,6 @@ pub mod excludes { overrides.unwrap_or_default(), gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?, None, - case, )); let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list)) From ca8ebdfb9647ff15b0293823bb3bb3c6779c8dd2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 08:57:53 +0200 Subject: [PATCH 07/18] feat!: turn `gix free index entries` into `gix index entries`. --- gitoxide-core/src/index/mod.rs | 3 --- .../src/{ => repository}/index/entries.rs | 22 ++++++------------- .../src/repository/{index.rs => index/mod.rs} | 3 +++ src/plumbing/main.rs | 20 ++++++++--------- src/plumbing/options/free.rs | 2 -- src/plumbing/options/mod.rs | 2 ++ 6 files changed, 22 insertions(+), 30 deletions(-) rename gitoxide-core/src/{ => repository}/index/entries.rs (75%) rename gitoxide-core/src/repository/{index.rs => index/mod.rs} (96%) diff --git a/gitoxide-core/src/index/mod.rs b/gitoxide-core/src/index/mod.rs index 12b23f74332..df75cf541b6 100644 --- a/gitoxide-core/src/index/mod.rs +++ b/gitoxide-core/src/index/mod.rs @@ -5,9 +5,6 @@ pub struct Options { pub format: crate::OutputFormat, } -mod entries; -pub use entries::entries; - pub mod information; fn parse_file(index_path: impl AsRef, object_hash: gix::hash::Kind) -> anyhow::Result { diff --git a/gitoxide-core/src/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs similarity index 75% rename from gitoxide-core/src/index/entries.rs rename to gitoxide-core/src/repository/index/entries.rs index 07a1481209c..05227dd88d2 100644 --- a/gitoxide-core/src/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -1,26 +1,18 @@ -use std::path::Path; - -use crate::index::{parse_file, Options}; - -pub fn entries( - index_path: impl AsRef, - mut out: impl std::io::Write, - Options { object_hash, format }: Options, -) -> anyhow::Result<()> { +pub fn entries(repo: gix::Repository, mut out: impl std::io::Write, format: crate::OutputFormat) -> anyhow::Result<()> { use crate::OutputFormat::*; - let file = parse_file(index_path, object_hash)?; + let index = repo.index()?; #[cfg(feature = "serde")] if let Json = format { out.write_all(b"[\n")?; } - let mut entries = file.entries().iter().peekable(); + let mut entries = index.entries().iter().peekable(); while let Some(entry) = entries.next() { match format { - Human => to_human(&mut out, &file, entry)?, + Human => to_human(&mut out, &index, entry)?, #[cfg(feature = "serde")] - Json => to_json(&mut out, &file, entry, entries.peek().is_none())?, + Json => to_json(&mut out, &index, entry, entries.peek().is_none())?, } } @@ -34,7 +26,7 @@ pub fn entries( #[cfg(feature = "serde")] pub(crate) fn to_json( mut out: &mut impl std::io::Write, - file: &gix::index::File, + index: &gix::index::File, entry: &gix::index::Entry, is_last: bool, ) -> anyhow::Result<()> { @@ -56,7 +48,7 @@ pub(crate) fn to_json( hex_id: entry.id.to_hex().to_string(), flags: entry.flags.bits(), mode: entry.mode.bits(), - path: entry.path(file).to_str_lossy(), + path: entry.path(index).to_str_lossy(), }, )?; diff --git a/gitoxide-core/src/repository/index.rs b/gitoxide-core/src/repository/index/mod.rs similarity index 96% rename from gitoxide-core/src/repository/index.rs rename to gitoxide-core/src/repository/index/mod.rs index d14fca2227e..7e1b3c274da 100644 --- a/gitoxide-core/src/repository/index.rs +++ b/gitoxide-core/src/repository/index/mod.rs @@ -34,3 +34,6 @@ pub fn from_tree( Ok(()) } + +mod entries; +pub use entries::entries; diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 3f35d5b2a0d..ac05d3964fc 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -339,16 +339,6 @@ pub fn main() -> Result<()> { ) }, ), - free::index::Subcommands::Entries => prepare_and_run( - "index-entries", - verbose, - progress, - progress_keep_open, - None, - move |_progress, out, _err| { - core::index::entries(index_path, out, core::index::Options { object_hash, format }) - }, - ), free::index::Subcommands::Verify => prepare_and_run( "index-verify", auto_verbose, @@ -865,6 +855,16 @@ pub fn main() -> Result<()> { ), }, Subcommands::Index(cmd) => match cmd { + index::Subcommands::Entries => prepare_and_run( + "index-entries", + verbose, + progress, + progress_keep_open, + None, + move |_progress, out, _err| { + core::repository::index::entries(repository(Mode::LenientWithGitInstallConfig)?, out, format) + }, + ), index::Subcommands::FromTree { force, index_output_path, diff --git a/src/plumbing/options/free.rs b/src/plumbing/options/free.rs index 8bff740a3ef..00641f33b28 100644 --- a/src/plumbing/options/free.rs +++ b/src/plumbing/options/free.rs @@ -55,8 +55,6 @@ pub mod index { pub enum Subcommands { /// Validate constraints and assumptions of an index along with its integrity. Verify, - /// Print all entries to standard output - Entries, /// Print information about the index structure Info { /// Do not extract specific extension information to gain only a superficial idea of the index's composition. diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 03adad04590..d2b8d7805d2 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -471,6 +471,8 @@ pub mod index { #[derive(Debug, clap::Subcommand)] pub enum Subcommands { + /// Print all entries to standard output + Entries, /// Create an index from a tree-ish. #[clap(visible_alias = "read-tree")] FromTree { From dd14a80a78fea3f092b1d5300637c80cfd9e59a7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 09:09:31 +0200 Subject: [PATCH 08/18] fix: printing to stdout for commands that don't show progress is greatly improved. Previously it would have to lock `stdout` on each write, now this is done only once. --- src/shared.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shared.rs b/src/shared.rs index 64c602b3f95..5fc2ab8eeac 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -113,7 +113,11 @@ pub mod pretty { crate::shared::init_env_logger(); match (verbose, progress) { - (false, false) => run(progress::DoOrDiscard::from(None), &mut stdout(), &mut stderr()), + (false, false) => { + let stdout = stdout(); + let mut stdout_lock = stdout.lock(); + run(progress::DoOrDiscard::from(None), &mut stdout_lock, &mut stderr()) + } (true, false) => { use crate::shared::{self, STANDARD_RANGE}; let progress = shared::progress_tree(); From 40a1b7444ba9d9b61a1c22a7f25662eec3c25a1b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 09:36:35 +0200 Subject: [PATCH 09/18] feat: add `index.threads` configuration to `gix::config::tree` --- gix/src/config/tree/mod.rs | 9 ++-- gix/src/config/tree/sections/index.rs | 62 +++++++++++++++++++++++++++ gix/src/config/tree/sections/mod.rs | 5 +++ gix/src/repository/worktree.rs | 18 +++----- gix/src/worktree/mod.rs | 10 +---- gix/tests/config/tree.rs | 24 +++++++++++ 6 files changed, 104 insertions(+), 24 deletions(-) create mode 100644 gix/src/config/tree/sections/index.rs diff --git a/gix/src/config/tree/mod.rs b/gix/src/config/tree/mod.rs index fd769f3ed9c..b378b8c49ca 100644 --- a/gix/src/config/tree/mod.rs +++ b/gix/src/config/tree/mod.rs @@ -38,6 +38,8 @@ pub(crate) mod root { pub const GITOXIDE: sections::Gitoxide = sections::Gitoxide; /// The `http` section. pub const HTTP: sections::Http = sections::Http; + /// The `index` section. + pub const INDEX: sections::Index = sections::Index; /// The `init` section. pub const INIT: sections::Init = sections::Init; /// The `pack` section. @@ -69,6 +71,7 @@ pub(crate) mod root { &Self::EXTENSIONS, &Self::GITOXIDE, &Self::HTTP, + &Self::INDEX, &Self::INIT, &Self::PACK, &Self::PROTOCOL, @@ -84,9 +87,9 @@ pub(crate) mod root { mod sections; pub use sections::{ - branch, checkout, core, credential, diff, extensions, gitoxide, http, protocol, remote, ssh, Author, Branch, - Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Init, Pack, Protocol, Remote, Safe, - Ssh, Url, User, + branch, checkout, core, credential, diff, extensions, gitoxide, http, index, protocol, remote, ssh, Author, Branch, + Checkout, Clone, Committer, Core, Credential, Diff, Extensions, Gitoxide, Http, Index, Init, Pack, Protocol, + Remote, Safe, Ssh, Url, User, }; /// Generic value implementations for static instantiation. diff --git a/gix/src/config/tree/sections/index.rs b/gix/src/config/tree/sections/index.rs new file mode 100644 index 00000000000..d033222472c --- /dev/null +++ b/gix/src/config/tree/sections/index.rs @@ -0,0 +1,62 @@ +use crate::{ + config, + config::tree::{keys, Index, Key, Section}, +}; + +impl Index { + /// The `index.threads` key. + pub const THREADS: IndexThreads = + IndexThreads::new_with_validate("threads", &config::Tree::INDEX, validate::IndexThreads); +} + +/// The `index.threads` key. +pub type IndexThreads = keys::Any; + +mod index_threads { + use crate::bstr::BStr; + use crate::config; + use crate::config::key::GenericErrorWithValue; + use crate::config::tree::index::IndexThreads; + use std::borrow::Cow; + + impl IndexThreads { + /// Parse `value` into the amount of threads to use, with `1` being single-threaded, or `0` indicating + /// to select the amount of threads, with any other number being the specific amount of threads to use. + pub fn try_into_index_threads( + &'static self, + value: Cow<'_, BStr>, + ) -> Result { + gix_config::Integer::try_from(value.as_ref()) + .ok() + .and_then(|i| i.to_decimal().and_then(|i| i.try_into().ok())) + .or_else(|| { + gix_config::Boolean::try_from(value.as_ref()) + .ok() + .map(|b| if b.0 { 0 } else { 1 }) + }) + .ok_or_else(|| GenericErrorWithValue::from_value(self, value.into_owned())) + } + } +} + +impl Section for Index { + fn name(&self) -> &str { + "index" + } + + fn keys(&self) -> &[&dyn Key] { + &[&Self::THREADS] + } +} + +mod validate { + use crate::{bstr::BStr, config::tree::keys}; + + pub struct IndexThreads; + impl keys::Validate for IndexThreads { + fn validate(&self, value: &BStr) -> Result<(), Box> { + super::Index::THREADS.try_into_index_threads(value.into())?; + Ok(()) + } + } +} diff --git a/gix/src/config/tree/sections/mod.rs b/gix/src/config/tree/sections/mod.rs index fb9b50786a7..9f0a50c93f3 100644 --- a/gix/src/config/tree/sections/mod.rs +++ b/gix/src/config/tree/sections/mod.rs @@ -55,6 +55,11 @@ pub mod gitoxide; pub struct Http; pub mod http; +/// The `index` top-level section. +#[derive(Copy, Clone, Default)] +pub struct Index; +pub mod index; + /// The `init` top-level section. #[derive(Copy, Clone, Default)] pub struct Init; diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 2de31bc86c3..2372b48b855 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -1,3 +1,4 @@ +use crate::config::cache::util::ApplyLeniencyDefault; use crate::{worktree, Worktree}; /// Worktree iteration @@ -58,23 +59,14 @@ impl crate::Repository { /// /// It will use the `index.threads` configuration key to learn how many threads to use. /// Note that it may fail if there is no index. - // TODO: test pub fn open_index(&self) -> Result { let thread_limit = self .config .resolved - .boolean("index", None, "threads") - .map(|res| { - res.map(|value| usize::from(!value)).or_else(|err| { - gix_config::Integer::try_from(err.input.as_ref()) - .map_err(|err| worktree::open_index::Error::ConfigIndexThreads { - value: err.input.clone(), - err, - }) - .map(|value| value.to_decimal().and_then(|v| v.try_into().ok()).unwrap_or(1)) - }) - }) - .transpose()?; + .string("index", None, "threads") + .map(|value| crate::config::tree::Index::THREADS.try_into_index_threads(value)) + .transpose() + .with_lenient_default(self.config.lenient_config)?; gix_index::File::at( self.index_path(), self.object_hash(), diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index b73f3be6130..a3a0cee2b4b 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -71,18 +71,12 @@ pub mod proxy; /// pub mod open_index { - use crate::bstr::BString; - /// The error returned by [`Worktree::open_index()`][crate::Worktree::open_index()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error("Could not interpret value '{}' as 'index.threads'", .value)] - ConfigIndexThreads { - value: BString, - #[source] - err: gix_config::value::Error, - }, + #[error(transparent)] + ConfigIndexThreads(#[from] crate::config::key::GenericErrorWithValue), #[error(transparent)] IndexFile(#[from] gix_index::file::init::Error), } diff --git a/gix/tests/config/tree.rs b/gix/tests/config/tree.rs index 720f0b69f87..3992b648f5e 100644 --- a/gix/tests/config/tree.rs +++ b/gix/tests/config/tree.rs @@ -351,6 +351,30 @@ mod core { } } +mod index { + use crate::config::tree::bcow; + use gix::config::tree::{Index, Key}; + + #[test] + fn threads() { + for (value, expected) in [("false", 1), ("true", 0), ("0", 0), ("1", 1), ("2", 2), ("12", 12)] { + assert_eq!( + Index::THREADS.try_into_index_threads(bcow(value)).unwrap(), + expected, + "{value}" + ); + assert!(Index::THREADS.validate(value.into()).is_ok()); + } + assert_eq!( + Index::THREADS + .try_into_index_threads(bcow("nothing")) + .unwrap_err() + .to_string(), + "The key \"index.threads=nothing\" was invalid" + ); + } +} + mod extensions { use gix::config::tree::{Extensions, Key}; From 26e6a661ed5827151708b9fcc3d7468aa60cf4e3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 11:03:06 +0200 Subject: [PATCH 10/18] feat!: add `Repository::excludes()` and simplify signature of `Worktree::excludes()`. Further, this change removes the `permission` module without replacement, and moves `permissions` into `open`. This corrects an artifact of this crate previously being name `gix-repository` and brings these types semantically closer to where they are actually used. --- gix/src/config/cache/access.rs | 2 +- gix/src/config/cache/init.rs | 16 +- gix/src/config/mod.rs | 4 +- gix/src/lib.rs | 9 +- gix/src/open/mod.rs | 17 +- gix/src/open/options.rs | 2 +- gix/src/open/permissions.rs | 215 ++++++++++++++++++++++++ gix/src/open/repository.rs | 5 +- gix/src/permission.rs | 8 - gix/src/permissions.rs | 1 - gix/src/repository/excludes.rs | 59 +++++++ gix/src/repository/mod.rs | 2 +- gix/src/repository/permissions.rs | 13 +- gix/src/repository/worktree.rs | 6 +- gix/src/worktree/mod.rs | 42 +---- gix/tests/remote/connect.rs | 8 +- gix/tests/repository/config/identity.rs | 25 +-- 17 files changed, 332 insertions(+), 102 deletions(-) create mode 100644 gix/src/open/permissions.rs delete mode 100644 gix/src/permission.rs delete mode 100644 gix/src/permissions.rs create mode 100644 gix/src/repository/excludes.rs diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 3b23e2821ba..d44cd9fe652 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -197,7 +197,7 @@ impl Cache { &self, git_dir: &std::path::Path, source: gix_worktree::cache::state::attributes::Source, - attributes: crate::permissions::Attributes, + attributes: crate::open::permissions::Attributes, ) -> Result { let configured_or_user_attributes = match self .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name) diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index 90d447ef401..ee20e03542e 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -12,7 +12,7 @@ use crate::{ tree::{gitoxide, Core, Http}, Cache, }, - repository, + open, }; /// Initialization @@ -32,7 +32,7 @@ impl Cache { filter_config_section: fn(&gix_config::file::Metadata) -> bool, git_install_dir: Option<&std::path::Path>, home: Option<&std::path::Path>, - environment @ repository::permissions::Environment { + environment @ open::permissions::Environment { git_prefix, ssh_prefix: _, xdg_config_home: _, @@ -40,16 +40,16 @@ impl Cache { http_transport, identity, objects, - }: repository::permissions::Environment, - attributes: repository::permissions::Attributes, - repository::permissions::Config { + }: open::permissions::Environment, + attributes: open::permissions::Attributes, + open::permissions::Config { git_binary: use_installation, system: use_system, git: use_git, user: use_user, env: use_env, includes: use_includes, - }: repository::permissions::Config, + }: open::permissions::Config, lenient_config: bool, api_config_overrides: &[BString], cli_config_overrides: &[BString], @@ -233,12 +233,12 @@ impl Cache { } pub(crate) fn make_source_env( - crate::permissions::Environment { + crate::open::permissions::Environment { xdg_config_home, git_prefix, home, .. - }: crate::permissions::Environment, + }: open::permissions::Environment, ) -> impl FnMut(&str) -> Option { move |name| { match name { diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 8fe8ce53f35..0b126de7bcd 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -462,7 +462,7 @@ pub(crate) struct Cache { /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient. /// Also available in options! Keep in sync! pub lenient_config: bool, - attributes: crate::permissions::Attributes, - environment: crate::permissions::Environment, + attributes: crate::open::permissions::Attributes, + environment: crate::open::permissions::Environment, // TODO: make core.precomposeUnicode available as well. } diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 1524590e1dc..87f8d6f221c 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -127,7 +127,7 @@ pub mod head; pub mod id; pub mod object; pub mod reference; -mod repository; +pub mod repository; pub mod tag; /// @@ -223,13 +223,6 @@ pub fn open_opts(directory: impl Into, options: open::Option ThreadSafeRepository::open_opts(directory, options).map(Into::into) } -/// -pub mod permission; - -/// -pub mod permissions; -pub use repository::permissions::Permissions; - /// pub mod create; diff --git a/gix/src/open/mod.rs b/gix/src/open/mod.rs index 943f357e838..03c9762049e 100644 --- a/gix/src/open/mod.rs +++ b/gix/src/open/mod.rs @@ -1,6 +1,17 @@ use std::path::PathBuf; -use crate::{bstr::BString, config, permission, Permissions}; +use crate::{bstr::BString, config}; + +/// Permissions associated with various resources of a git repository +#[derive(Debug, Clone)] +pub struct Permissions { + /// Control which environment variables may be accessed. + pub env: permissions::Environment, + /// Permissions related where git configuration should be loaded from. + pub config: permissions::Config, + /// Permissions related to where `gitattributes` should be loaded from. + pub attributes: permissions::Attributes, +} /// The options used in [`ThreadSafeRepository::open_opts()`][crate::ThreadSafeRepository::open_opts()]. /// @@ -44,11 +55,11 @@ pub enum Error { #[error("The git directory at '{}' is considered unsafe as it's not owned by the current user.", .path.display())] UnsafeGitDir { path: PathBuf }, #[error(transparent)] - EnvironmentAccessDenied(#[from] permission::env_var::resource::Error), + EnvironmentAccessDenied(#[from] gix_sec::permission::Error), } mod options; - +pub mod permissions; mod repository; #[cfg(test)] diff --git a/gix/src/open/options.rs b/gix/src/open/options.rs index 8089ab7dfba..b098d55c124 100644 --- a/gix/src/open/options.rs +++ b/gix/src/open/options.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use super::{Error, Options}; -use crate::{bstr::BString, config, Permissions, ThreadSafeRepository}; +use crate::{bstr::BString, config, open::Permissions, ThreadSafeRepository}; impl Default for Options { fn default() -> Self { diff --git a/gix/src/open/permissions.rs b/gix/src/open/permissions.rs new file mode 100644 index 00000000000..633575a9d32 --- /dev/null +++ b/gix/src/open/permissions.rs @@ -0,0 +1,215 @@ +//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository]. +use crate::open::Permissions; +use gix_sec::Trust; + +/// Configure from which sources git configuration may be loaded. +/// +/// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] +pub struct Config { + /// The git binary may come with configuration as part of its configuration, and if this is true (default false) + /// we will load the configuration of the git binary, if present and not a duplicate of the ones below. + /// + /// It's disabled by default as it may involve executing the git binary once per execution of the application. + pub git_binary: bool, + /// Whether to use the system configuration. + /// This is defined as `$(prefix)/etc/gitconfig` on unix. + pub system: bool, + /// Whether to use the git application configuration. + /// + /// A platform defined location for where a user's git application configuration should be located. + /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/config` will be used + /// on unix. + pub git: bool, + /// Whether to use the user configuration. + /// This is usually `~/.gitconfig` on unix. + pub user: bool, + /// Whether to use the configuration from environment variables. + pub env: bool, + /// Whether to follow include files are encountered in loaded configuration, + /// via `include` and `includeIf` sections. + pub includes: bool, +} + +impl Config { + /// Allow everything which usually relates to a fully trusted environment + pub fn all() -> Self { + Config { + git_binary: false, + system: true, + git: true, + user: true, + env: true, + includes: true, + } + } + + /// Load only configuration local to the git repository. + pub fn isolated() -> Self { + Config { + git_binary: false, + system: false, + git: false, + user: false, + env: false, + includes: false, + } + } +} + +impl Default for Config { + fn default() -> Self { + Self::all() + } +} + +/// Configure from which `gitattribute` files may be loaded. +/// +/// Note that `.gitattribute` files from within the repository are always loaded. +#[derive(Copy, Clone, Ord, PartialOrd, PartialEq, Eq, Debug, Hash)] +pub struct Attributes { + /// The git binary may come with attribute configuration in its installation directory, and if this is true (default false) + /// we will load the configuration of the git binary. + /// + /// It's disabled by default as it involves executing the git binary once per execution of the application. + pub git_binary: bool, + /// Whether to use the system configuration. + /// This is typically defined as `$(prefix)/etc/gitconfig`. + pub system: bool, + /// Whether to use the git application configuration. + /// + /// A platform defined location for where a user's git application configuration should be located. + /// If `$XDG_CONFIG_HOME` is not set or empty, `$HOME/.config/git/attributes` will be used + /// on unix. + pub git: bool, +} + +impl Attributes { + /// Allow everything which usually relates to a fully trusted environment + pub fn all() -> Self { + Attributes { + git_binary: false, + system: true, + git: true, + } + } + + /// Allow loading attributes that are local to the git repository. + pub fn isolated() -> Self { + Attributes { + git_binary: false, + system: false, + git: false, + } + } +} + +impl Default for Attributes { + fn default() -> Self { + Self::all() + } +} + +/// Permissions related to the usage of environment variables +#[derive(Debug, Clone, Copy)] +pub struct Environment { + /// Control whether resources pointed to by `XDG_CONFIG_HOME` can be used when looking up common configuration values. + /// + /// Note that [`gix_sec::Permission::Forbid`] will cause the operation to abort if a resource is set via the XDG config environment. + pub xdg_config_home: gix_sec::Permission, + /// Control the way resources pointed to by the home directory (similar to `xdg_config_home`) may be used. + pub home: gix_sec::Permission, + /// Control if environment variables to configure the HTTP transport, like `http_proxy` may be used. + /// + /// Note that http-transport related environment variables prefixed with `GIT_` may also be included here + /// if they match this category like `GIT_HTTP_USER_AGENT`. + pub http_transport: gix_sec::Permission, + /// Control if the `EMAIL` environment variables may be read. + /// + /// Note that identity related environment variables prefixed with `GIT_` may also be included here + /// if they match this category. + pub identity: gix_sec::Permission, + /// Control if environment variables related to the object database are handled. This includes features and performance + /// options alike. + pub objects: gix_sec::Permission, + /// Control if resources pointed to by `GIT_*` prefixed environment variables can be used, **but only** if they + /// are not contained in any other category. This is a catch-all section. + pub git_prefix: gix_sec::Permission, + /// Control if resources pointed to by `SSH_*` prefixed environment variables can be used (like `SSH_ASKPASS`) + pub ssh_prefix: gix_sec::Permission, +} + +impl Environment { + /// Allow access to the entire environment. + pub fn all() -> Self { + let allow = gix_sec::Permission::Allow; + Environment { + xdg_config_home: allow, + home: allow, + git_prefix: allow, + ssh_prefix: allow, + http_transport: allow, + identity: allow, + objects: allow, + } + } + + /// Don't allow loading any environment variables. + pub fn isolated() -> Self { + let deny = gix_sec::Permission::Deny; + Environment { + xdg_config_home: deny, + home: deny, + ssh_prefix: deny, + git_prefix: deny, + http_transport: deny, + identity: deny, + objects: deny, + } + } +} + +impl Permissions { + /// Secure permissions are similar to `all()` + pub fn secure() -> Self { + Permissions { + env: Environment::all(), + config: Config::all(), + attributes: Attributes::all(), + } + } + + /// Everything is allowed with this set of permissions, thus we read all configuration and do what git typically + /// does with owned repositories. + pub fn all() -> Self { + Permissions { + env: Environment::all(), + config: Config::all(), + attributes: Attributes::all(), + } + } + + /// Don't read any but the local git configuration and deny reading any environment variables. + pub fn isolated() -> Self { + Permissions { + config: Config::isolated(), + attributes: Attributes::isolated(), + env: Environment::isolated(), + } + } +} + +impl gix_sec::trust::DefaultForLevel for Permissions { + fn default_for_level(level: Trust) -> Self { + match level { + Trust::Full => Permissions::all(), + Trust::Reduced => Permissions::secure(), + } + } +} + +impl Default for Permissions { + fn default() -> Self { + Permissions::secure() + } +} diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 8ce5a5315a9..c7702b5f660 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -10,7 +10,8 @@ use crate::{ cache::{interpolate_context, util::ApplyLeniency}, tree::{gitoxide, Core, Key, Safe}, }, - permission, Permissions, ThreadSafeRepository, + open::Permissions, + ThreadSafeRepository, }; #[derive(Default, Clone)] @@ -26,7 +27,7 @@ pub(crate) struct EnvironmentOverrides { } impl EnvironmentOverrides { - fn from_env() -> Result { + fn from_env() -> Result> { let mut worktree_dir = None; if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) { worktree_dir = PathBuf::from(path).into(); diff --git a/gix/src/permission.rs b/gix/src/permission.rs deleted file mode 100644 index f74859def43..00000000000 --- a/gix/src/permission.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// -pub mod env_var { - /// - pub mod resource { - /// - pub type Error = gix_sec::permission::Error; - } -} diff --git a/gix/src/permissions.rs b/gix/src/permissions.rs deleted file mode 100644 index c1838bf27df..00000000000 --- a/gix/src/permissions.rs +++ /dev/null @@ -1 +0,0 @@ -pub use crate::repository::permissions::{Attributes, Config, Environment}; diff --git a/gix/src/repository/excludes.rs b/gix/src/repository/excludes.rs new file mode 100644 index 00000000000..465d3029fcc --- /dev/null +++ b/gix/src/repository/excludes.rs @@ -0,0 +1,59 @@ +//! exclude information +use crate::Repository; +use std::path::PathBuf; + +/// The error returned by [`Repository::excludes()`]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could not read repository exclude")] + Io(#[from] std::io::Error), + #[error(transparent)] + EnvironmentPermission(#[from] gix_sec::permission::Error), + #[error("The value for `core.excludesFile` could not be read from configuration")] + ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), +} + +impl Repository { + /// Configure a file-system cache checking if files below the repository are excluded. + /// + /// Note that no worktree is required for this to work, even though access to in-tree `.gitignore` files would require + /// a non-empty `index` that represents a tree with `.gitignore` files. + /// + /// This takes into consideration all the usual repository configuration, namely: + /// + /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. + /// * `$GIT_DIR/info/exclude` if present. + // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries + // by non-relative path. + pub fn excludes( + &self, + index: &gix_index::State, + overrides: Option, + ) -> Result { + let case = if self.config.ignore_case { + gix_glob::pattern::Case::Fold + } else { + gix_glob::pattern::Case::Sensitive + }; + let mut buf = Vec::with_capacity(512); + let excludes_file = match self.config.excludes_file().transpose()? { + Some(user_path) => Some(user_path), + None => self.config.xdg_config_path("ignore")?, + }; + let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new( + overrides.unwrap_or_default(), + gix_ignore::Search::from_git_dir(self.git_dir(), excludes_file, &mut buf)?, + None, + )); + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); + Ok(gix_worktree::Cache::new( + // this is alright as we don't cause mutation of that directory, it's virtual. + self.work_dir().unwrap_or(self.git_dir()), + state, + case, + buf, + attribute_list, + )) + } +} diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 4fe3b27c622..275af88e421 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -21,12 +21,12 @@ impl crate::Repository { mod cache; mod config; +pub mod excludes; pub(crate) mod identity; mod impls; mod init; mod location; mod object; -pub(crate) mod permissions; mod reference; mod remote; mod revision; diff --git a/gix/src/repository/permissions.rs b/gix/src/repository/permissions.rs index d6d0f6ee72c..633575a9d32 100644 --- a/gix/src/repository/permissions.rs +++ b/gix/src/repository/permissions.rs @@ -1,16 +1,7 @@ +//! Various permissions to define what can be done when operating a [`Repository`][crate::Repository]. +use crate::open::Permissions; use gix_sec::Trust; -/// Permissions associated with various resources of a git repository -#[derive(Debug, Clone)] -pub struct Permissions { - /// Control which environment variables may be accessed. - pub env: Environment, - /// Permissions related where git configuration should be loaded from. - pub config: Config, - /// Permissions related to where `gitattributes` should be loaded from. - pub attributes: Attributes, -} - /// Configure from which sources git configuration may be loaded. /// /// Note that configuration from inside of the repository is always loaded as it's definitely required for correctness. diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 2372b48b855..316009d2963 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -1,7 +1,7 @@ use crate::config::cache::util::ApplyLeniencyDefault; use crate::{worktree, Worktree}; -/// Worktree iteration +/// Interact with individual worktrees and their information. impl crate::Repository { /// Return a list of all _linked_ worktrees sorted by private git dir path as a lightweight proxy. /// @@ -26,10 +26,6 @@ impl crate::Repository { res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir)); Ok(res) } -} - -/// Interact with individual worktrees and their information. -impl crate::Repository { /// Return the repository owning the main worktree, typically from a linked worktree. /// /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree. diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index a3a0cee2b4b..1d9955d4a4e 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -96,18 +96,14 @@ pub mod open_index { /// pub mod excludes { - use std::path::PathBuf; - /// The error returned by [`Worktree::excludes()`][crate::Worktree::excludes()]. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error("Could not read repository exclude.")] - Io(#[from] std::io::Error), #[error(transparent)] - EnvironmentPermission(#[from] gix_sec::permission::Error), - #[error("The value for `core.excludesFile` could not be read from configuration")] - ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), + OpenIndex(#[from] crate::worktree::open_index::Error), + #[error(transparent)] + CreateCache(#[from] crate::repository::excludes::Error), } impl<'repo> crate::Worktree<'repo> { @@ -117,35 +113,9 @@ pub mod excludes { /// /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. /// * `$GIT_DIR/info/exclude` if present. - /// - /// `index` may be used to obtain `.gitignore` files directly from the index under certain conditions. - // TODO: test, provide higher-level interface that is much easier to use and doesn't panic when accessing entries - // by non-relative path. - // TODO: `index` might be so special (given the conditions we are talking about) that it's better obtained internally - // so the caller won't have to care. - pub fn excludes( - &self, - index: &gix_index::State, - overrides: Option, - ) -> Result { - let repo = self.parent; - let case = if repo.config.ignore_case { - gix_glob::pattern::Case::Fold - } else { - gix_glob::pattern::Case::Sensitive - }; - let mut buf = Vec::with_capacity(512); - let excludes_file = match repo.config.excludes_file().transpose()? { - Some(user_path) => Some(user_path), - None => repo.config.xdg_config_path("ignore")?, - }; - let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new( - overrides.unwrap_or_default(), - gix_ignore::Search::from_git_dir(repo.git_dir(), excludes_file, &mut buf)?, - None, - )); - let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); - Ok(gix_worktree::Cache::new(self.path, state, case, buf, attribute_list)) + pub fn excludes(&self, overrides: Option) -> Result { + let index = self.index()?; + Ok(self.parent.excludes(&index, overrides)?) } } } diff --git a/gix/tests/remote/connect.rs b/gix/tests/remote/connect.rs index 912f57e5f57..66ed36d86b2 100644 --- a/gix/tests/remote/connect.rs +++ b/gix/tests/remote/connect.rs @@ -28,12 +28,12 @@ mod blocking_io { let _env = env_value.map(|value| gix_testtools::Env::new().set("GIT_PROTOCOL_FROM_USER", value)); let repo = gix::open_opts( remote::repo("protocol_file_user").git_dir(), - gix::open::Options::isolated().permissions(gix::Permissions { - env: gix::permissions::Environment { + gix::open::Options::isolated().permissions(gix::open::Permissions { + env: gix::open::permissions::Environment { git_prefix: gix_sec::Permission::Allow, - ..gix::permissions::Environment::all() + ..gix::open::permissions::Environment::all() }, - ..gix::Permissions::isolated() + ..gix::open::Permissions::isolated() }), )?; let remote = repo.find_remote("origin")?; diff --git a/gix/tests/repository/config/identity.rs b/gix/tests/repository/config/identity.rs index f49c8ae7d75..e66c1e99113 100644 --- a/gix/tests/repository/config/identity.rs +++ b/gix/tests/repository/config/identity.rs @@ -29,14 +29,17 @@ fn author_and_committer_and_fallback() -> crate::Result { .set("GIT_CONFIG_VALUE_0", work_dir.join("c.config").display().to_string()); let repo = gix::open_opts( repo.git_dir(), - repo.open_options().clone().with(trust).permissions(gix::Permissions { - env: gix::permissions::Environment { - xdg_config_home: Permission::Deny, - home: Permission::Deny, - ..gix::permissions::Environment::all() - }, - ..Default::default() - }), + repo.open_options() + .clone() + .with(trust) + .permissions(gix::open::Permissions { + env: gix::open::permissions::Environment { + xdg_config_home: Permission::Deny, + home: Permission::Deny, + ..gix::open::permissions::Environment::all() + }, + ..Default::default() + }), )?; assert_eq!( @@ -148,11 +151,11 @@ fn author_from_different_config_sections() -> crate::Result { .clone() .config_overrides(None::<&str>) .with(gix_sec::Trust::Full) - .permissions(gix::Permissions { - env: gix::permissions::Environment { + .permissions(gix::open::Permissions { + env: gix::open::permissions::Environment { xdg_config_home: Permission::Deny, home: Permission::Deny, - ..gix::permissions::Environment::all() + ..gix::open::permissions::Environment::all() }, ..Default::default() }), From bc28443e452c4de81368739a11a2482ae0a93485 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 12:57:08 +0200 Subject: [PATCH 11/18] feat: add `Repository::attributes()` and `Worktree::attributes()`. --- crate-status.md | 2 +- gix/src/attributes.rs | 9 ++++++ gix/src/config/cache/access.rs | 37 +++++++++++++++++++------ gix/src/config/mod.rs | 17 ++++++++++++ gix/src/lib.rs | 5 +++- gix/src/repository/attributes.rs | 47 ++++++++++++++++++++++++++++++++ gix/src/repository/excludes.rs | 34 ++++++----------------- gix/src/repository/mod.rs | 3 +- gix/src/worktree/mod.rs | 38 +++++++++++++++++++++++++- 9 files changed, 154 insertions(+), 38 deletions(-) create mode 100644 gix/src/attributes.rs create mode 100644 gix/src/repository/attributes.rs diff --git a/crate-status.md b/crate-status.md index 6dd4123e851..8ddb4d868c9 100644 --- a/crate-status.md +++ b/crate-status.md @@ -690,7 +690,7 @@ See its [README.md](https://github.com/Byron/gitoxide/blob/main/gix-lock/README. * [x] proper handling of worktree related refs * [ ] create, move, remove, and repair * [x] access exclude information - * [ ] access attribute information + * [x] access attribute information * [x] respect `core.worktree` configuration - **deviation** * The delicate interplay between `GIT_COMMON_DIR` and `GIT_WORK_TREE` isn't implemented. diff --git a/gix/src/attributes.rs b/gix/src/attributes.rs new file mode 100644 index 00000000000..bb863646001 --- /dev/null +++ b/gix/src/attributes.rs @@ -0,0 +1,9 @@ +/// The error returned by [`Repository::attributes()`][crate::Repository::attributes()]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + ConfigureAttributes(#[from] crate::config::attribute_stack::Error), + #[error(transparent)] + ConfigureExcludes(#[from] crate::config::exclude_stack::Error), +} diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index d44cd9fe652..02caf9e327a 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -167,11 +167,13 @@ impl Cache { symlink: boolean(self, "core.symlinks", &Core::SYMLINKS, true)?, }; Ok(gix_worktree::checkout::Options { - attributes: self.assemble_attribute_globals( - git_dir, - gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, - self.attributes, - )?, + attributes: self + .assemble_attribute_globals( + git_dir, + gix_worktree::cache::state::attributes::Source::IdMappingThenWorktree, + self.attributes, + )? + .0, fs: capabilities, thread_limit, destination_is_initially_empty: false, @@ -192,13 +194,29 @@ impl Cache { }) } + pub(crate) fn assemble_exclude_globals( + &self, + git_dir: &std::path::Path, + overrides: Option, + buf: &mut Vec, + ) -> Result { + let excludes_file = match self.excludes_file().transpose()? { + Some(user_path) => Some(user_path), + None => self.xdg_config_path("ignore")?, + }; + Ok(gix_worktree::cache::state::Ignore::new( + overrides.unwrap_or_default(), + gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf)?, + None, + )) + } // TODO: at least one test, maybe related to core.attributesFile configuration. - fn assemble_attribute_globals( + pub(crate) fn assemble_attribute_globals( &self, git_dir: &std::path::Path, source: gix_worktree::cache::state::attributes::Source, attributes: crate::open::permissions::Attributes, - ) -> Result { + ) -> Result<(gix_worktree::cache::state::Attributes, Vec), config::attribute_stack::Error> { let configured_or_user_attributes = match self .trusted_file_path("core", None, Core::ATTRIBUTES_FILE.name) .transpose()? @@ -224,12 +242,13 @@ impl Cache { let info_attributes_path = git_dir.join("info").join("attributes"); let mut buf = Vec::new(); let mut collection = gix_attributes::search::MetadataCollection::default(); - Ok(gix_worktree::cache::state::Attributes::new( + let res = gix_worktree::cache::state::Attributes::new( gix_attributes::Search::new_globals(attribute_files, &mut buf, &mut collection)?, Some(info_attributes_path), source, collection, - )) + ); + Ok((res, buf)) } pub(crate) fn xdg_config_path( diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 0b126de7bcd..5da56960551 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -118,6 +118,23 @@ pub mod checkout_options { } } +/// +pub mod exclude_stack { + use std::path::PathBuf; + + /// The error produced when setting up a stack to query `gitignore` information. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not read repository exclude")] + Io(#[from] std::io::Error), + #[error(transparent)] + EnvironmentPermission(#[from] gix_sec::permission::Error), + #[error("The value for `core.excludesFile` could not be read from configuration")] + ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), + } +} + /// pub mod attribute_stack { /// The error produced when setting up the attribute stack to query `gitattributes`. diff --git a/gix/src/lib.rs b/gix/src/lib.rs index 87f8d6f221c..eb5efcfdf74 100644 --- a/gix/src/lib.rs +++ b/gix/src/lib.rs @@ -99,6 +99,9 @@ pub use hash::{oid, ObjectId}; pub mod interrupt; +/// +pub mod attributes; + mod ext; /// pub mod prelude; @@ -127,7 +130,7 @@ pub mod head; pub mod id; pub mod object; pub mod reference; -pub mod repository; +mod repository; pub mod tag; /// diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs new file mode 100644 index 00000000000..e43ae78e05f --- /dev/null +++ b/gix/src/repository/attributes.rs @@ -0,0 +1,47 @@ +//! exclude information +use crate::Repository; + +impl Repository { + /// Configure a file-system cache for accessing git attributes *and* excludes on a per-path basis. + /// + /// Use `attribute_source` to specify where to read attributes from. Also note that exclude information will + /// always try to read `.gitignore` files from disk before trying to read it from the `index`. + /// + /// Note that no worktree is required for this to work, even though access to in-tree `.gitattributes` and `.gitignore` files + /// would require a non-empty `index` that represents a git tree. + /// + /// This takes into consideration all the usual repository configuration, namely: + /// + /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. + /// * `$GIT_DIR/info/exclude|attributes` if present. + // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries + // by non-relative path. + pub fn attributes( + &self, + index: &gix_index::State, + source: gix_worktree::cache::state::attributes::Source, + exclude_overrides: Option, + ) -> Result { + let case = if self.config.ignore_case { + gix_glob::pattern::Case::Fold + } else { + gix_glob::pattern::Case::Sensitive + }; + let (attributes, mut buf) = + self.config + .assemble_attribute_globals(self.git_dir(), source, self.options.permissions.attributes)?; + let ignore = self + .config + .assemble_exclude_globals(self.git_dir(), exclude_overrides, &mut buf)?; + let state = gix_worktree::cache::State::AttributesAndIgnoreStack { attributes, ignore }; + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); + Ok(gix_worktree::Cache::new( + // this is alright as we don't cause mutation of that directory, it's virtual. + self.work_dir().unwrap_or(self.git_dir()), + state, + case, + buf, + attribute_list, + )) + } +} diff --git a/gix/src/repository/excludes.rs b/gix/src/repository/excludes.rs index 465d3029fcc..c36162b40d1 100644 --- a/gix/src/repository/excludes.rs +++ b/gix/src/repository/excludes.rs @@ -1,19 +1,5 @@ //! exclude information -use crate::Repository; -use std::path::PathBuf; - -/// The error returned by [`Repository::excludes()`]. -#[derive(Debug, thiserror::Error)] -#[allow(missing_docs)] -pub enum Error { - #[error("Could not read repository exclude")] - Io(#[from] std::io::Error), - #[error(transparent)] - EnvironmentPermission(#[from] gix_sec::permission::Error), - #[error("The value for `core.excludesFile` could not be read from configuration")] - ExcludesFilePathInterpolation(#[from] gix_config::path::interpolate::Error), -} - +use crate::{config, Repository}; impl Repository { /// Configure a file-system cache checking if files below the repository are excluded. /// @@ -24,28 +10,26 @@ impl Repository { /// /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. /// * `$GIT_DIR/info/exclude` if present. + /// + /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use + /// [`Repository::attributes()`] for accessing both attributes and excludes. // TODO: test, provide higher-level custom Cache wrapper that is much easier to use and doesn't panic when accessing entries // by non-relative path. pub fn excludes( &self, index: &gix_index::State, overrides: Option, - ) -> Result { + ) -> Result { let case = if self.config.ignore_case { gix_glob::pattern::Case::Fold } else { gix_glob::pattern::Case::Sensitive }; let mut buf = Vec::with_capacity(512); - let excludes_file = match self.config.excludes_file().transpose()? { - Some(user_path) => Some(user_path), - None => self.config.xdg_config_path("ignore")?, - }; - let state = gix_worktree::cache::State::IgnoreStack(gix_worktree::cache::state::Ignore::new( - overrides.unwrap_or_default(), - gix_ignore::Search::from_git_dir(self.git_dir(), excludes_file, &mut buf)?, - None, - )); + let ignore = self + .config + .assemble_exclude_globals(self.git_dir(), overrides, &mut buf)?; + let state = gix_worktree::cache::State::IgnoreStack(ignore); let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); Ok(gix_worktree::Cache::new( // this is alright as we don't cause mutation of that directory, it's virtual. diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index 275af88e421..5b7a70d3b07 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -19,9 +19,10 @@ impl crate::Repository { } } +mod attributes; mod cache; mod config; -pub mod excludes; +mod excludes; pub(crate) mod identity; mod impls; mod init; diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 1d9955d4a4e..894536820db 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -103,7 +103,7 @@ pub mod excludes { #[error(transparent)] OpenIndex(#[from] crate::worktree::open_index::Error), #[error(transparent)] - CreateCache(#[from] crate::repository::excludes::Error), + CreateCache(#[from] crate::config::exclude_stack::Error), } impl<'repo> crate::Worktree<'repo> { @@ -113,9 +113,45 @@ pub mod excludes { /// /// * `$XDG_CONFIG_HOME/…/ignore` if `core.excludesFile` is *not* set, otherwise use the configured file. /// * `$GIT_DIR/info/exclude` if present. + /// + /// When only excludes are desired, this is the most efficient way to obtain them. Otherwise use + /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. pub fn excludes(&self, overrides: Option) -> Result { let index = self.index()?; Ok(self.parent.excludes(&index, overrides)?) } } } + +/// +pub mod attributes { + /// The error returned by [`Worktree::attributes()`][crate::Worktree::attributes()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + OpenIndex(#[from] crate::worktree::open_index::Error), + #[error(transparent)] + CreateCache(#[from] crate::attributes::Error), + } + + impl<'repo> crate::Worktree<'repo> { + /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. + /// + /// Use `attribute_source` to specify where to read attributes from. Also note that exclude information will + /// always try to read `.gitignore` files from disk before trying to read it from the `index`. + /// + /// This takes into consideration all the usual repository configuration, namely: + /// + /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. + /// * `$GIT_DIR/info/exclude|attributes` if present. + pub fn attributes( + &self, + source: gix_worktree::cache::state::attributes::Source, + overrides: Option, + ) -> Result { + let index = self.index()?; + Ok(self.parent.attributes(&index, source, overrides)?) + } + } +} From a09b63f75cf3e947cdcf052fe668d10710fac1fb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 12:51:42 +0200 Subject: [PATCH 12/18] adjust to changes in `gix` --- gitoxide-core/src/repository/exclude.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs index 4239d17339f..d5bde391f78 100644 --- a/gitoxide-core/src/repository/exclude.rs +++ b/gitoxide-core/src/repository/exclude.rs @@ -1,6 +1,6 @@ use std::io; -use anyhow::{bail, Context}; +use anyhow::bail; use gix::prelude::FindExt; use crate::OutputFormat; @@ -31,11 +31,8 @@ pub fn query( bail!("JSON output isn't implemented yet"); } - let worktree = repo - .worktree() - .with_context(|| "Cannot check excludes without a current worktree")?; - let index = worktree.index()?; - let mut cache = worktree.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?; + let index = repo.index()?; + let mut cache = repo.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?; let prefix = repo.prefix().expect("worktree - we have an index by now")?; From 745fc37b87d64e4fd821a692b223eda4f5df81ce Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 18:38:47 +0200 Subject: [PATCH 13/18] feat: provide statistics for cache operations, and turn debug API into better public API for `Cache`. That way it's a bit clearer what it is doing and does away with some rather dubious test code. --- gix-worktree/src/cache/delegate.rs | 81 ++++++++++++------- gix-worktree/src/cache/mod.rs | 72 +++++++++-------- gix-worktree/src/cache/state/attributes.rs | 40 +++++---- gix-worktree/src/cache/state/ignore.rs | 54 +++++-------- gix-worktree/src/cache/state/mod.rs | 28 +++++-- gix-worktree/src/lib.rs | 1 + .../make_ignore_and_attributes_setup.sh | 2 + .../tests/worktree/cache/create_directory.rs | 28 ++++--- gix-worktree/tests/worktree/cache/ignore.rs | 10 --- 9 files changed, 179 insertions(+), 137 deletions(-) diff --git a/gix-worktree/src/cache/delegate.rs b/gix-worktree/src/cache/delegate.rs index 272b537476e..90a141b0dde 100644 --- a/gix-worktree/src/cache/delegate.rs +++ b/gix-worktree/src/cache/delegate.rs @@ -1,13 +1,30 @@ use crate::cache::State; use crate::PathIdMapping; -pub struct StackDelegate<'a, Find> { +/// Various aggregate numbers related to the stack delegate itself. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// The amount of `std::fs::create_dir` calls. + /// + /// This only happens if we are in the respective mode to create leading directories efficiently. + pub num_mkdir_calls: usize, + /// Amount of calls to push a path element. + pub push_element: usize, + /// Amount of calls to push a directory. + pub push_directory: usize, + /// Amount of calls to pop a directory. + pub pop_directory: usize, +} + +pub(crate) struct StackDelegate<'a, Find> { pub state: &'a mut State, pub buf: &'a mut Vec, pub is_dir: bool, pub id_mappings: &'a Vec, pub find: Find, pub case: gix_glob::pattern::Case, + pub statistics: &'a mut super::Statistics, } impl<'a, Find, E> gix_fs::stack::Delegate for StackDelegate<'a, Find> @@ -16,76 +33,86 @@ where E: std::error::Error + Send + Sync + 'static, { fn push_directory(&mut self, stack: &gix_fs::Stack) -> std::io::Result<()> { + self.statistics.delegate.push_directory += 1; + let dir_bstr = gix_path::into_bstr(stack.current()); + let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( + gix_path::into_bstr(stack.root()).as_ref(), + dir_bstr.as_ref(), + None, + self.case, + ) + .expect("dir in root") + .0; + if rela_dir.starts_with(b"/") { + rela_dir = &rela_dir[1..]; + } match &mut self.state { State::CreateDirectoryAndAttributesStack { attributes, .. } => { attributes.push_directory( stack.root(), stack.current(), + rela_dir, self.buf, self.id_mappings, &mut self.find, - self.case, + &mut self.statistics.attributes, )?; } State::AttributesAndIgnoreStack { ignore, attributes } => { attributes.push_directory( stack.root(), stack.current(), + rela_dir, self.buf, self.id_mappings, &mut self.find, - self.case, + &mut self.statistics.attributes, )?; ignore.push_directory( stack.root(), stack.current(), + rela_dir, self.buf, self.id_mappings, &mut self.find, self.case, + &mut self.statistics.ignore, )? } State::IgnoreStack(ignore) => ignore.push_directory( stack.root(), stack.current(), + rela_dir, self.buf, self.id_mappings, &mut self.find, self.case, + &mut self.statistics.ignore, )?, } Ok(()) } fn push(&mut self, is_last_component: bool, stack: &gix_fs::Stack) -> std::io::Result<()> { + self.statistics.delegate.push_element += 1; match &mut self.state { State::CreateDirectoryAndAttributesStack { - #[cfg(debug_assertions)] - test_mkdir_calls, unlink_on_collision, attributes: _, - } => { - #[cfg(debug_assertions)] - { - create_leading_directory( - is_last_component, - stack, - self.is_dir, - test_mkdir_calls, - *unlink_on_collision, - )? - } - #[cfg(not(debug_assertions))] - { - create_leading_directory(is_last_component, stack, self.is_dir, *unlink_on_collision)? - } - } + } => create_leading_directory( + is_last_component, + stack, + self.is_dir, + &mut self.statistics.delegate.num_mkdir_calls, + *unlink_on_collision, + )?, State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => {} } Ok(()) } fn pop_directory(&mut self) { + self.statistics.delegate.pop_directory += 1; match &mut self.state { State::CreateDirectoryAndAttributesStack { attributes, .. } => { attributes.pop_directory(); @@ -105,16 +132,13 @@ fn create_leading_directory( is_last_component: bool, stack: &gix_fs::Stack, is_dir: bool, - #[cfg(debug_assertions)] mkdir_calls: &mut usize, + mkdir_calls: &mut usize, unlink_on_collision: bool, ) -> std::io::Result<()> { if is_last_component && !is_dir { return Ok(()); } - #[cfg(debug_assertions)] - { - *mkdir_calls += 1; - } + *mkdir_calls += 1; match std::fs::create_dir(stack.current()) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { @@ -127,10 +151,7 @@ fn create_leading_directory( } else { std::fs::remove_file(stack.current())?; } - #[cfg(debug_assertions)] - { - *mkdir_calls += 1; - } + *mkdir_calls += 1; std::fs::create_dir(stack.current()) } else { Err(err) diff --git a/gix-worktree/src/cache/mod.rs b/gix-worktree/src/cache/mod.rs index 829f2909e64..7984b2c4c43 100644 --- a/gix-worktree/src/cache/mod.rs +++ b/gix-worktree/src/cache/mod.rs @@ -7,16 +7,26 @@ use gix_hash::oid; use super::Cache; use crate::PathIdMapping; +/// Various aggregate numbers collected from when the corresponding [`Cache`] was instantiated. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// The amount of platforms created to do further matching. + pub platforms: usize, + /// Information about the stack delegate. + pub delegate: delegate::Statistics, + /// Information about attributes + pub attributes: state::attributes::Statistics, + /// Information about the ignore stack + pub ignore: state::ignore::Statistics, +} + #[derive(Clone)] pub enum State { /// Useful for checkout where directories need creation, but we need to access attributes as well. CreateDirectoryAndAttributesStack { /// If there is a symlink or a file in our path, try to unlink it before creating the directory. unlink_on_collision: bool, - - /// just for testing - #[cfg(debug_assertions)] - test_mkdir_calls: usize, /// State to handle attribute information attributes: state::Attributes, }, @@ -31,35 +41,6 @@ pub enum State { IgnoreStack(state::Ignore), } -#[cfg(debug_assertions)] -/// debug builds only for use in tests. -impl Cache { - pub fn set_case(&mut self, case: gix_glob::pattern::Case) { - self.case = case; - } - pub fn num_mkdir_calls(&self) -> usize { - match self.state { - State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } => test_mkdir_calls, - _ => 0, - } - } - - pub fn reset_mkdir_calls(&mut self) { - if let State::CreateDirectoryAndAttributesStack { test_mkdir_calls, .. } = &mut self.state { - *test_mkdir_calls = 0; - } - } - - pub fn unlink_on_collision(&mut self, value: bool) { - if let State::CreateDirectoryAndAttributesStack { - unlink_on_collision, .. - } = &mut self.state - { - *unlink_on_collision = value; - } - } -} - #[must_use] pub struct Platform<'a> { parent: &'a Cache, @@ -87,11 +68,12 @@ impl Cache { case, buf, id_mappings, + statistics: Statistics::default(), } } } -/// Mutable Access +/// Entry points for attribute query impl Cache { /// Append the `relative` path to the root directory of the cache and efficiently create leading directories, while assuring that no /// symlinks are in that path. @@ -110,6 +92,7 @@ impl Cache { Find: for<'a> FnMut(&oid, &'a mut Vec) -> Result, E>, E: std::error::Error + Send + Sync + 'static, { + self.statistics.platforms += 1; let mut delegate = StackDelegate { state: &mut self.state, buf: &mut self.buf, @@ -117,6 +100,7 @@ impl Cache { id_mappings: &self.id_mappings, find, case: self.case, + statistics: &mut self.statistics, }; self.stack.make_relative_path_current(relative, &mut delegate)?; Ok(Platform { parent: self, is_dir }) @@ -153,8 +137,25 @@ impl Cache { } } +/// Mutation +impl Cache { + /// Reset the statistics after returning them. + pub fn take_statistics(&mut self) -> Statistics { + std::mem::take(&mut self.statistics) + } + + /// Return our state for applying changes. + pub fn state_mut(&mut self) -> &mut State { + &mut self.state + } +} + /// Access impl Cache { + /// Return the statistics we gathered thus far. + pub fn statistics(&self) -> &Statistics { + &self.statistics + } /// Return the state for introspection. pub fn state(&self) -> &State { &self.state @@ -168,7 +169,8 @@ impl Cache { } } -mod delegate; +/// +pub mod delegate; use delegate::StackDelegate; mod platform; diff --git a/gix-worktree/src/cache/state/attributes.rs b/gix-worktree/src/cache/state/attributes.rs index edc902d45cd..64316b35b43 100644 --- a/gix-worktree/src/cache/state/attributes.rs +++ b/gix-worktree/src/cache/state/attributes.rs @@ -6,6 +6,18 @@ use gix_glob::pattern::Case; use crate::cache::state::{AttributeMatchGroup, Attributes}; use bstr::{BStr, ByteSlice}; +/// Various aggregate numbers related [`Attributes`]. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// Amount of patterns buffers read from the index. + pub patterns_buffers: usize, + /// Amount of pattern files read from disk. + pub pattern_files: usize, + /// Amount of pattern files we tried to find on disk. + pub tried_pattern_files: usize, +} + /// Decide where to read `.gitattributes` files from. #[derive(Default, Debug, Clone, Copy)] pub enum Source { @@ -60,32 +72,21 @@ impl Attributes { self.stack.pop_pattern_list().expect("something to pop"); } + #[allow(clippy::too_many_arguments)] pub(crate) fn push_directory( &mut self, root: &Path, dir: &Path, + rela_dir: &BStr, buf: &mut Vec, id_mappings: &[PathIdMapping], mut find: Find, - case: Case, + stats: &mut Statistics, ) -> std::io::Result<()> where Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec) -> Result, E>, E: std::error::Error + Send + Sync + 'static, { - let dir_bstr = gix_path::into_bstr(dir); - let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( - gix_path::into_bstr(root).as_ref(), - dir_bstr.as_ref(), - None, - case, - ) - .expect("dir in root") - .0; - if rela_dir.starts_with(b"/") { - rela_dir = &rela_dir[1..]; - } - let attr_path_relative = gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitattributes")); let attr_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(attr_path_relative.as_ref())); @@ -102,6 +103,7 @@ impl Attributes { self.stack .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); added = true; + stats.patterns_buffers += 1; } if !added && matches!(self.source, Source::IdMappingThenWorktree) { added = self.stack.add_patterns_file( @@ -111,6 +113,8 @@ impl Attributes { buf, &mut self.collection, )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; } } Source::WorktreeThenIdMapping => { @@ -121,6 +125,8 @@ impl Attributes { buf, &mut self.collection, )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; if let Some(idx) = attr_file_in_index.ok().filter(|_| !added) { let blob = find(&id_mappings[idx].1, buf) .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; @@ -128,6 +134,7 @@ impl Attributes { self.stack .add_patterns_buffer(blob.data, attr_path, Some(Path::new("")), &mut self.collection); added = true; + stats.patterns_buffers += 1; } } } @@ -141,8 +148,11 @@ impl Attributes { // When reading the root, always the first call, we can try to also read the `.git/info/attributes` file which is // by nature never popped, and follows the root, as global. if let Some(info_attr) = self.info_attributes.take() { - self.stack + let added = self + .stack .add_patterns_file(info_attr, true, None, buf, &mut self.collection)?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; } Ok(()) diff --git a/gix-worktree/src/cache/state/ignore.rs b/gix-worktree/src/cache/state/ignore.rs index 9013a82a734..54f953e2cf4 100644 --- a/gix-worktree/src/cache/state/ignore.rs +++ b/gix-worktree/src/cache/state/ignore.rs @@ -1,27 +1,20 @@ use std::path::Path; +use crate::cache::state::Ignore; use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; -use bstr::{BStr, BString, ByteSlice}; +use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; -/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read -/// ignore files from disk or from the index each time the directory changes. -#[derive(Default, Clone)] -#[allow(unused)] -pub struct Ignore { - /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to - /// be consulted. - overrides: IgnoreMatchGroup, - /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. - stack: IgnoreMatchGroup, - /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. - globals: IgnoreMatchGroup, - /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the - /// currently set directory had a pattern matched. Note that this one could be negated. - /// (index into match groups, index into list of pattern lists, index into pattern list) - matched_directory_patterns_stack: Vec>, - /// The name of the file to look for in directories. - pub(crate) exclude_file_name_for_directories: BString, +/// Various aggregate numbers related [`Attributes`]. +#[derive(Default, Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Statistics { + /// Amount of patterns buffers read from the index. + pub patterns_buffers: usize, + /// Amount of pattern files read from disk. + pub pattern_files: usize, + /// Amount of pattern files we tried to find on disk. + pub tried_pattern_files: usize, } impl Ignore { @@ -131,31 +124,22 @@ impl Ignore { }) } + #[allow(clippy::too_many_arguments)] pub(crate) fn push_directory( &mut self, root: &Path, dir: &Path, + rela_dir: &BStr, buf: &mut Vec, id_mappings: &[PathIdMapping], mut find: Find, case: Case, + stats: &mut Statistics, ) -> std::io::Result<()> where Find: for<'b> FnMut(&gix_hash::oid, &'b mut Vec) -> Result, E>, E: std::error::Error + Send + Sync + 'static, { - let dir_bstr = gix_path::into_bstr(dir); - let mut rela_dir = gix_glob::search::pattern::strip_base_handle_recompute_basename_pos( - gix_path::into_bstr(root).as_ref(), - dir_bstr.as_ref(), - None, - case, - ) - .expect("dir in root") - .0; - if rela_dir.starts_with(b"/") { - rela_dir = &rela_dir[1..]; - } self.matched_directory_patterns_stack .push(self.matching_exclude_pattern_no_dir(rela_dir, Some(true), case)); @@ -163,13 +147,16 @@ impl Ignore { gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref())); let follow_symlinks = ignore_file_in_index.is_err(); - if !gix_glob::search::add_patterns_file( + let added = gix_glob::search::add_patterns_file( &mut self.stack.patterns, dir.join(".gitignore"), follow_symlinks, Some(root), buf, - )? { + )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; + if !added { match ignore_file_in_index { Ok(idx) => { let ignore_blob = find(&id_mappings[idx].1, buf) @@ -177,6 +164,7 @@ impl Ignore { let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); self.stack .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); + stats.patterns_buffers += 1; } Err(_) => { // Need one stack level per component so push and pop matches. diff --git a/gix-worktree/src/cache/state/mod.rs b/gix-worktree/src/cache/state/mod.rs index 779b5414742..9d6dc03a669 100644 --- a/gix-worktree/src/cache/state/mod.rs +++ b/gix-worktree/src/cache/state/mod.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use bstr::ByteSlice; +use bstr::{BString, ByteSlice}; use gix_glob::pattern::Case; use crate::{cache::State, PathIdMapping}; @@ -27,10 +27,30 @@ pub struct Attributes { source: attributes::Source, } +/// State related to the exclusion of files, supporting static overrides and globals, along with a stack of dynamically read +/// ignore files from disk or from the index each time the directory changes. +#[derive(Default, Clone)] +#[allow(unused)] +pub struct Ignore { + /// Ignore patterns passed as overrides to everything else, typically passed on the command-line and the first patterns to + /// be consulted. + overrides: IgnoreMatchGroup, + /// Ignore patterns that match the currently set director (in the stack), which is pushed and popped as needed. + stack: IgnoreMatchGroup, + /// Ignore patterns which aren't tied to the repository root, hence are global. They are consulted last. + globals: IgnoreMatchGroup, + /// A matching stack of pattern indices which is empty if we have just been initialized to indicate that the + /// currently set directory had a pattern matched. Note that this one could be negated. + /// (index into match groups, index into list of pattern lists, index into pattern list) + matched_directory_patterns_stack: Vec>, + /// The name of the file to look for in directories. + pub(crate) exclude_file_name_for_directories: BString, +} + /// pub mod attributes; -mod ignore; -pub use ignore::Ignore; +/// +pub mod ignore; /// Initialization impl State { @@ -38,8 +58,6 @@ impl State { pub fn for_checkout(unlink_on_collision: bool, attributes: Attributes) -> Self { State::CreateDirectoryAndAttributesStack { unlink_on_collision, - #[cfg(debug_assertions)] - test_mkdir_calls: 0, attributes, } } diff --git a/gix-worktree/src/lib.rs b/gix-worktree/src/lib.rs index b228fbd17e4..2626fe508c5 100644 --- a/gix-worktree/src/lib.rs +++ b/gix-worktree/src/lib.rs @@ -45,6 +45,7 @@ pub struct Cache { case: gix_glob::pattern::Case, /// A lookup table for object ids to read from in some situations when looking up attributes or exclusions. id_mappings: Vec, + statistics: cache::Statistics, } pub(crate) type PathIdMapping = (BString, gix_hash::ObjectId); diff --git a/gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh b/gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh index 81ac2fad595..10be29adbcd 100644 --- a/gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh +++ b/gix-worktree/tests/fixtures/make_ignore_and_attributes_setup.sh @@ -111,6 +111,8 @@ other-dir-with-ignore/other-sub-level-dir-anywhere/hello other-dir-with-ignore/other-sub-level-dir-anywhere/ dir-with-ignore/negated dir-with-ignore/negated-dir/hello +User-file-ANYWHERE +User-Dir-ANYWHERE a/b/C a/B/c A/B/C diff --git a/gix-worktree/tests/worktree/cache/create_directory.rs b/gix-worktree/tests/worktree/cache/create_directory.rs index fe7a02c8962..ddb369e5027 100644 --- a/gix-worktree/tests/worktree/cache/create_directory.rs +++ b/gix-worktree/tests/worktree/cache/create_directory.rs @@ -17,11 +17,11 @@ fn root_is_assumed_to_exist_and_files_in_root_do_not_create_directory() -> crate Vec::new(), Default::default(), ); - assert_eq!(cache.num_mkdir_calls(), 0); + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); let path = cache.at_path("hello", Some(false), panic_on_find)?.path(); assert!(!path.parent().unwrap().exists(), "prefix itself is never created"); - assert_eq!(cache.num_mkdir_calls(), 0); + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 0); Ok(()) } @@ -43,7 +43,7 @@ fn directory_paths_are_created_in_full() { assert!(path.parent().unwrap().is_dir(), "dir exists"); } - assert_eq!(cache.num_mkdir_calls(), 3); + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 3); } #[test] @@ -54,7 +54,7 @@ fn existing_directories_are_fine() -> crate::Result { let path = cache.at_path("dir/file", Some(false), panic_on_find)?.path(); assert!(path.parent().unwrap().is_dir(), "directory is still present"); assert!(!path.exists(), "it won't create the file"); - assert_eq!(cache.num_mkdir_calls(), 1); + assert_eq!(cache.statistics().delegate.num_mkdir_calls, 1); Ok(()) } @@ -67,7 +67,12 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R std::fs::write(tmp.path().join("file-in-dir"), &[])?; for dirname in &["file-in-dir", "link-to-dir"] { - cache.unlink_on_collision(false); + if let cache::State::CreateDirectoryAndAttributesStack { + unlink_on_collision, .. + } = cache.state_mut() + { + *unlink_on_collision = false; + } let relative_path = format!("{}/file", dirname); assert_eq!( cache @@ -78,20 +83,25 @@ fn symlinks_or_files_in_path_are_forbidden_or_unlinked_when_forced() -> crate::R ); } assert_eq!( - cache.num_mkdir_calls(), + cache.statistics().delegate.num_mkdir_calls, 2, "it tries to create each directory once, but it's a file" ); - cache.reset_mkdir_calls(); + cache.take_statistics(); for dirname in &["link-to-dir", "file-in-dir"] { - cache.unlink_on_collision(true); + if let cache::State::CreateDirectoryAndAttributesStack { + unlink_on_collision, .. + } = cache.state_mut() + { + *unlink_on_collision = true; + } let relative_path = format!("{}/file", dirname); let path = cache.at_path(&relative_path, Some(false), panic_on_find)?.path(); assert!(path.parent().unwrap().is_dir(), "directory was forcefully created"); assert!(!path.exists()); } assert_eq!( - cache.num_mkdir_calls(), + cache.statistics().delegate.num_mkdir_calls, 4, "like before, but it unlinks what's there and tries again" ); diff --git a/gix-worktree/tests/worktree/cache/ignore.rs b/gix-worktree/tests/worktree/cache/ignore.rs index 393a86ad776..8c200210fec 100644 --- a/gix-worktree/tests/worktree/cache/ignore.rs +++ b/gix-worktree/tests/worktree/cache/ignore.rs @@ -154,15 +154,5 @@ fn check_against_baseline() -> crate::Result { } } } - - cache.set_case(Case::Fold); - let platform = cache.at_entry("User-file-ANYWHERE", Some(false), |oid, buf| odb.find_blob(oid, buf))?; - let m = platform.matching_exclude_pattern().expect("match"); - assert_eq!(m.pattern.text, "user-file-anywhere"); - - cache.set_case(Case::Fold); - let platform = cache.at_entry("User-Dir-ANYWHERE", Some(true), |oid, buf| odb.find_blob(oid, buf))?; - let m = platform.matching_exclude_pattern().expect("match"); - assert_eq!(m.pattern.text, "user-Dir-anywhere"); Ok(()) } From c402891d269a77913be39a92b1fc7fccba509557 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 21:03:03 +0200 Subject: [PATCH 14/18] feat: `cache::state::ignore::Source` to specify where to read `.gitignore` files from. This allows better tuning and makes it more versatile for usage in any application, not just `git`. --- gix-worktree/src/cache/state/ignore.rs | 85 +++++++++++++++------ gix-worktree/src/cache/state/mod.rs | 16 +++- gix-worktree/src/checkout/function.rs | 2 +- gix-worktree/tests/worktree/cache/ignore.rs | 5 +- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/gix-worktree/src/cache/state/ignore.rs b/gix-worktree/src/cache/state/ignore.rs index 54f953e2cf4..5ff4ccd42b5 100644 --- a/gix-worktree/src/cache/state/ignore.rs +++ b/gix-worktree/src/cache/state/ignore.rs @@ -5,7 +5,24 @@ use crate::{cache::state::IgnoreMatchGroup, PathIdMapping}; use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; -/// Various aggregate numbers related [`Attributes`]. +/// Decide where to read `.gitignore` files from. +#[derive(Default, Debug, Clone, Copy)] +pub enum Source { + /// Retrieve ignore files from id mappings, see + /// [State::id_mappings_from_index()][crate::cache::State::id_mappings_from_index()]. + /// + /// These mappings are typically produced from an index. + /// If a tree should be the source, build an attribute list from a tree instead, or convert a tree to an index. + /// + /// Use this when no worktree checkout is available, like in bare repositories or when accessing blobs from other parts + /// of the history which aren't checked out. + IdMapping, + /// Read from the worktree and if not present, read them from the id mappings *if* these don't have the skip-worktree bit set. + #[default] + WorktreeThenIdMappingIfNotSkipped, +} + +/// Various aggregate numbers related [`Ignore`]. #[derive(Default, Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Statistics { @@ -27,6 +44,7 @@ impl Ignore { overrides: IgnoreMatchGroup, globals: IgnoreMatchGroup, exclude_file_name_for_directories: Option<&BStr>, + source: Source, ) -> Self { Ignore { overrides, @@ -36,6 +54,7 @@ impl Ignore { exclude_file_name_for_directories: exclude_file_name_for_directories .map(ToOwned::to_owned) .unwrap_or_else(|| ".gitignore".into()), + source, } } } @@ -146,29 +165,49 @@ impl Ignore { let ignore_path_relative = gix_path::to_unix_separators_on_windows(gix_path::join_bstr_unix_pathsep(rela_dir, ".gitignore")); let ignore_file_in_index = id_mappings.binary_search_by(|t| t.0.as_bstr().cmp(ignore_path_relative.as_ref())); - let follow_symlinks = ignore_file_in_index.is_err(); - let added = gix_glob::search::add_patterns_file( - &mut self.stack.patterns, - dir.join(".gitignore"), - follow_symlinks, - Some(root), - buf, - )?; - stats.pattern_files += usize::from(added); - stats.tried_pattern_files += 1; - if !added { - match ignore_file_in_index { - Ok(idx) => { - let ignore_blob = find(&id_mappings[idx].1, buf) - .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; - let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); - self.stack - .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); - stats.patterns_buffers += 1; + match self.source { + Source::IdMapping => { + match ignore_file_in_index { + Ok(idx) => { + let ignore_blob = find(&id_mappings[idx].1, buf) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); + self.stack + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); + stats.patterns_buffers += 1; + } + Err(_) => { + // Need one stack level per component so push and pop matches. + self.stack.patterns.push(Default::default()) + } } - Err(_) => { - // Need one stack level per component so push and pop matches. - self.stack.patterns.push(Default::default()) + } + Source::WorktreeThenIdMappingIfNotSkipped => { + let follow_symlinks = ignore_file_in_index.is_err(); + let added = gix_glob::search::add_patterns_file( + &mut self.stack.patterns, + dir.join(".gitignore"), + follow_symlinks, + Some(root), + buf, + )?; + stats.pattern_files += usize::from(added); + stats.tried_pattern_files += 1; + if !added { + match ignore_file_in_index { + Ok(idx) => { + let ignore_blob = find(&id_mappings[idx].1, buf) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?; + let ignore_path = gix_path::from_bstring(ignore_path_relative.into_owned()); + self.stack + .add_patterns_buffer(ignore_blob.data, ignore_path, Some(Path::new(""))); + stats.patterns_buffers += 1; + } + Err(_) => { + // Need one stack level per component so push and pop matches. + self.stack.patterns.push(Default::default()) + } + } } } } diff --git a/gix-worktree/src/cache/state/mod.rs b/gix-worktree/src/cache/state/mod.rs index 9d6dc03a669..ae2c6bafd6a 100644 --- a/gix-worktree/src/cache/state/mod.rs +++ b/gix-worktree/src/cache/state/mod.rs @@ -45,6 +45,8 @@ pub struct Ignore { matched_directory_patterns_stack: Vec>, /// The name of the file to look for in directories. pub(crate) exclude_file_name_for_directories: BString, + /// Where to read ignore files from + source: ignore::Source, } /// @@ -91,6 +93,7 @@ impl State { &self, index: &gix_index::State, paths: &gix_index::PathStorageRef, + ignore_source: ignore::Source, case: Case, ) -> Vec { let a1_backing; @@ -133,9 +136,16 @@ impl State { } .then_some(t.1) })?; - // See https://github.com/git/git/blob/master/dir.c#L912:L912 - if is_ignore && !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { - return None; + if is_ignore { + match ignore_source { + ignore::Source::IdMapping => {} + ignore::Source::WorktreeThenIdMappingIfNotSkipped => { + // See https://github.com/git/git/blob/master/dir.c#L912:L912 + if !entry.flags.contains(gix_index::entry::Flags::SKIP_WORKTREE) { + return None; + } + } + }; } Some((path.to_owned(), entry.id)) } else { diff --git a/gix-worktree/src/checkout/function.rs b/gix-worktree/src/checkout/function.rs index 089938795e1..8e69fd4d6a6 100644 --- a/gix-worktree/src/checkout/function.rs +++ b/gix-worktree/src/checkout/function.rs @@ -58,7 +58,7 @@ where ); let state = cache::State::for_checkout(options.overwrite_existing, options.attributes.clone()); - let attribute_files = state.id_mappings_from_index(index, paths, case); + let attribute_files = state.id_mappings_from_index(index, paths, Default::default(), case); let mut ctx = chunk::Context { buf: Vec::new(), path_cache: Cache::new(dir, state, case, Vec::with_capacity(512), attribute_files), diff --git a/gix-worktree/tests/worktree/cache/ignore.rs b/gix-worktree/tests/worktree/cache/ignore.rs index 8c200210fec..671d061ff5a 100644 --- a/gix-worktree/tests/worktree/cache/ignore.rs +++ b/gix-worktree/tests/worktree/cache/ignore.rs @@ -1,6 +1,7 @@ use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; use gix_odb::FindExt; +use gix_worktree::cache::state::ignore::Source; use gix_worktree::Cache; use crate::hex_to_id; @@ -43,6 +44,7 @@ fn special_exclude_cases_we_handle_differently() { Default::default(), gix_ignore::Search::from_git_dir(&git_dir, None, &mut buf).unwrap(), None, + Source::WorktreeThenIdMappingIfNotSkipped, ), ); let mut cache = Cache::new(&dir, state, case, buf, Default::default()); @@ -103,10 +105,11 @@ fn check_against_baseline() -> crate::Result { gix_ignore::Search::from_overrides(vec!["!force-include"]), gix_ignore::Search::from_git_dir(&git_dir, Some(user_exclude_path), &mut buf)?, None, + Source::WorktreeThenIdMappingIfNotSkipped, ), ); let paths_storage = index.take_path_backing(); - let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, case); + let attribute_files_in_index = state.id_mappings_from_index(&index, &paths_storage, Default::default(), case); assert_eq!( attribute_files_in_index, vec![( From 27a39cad498ca8b2c9cba05790284e2b68ba7636 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 21:09:44 +0200 Subject: [PATCH 15/18] adjust to changes in `gix-worktree` --- gitoxide-core/src/repository/exclude.rs | 6 +++++- gix/src/config/cache/access.rs | 2 ++ gix/src/repository/attributes.rs | 17 ++++++++++------- gix/src/repository/excludes.rs | 8 +++++--- gix/src/worktree/mod.rs | 22 ++++++++++++---------- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/gitoxide-core/src/repository/exclude.rs b/gitoxide-core/src/repository/exclude.rs index d5bde391f78..83328deba07 100644 --- a/gitoxide-core/src/repository/exclude.rs +++ b/gitoxide-core/src/repository/exclude.rs @@ -32,7 +32,11 @@ pub fn query( } let index = repo.index()?; - let mut cache = repo.excludes(&index, Some(gix::ignore::Search::from_overrides(overrides)))?; + let mut cache = repo.excludes( + &index, + Some(gix::ignore::Search::from_overrides(overrides)), + Default::default(), + )?; let prefix = repo.prefix().expect("worktree - we have an index by now")?; diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index 02caf9e327a..77324efe35b 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -198,6 +198,7 @@ impl Cache { &self, git_dir: &std::path::Path, overrides: Option, + source: gix_worktree::cache::state::ignore::Source, buf: &mut Vec, ) -> Result { let excludes_file = match self.excludes_file().transpose()? { @@ -208,6 +209,7 @@ impl Cache { overrides.unwrap_or_default(), gix_ignore::Search::from_git_dir(git_dir, excludes_file, buf)?, None, + source, )) } // TODO: at least one test, maybe related to core.attributesFile configuration. diff --git a/gix/src/repository/attributes.rs b/gix/src/repository/attributes.rs index e43ae78e05f..25252976132 100644 --- a/gix/src/repository/attributes.rs +++ b/gix/src/repository/attributes.rs @@ -19,7 +19,8 @@ impl Repository { pub fn attributes( &self, index: &gix_index::State, - source: gix_worktree::cache::state::attributes::Source, + attributes_source: gix_worktree::cache::state::attributes::Source, + ignore_source: gix_worktree::cache::state::ignore::Source, exclude_overrides: Option, ) -> Result { let case = if self.config.ignore_case { @@ -27,14 +28,16 @@ impl Repository { } else { gix_glob::pattern::Case::Sensitive }; - let (attributes, mut buf) = + let (attributes, mut buf) = self.config.assemble_attribute_globals( + self.git_dir(), + attributes_source, + self.options.permissions.attributes, + )?; + let ignore = self.config - .assemble_attribute_globals(self.git_dir(), source, self.options.permissions.attributes)?; - let ignore = self - .config - .assemble_exclude_globals(self.git_dir(), exclude_overrides, &mut buf)?; + .assemble_exclude_globals(self.git_dir(), exclude_overrides, ignore_source, &mut buf)?; let state = gix_worktree::cache::State::AttributesAndIgnoreStack { attributes, ignore }; - let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), ignore_source, case); Ok(gix_worktree::Cache::new( // this is alright as we don't cause mutation of that directory, it's virtual. self.work_dir().unwrap_or(self.git_dir()), diff --git a/gix/src/repository/excludes.rs b/gix/src/repository/excludes.rs index c36162b40d1..6281549e074 100644 --- a/gix/src/repository/excludes.rs +++ b/gix/src/repository/excludes.rs @@ -1,7 +1,8 @@ //! exclude information use crate::{config, Repository}; impl Repository { - /// Configure a file-system cache checking if files below the repository are excluded. + /// Configure a file-system cache checking if files below the repository are excluded, reading `.gitignore` files from + /// the specified `source`. /// /// Note that no worktree is required for this to work, even though access to in-tree `.gitignore` files would require /// a non-empty `index` that represents a tree with `.gitignore` files. @@ -19,6 +20,7 @@ impl Repository { &self, index: &gix_index::State, overrides: Option, + source: gix_worktree::cache::state::ignore::Source, ) -> Result { let case = if self.config.ignore_case { gix_glob::pattern::Case::Fold @@ -28,9 +30,9 @@ impl Repository { let mut buf = Vec::with_capacity(512); let ignore = self .config - .assemble_exclude_globals(self.git_dir(), overrides, &mut buf)?; + .assemble_exclude_globals(self.git_dir(), overrides, source, &mut buf)?; let state = gix_worktree::cache::State::IgnoreStack(ignore); - let attribute_list = state.id_mappings_from_index(index, index.path_backing(), case); + let attribute_list = state.id_mappings_from_index(index, index.path_backing(), source, case); Ok(gix_worktree::Cache::new( // this is alright as we don't cause mutation of that directory, it's virtual. self.work_dir().unwrap_or(self.git_dir()), diff --git a/gix/src/worktree/mod.rs b/gix/src/worktree/mod.rs index 894536820db..8db123554de 100644 --- a/gix/src/worktree/mod.rs +++ b/gix/src/worktree/mod.rs @@ -118,7 +118,11 @@ pub mod excludes { /// [`Worktree::attributes()`][crate::Worktree::attributes()] for accessing both attributes and excludes. pub fn excludes(&self, overrides: Option) -> Result { let index = self.index()?; - Ok(self.parent.excludes(&index, overrides)?) + Ok(self.parent.excludes( + &index, + overrides, + gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, + )?) } } } @@ -138,20 +142,18 @@ pub mod attributes { impl<'repo> crate::Worktree<'repo> { /// Configure a file-system cache checking if files below the repository are excluded or for querying their attributes. /// - /// Use `attribute_source` to specify where to read attributes from. Also note that exclude information will - /// always try to read `.gitignore` files from disk before trying to read it from the `index`. - /// /// This takes into consideration all the usual repository configuration, namely: /// /// * `$XDG_CONFIG_HOME/…/ignore|attributes` if `core.excludesFile|attributesFile` is *not* set, otherwise use the configured file. /// * `$GIT_DIR/info/exclude|attributes` if present. - pub fn attributes( - &self, - source: gix_worktree::cache::state::attributes::Source, - overrides: Option, - ) -> Result { + pub fn attributes(&self, overrides: Option) -> Result { let index = self.index()?; - Ok(self.parent.attributes(&index, source, overrides)?) + Ok(self.parent.attributes( + &index, + gix_worktree::cache::state::attributes::Source::WorktreeThenIdMapping, + gix_worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped, + overrides, + )?) } } } From bd1ae0db32191fb96d6b2a55b9fcb635d1652c1f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 20:56:37 +0200 Subject: [PATCH 16/18] fix: `join_bstr_unix_pathsep()` works more suitably if base path is empty. --- gix-path/src/convert.rs | 2 +- gix-path/tests/convert/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gix-path/src/convert.rs b/gix-path/src/convert.rs index c4231c98801..d8bf7035326 100644 --- a/gix-path/src/convert.rs +++ b/gix-path/src/convert.rs @@ -85,7 +85,7 @@ pub fn into_bstr<'a>(path: impl Into>) -> Cow<'a, BStr> { /// Join `path` to `base` such that they are separated with a `/`, i.e. `base/path`. pub fn join_bstr_unix_pathsep<'a, 'b>(base: impl Into>, path: impl Into<&'b BStr>) -> Cow<'a, BStr> { let mut base = base.into(); - if base.last() != Some(&b'/') { + if !base.is_empty() && base.last() != Some(&b'/') { base.to_mut().push(b'/'); } base.to_mut().extend_from_slice(path.into()); diff --git a/gix-path/tests/convert/mod.rs b/gix-path/tests/convert/mod.rs index 43234bfbd3c..4f43691e6bf 100644 --- a/gix-path/tests/convert/mod.rs +++ b/gix-path/tests/convert/mod.rs @@ -56,9 +56,9 @@ mod join_bstr_unix_pathsep { assert_eq!(join_bstr_unix_pathsep(b("base/"), ""), b("base/")); } #[test] - fn empty_base_creates_absolute_paths() { - assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("/")); - assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("/hi")); - assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("//hi")); + fn empty_base_leaves_everything_untouched() { + assert_eq!(join_bstr_unix_pathsep(b(""), ""), b("")); + assert_eq!(join_bstr_unix_pathsep(b(""), "hi"), b("hi")); + assert_eq!(join_bstr_unix_pathsep(b(""), "/hi"), b("/hi")); } } From 08e8fc2152794652ba1c986df493c2ac915af9e7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 09:21:03 +0200 Subject: [PATCH 17/18] feat: `gix index entries` also prints attributes. --- gitoxide-core/src/repository/index/entries.rs | 286 +++++++++++++----- gitoxide-core/src/repository/index/mod.rs | 4 +- gix/Cargo.toml | 1 + src/plumbing/main.rs | 38 ++- src/plumbing/options/free.rs | 2 +- src/plumbing/options/mod.rs | 18 +- 6 files changed, 268 insertions(+), 81 deletions(-) diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 05227dd88d2..07241e52f52 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/gitoxide-core/src/repository/index/entries.rs @@ -1,86 +1,228 @@ -pub fn entries(repo: gix::Repository, mut out: impl std::io::Write, format: crate::OutputFormat) -> anyhow::Result<()> { - use crate::OutputFormat::*; - let index = repo.index()?; +#[derive(Debug)] +pub struct Options { + pub format: crate::OutputFormat, + /// If true, also show attributes + pub attributes: Option, + pub statistics: bool, +} - #[cfg(feature = "serde")] - if let Json = format { - out.write_all(b"[\n")?; - } +#[derive(Debug)] +pub enum Attributes { + /// Look at worktree attributes and index as fallback. + WorktreeAndIndex, + /// Look at attributes from index files only. + Index, +} + +pub(crate) mod function { + use crate::repository::index::entries::{Attributes, Options}; + use gix::attrs::State; + use gix::bstr::ByteSlice; + use gix::odb::FindExt; + use std::borrow::Cow; + use std::io::{BufWriter, Write}; - let mut entries = index.entries().iter().peekable(); - while let Some(entry) = entries.next() { - match format { - Human => to_human(&mut out, &index, entry)?, - #[cfg(feature = "serde")] - Json => to_json(&mut out, &index, entry, entries.peek().is_none())?, + pub fn entries( + repo: gix::Repository, + out: impl std::io::Write, + mut err: impl std::io::Write, + Options { + format, + attributes, + statistics, + }: Options, + ) -> anyhow::Result<()> { + use crate::OutputFormat::*; + let index = repo.index()?; + let mut cache = attributes + .map(|attrs| { + repo.attributes( + &index, + match attrs { + Attributes::WorktreeAndIndex => { + gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping + } + Attributes::Index => gix::worktree::cache::state::attributes::Source::IdMapping, + }, + match attrs { + Attributes::WorktreeAndIndex => { + gix::worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped + } + Attributes::Index => gix::worktree::cache::state::ignore::Source::IdMapping, + }, + None, + ) + .map(|cache| (cache.attribute_matches(), cache)) + }) + .transpose()?; + let mut stats = Statistics { + entries: index.entries().len(), + ..Default::default() + }; + + let mut out = BufWriter::new(out); + #[cfg(feature = "serde")] + if let Json = format { + out.write_all(b"[\n")?; + } + let mut entries = index.entries().iter().peekable(); + while let Some(entry) = entries.next() { + let attrs = cache + .as_mut() + .map(|(attrs, cache)| { + cache + .at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf)) + .map(|entry| { + let is_excluded = entry.is_excluded(); + stats.excluded += usize::from(is_excluded); + let attributes: Vec<_> = { + entry.matching_attributes(attrs); + attrs.iter().map(|m| m.assignment.to_owned()).collect() + }; + stats.with_attributes += usize::from(!attributes.is_empty()); + Attrs { + is_excluded, + attributes, + } + }) + }) + .transpose()?; + match format { + Human => to_human(&mut out, &index, entry, attrs)?, + #[cfg(feature = "serde")] + Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?, + } } - } - #[cfg(feature = "serde")] - if let Json = format { - out.write_all(b"]\n")?; + #[cfg(feature = "serde")] + if format == Json { + out.write_all(b"]\n")?; + out.flush()?; + if statistics { + serde_json::to_writer_pretty(&mut err, &stats)?; + } + } + if format == Human && statistics { + out.flush()?; + stats.cache = cache.map(|c| *c.1.statistics()); + writeln!(err, "{:#?}", stats)?; + } + Ok(()) } - Ok(()) -} -#[cfg(feature = "serde")] -pub(crate) fn to_json( - mut out: &mut impl std::io::Write, - index: &gix::index::File, - entry: &gix::index::Entry, - is_last: bool, -) -> anyhow::Result<()> { - use gix::bstr::ByteSlice; + #[cfg_attr(feature = "serde", derive(serde::Serialize))] + struct Attrs { + is_excluded: bool, + attributes: Vec, + } #[cfg_attr(feature = "serde", derive(serde::Serialize))] - struct Entry<'a> { - stat: &'a gix::index::entry::Stat, - hex_id: String, - flags: u32, - mode: u32, - path: std::borrow::Cow<'a, str>, + #[derive(Default, Debug)] + struct Statistics { + #[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial. + pub entries: usize, + pub excluded: usize, + pub with_attributes: usize, + pub cache: Option, } - serde_json::to_writer( - &mut out, - &Entry { - stat: &entry.stat, - hex_id: entry.id.to_hex().to_string(), - flags: entry.flags.bits(), - mode: entry.mode.bits(), - path: entry.path(index).to_str_lossy(), - }, - )?; + #[cfg(feature = "serde")] + fn to_json( + mut out: &mut impl std::io::Write, + index: &gix::index::File, + entry: &gix::index::Entry, + attrs: Option, + is_last: bool, + ) -> anyhow::Result<()> { + #[cfg_attr(feature = "serde", derive(serde::Serialize))] + struct Entry<'a> { + stat: &'a gix::index::entry::Stat, + hex_id: String, + flags: u32, + mode: u32, + path: std::borrow::Cow<'a, str>, + meta: Option, + } + + serde_json::to_writer( + &mut out, + &Entry { + stat: &entry.stat, + hex_id: entry.id.to_hex().to_string(), + flags: entry.flags.bits(), + mode: entry.mode.bits(), + path: entry.path(index).to_str_lossy(), + meta: attrs, + }, + )?; - if is_last { - out.write_all(b"\n")?; - } else { - out.write_all(b",\n")?; + if is_last { + out.write_all(b"\n")?; + } else { + out.write_all(b",\n")?; + } + Ok(()) } - Ok(()) -} -pub(crate) fn to_human( - out: &mut impl std::io::Write, - file: &gix::index::File, - entry: &gix::index::Entry, -) -> std::io::Result<()> { - writeln!( - out, - "{} {}{:?} {} {}", - match entry.flags.stage() { - 0 => "BASE ", - 1 => "OURS ", - 2 => "THEIRS ", - _ => "UNKNOWN", - }, - if entry.flags.is_empty() { - "".to_string() - } else { - format!("{:?} ", entry.flags) - }, - entry.mode, - entry.id, - entry.path(file) - ) + fn to_human( + out: &mut impl std::io::Write, + file: &gix::index::File, + entry: &gix::index::Entry, + attrs: Option, + ) -> std::io::Result<()> { + writeln!( + out, + "{} {}{:?} {} {}{}", + match entry.flags.stage() { + 0 => "BASE ", + 1 => "OURS ", + 2 => "THEIRS ", + _ => "UNKNOWN", + }, + if entry.flags.is_empty() { + "".to_string() + } else { + format!("{:?} ", entry.flags) + }, + entry.mode, + entry.id, + entry.path(file), + attrs + .map(|a| { + let mut buf = String::new(); + if a.is_excluded { + buf.push_str(" ❌"); + } + if !a.attributes.is_empty() { + buf.push_str(" ("); + for assignment in a.attributes { + match assignment.state { + State::Set => { + buf.push_str(assignment.name.as_str()); + } + State::Unset => { + buf.push('-'); + buf.push_str(assignment.name.as_str()); + } + State::Value(v) => { + buf.push_str(assignment.name.as_str()); + buf.push('='); + buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref()); + } + State::Unspecified => { + buf.push('!'); + buf.push_str(assignment.name.as_str()); + } + } + buf.push_str(", "); + } + buf.pop(); + buf.pop(); + buf.push(')'); + } + buf.into() + }) + .unwrap_or(Cow::Borrowed("")) + ) + } } diff --git a/gitoxide-core/src/repository/index/mod.rs b/gitoxide-core/src/repository/index/mod.rs index 7e1b3c274da..bdaa61b5abf 100644 --- a/gitoxide-core/src/repository/index/mod.rs +++ b/gitoxide-core/src/repository/index/mod.rs @@ -35,5 +35,5 @@ pub fn from_tree( Ok(()) } -mod entries; -pub use entries::entries; +pub mod entries; +pub use entries::function::entries; diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 6f557886087..3b6c6bca844 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -67,6 +67,7 @@ serde = [ "dep:serde", "gix-attributes/serde", "gix-ignore/serde", "gix-revision/serde", + "gix-worktree/serde", "gix-credentials/serde"] ## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`. diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index ac05d3964fc..6aafdb5f2ce 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -855,14 +855,35 @@ pub fn main() -> Result<()> { ), }, Subcommands::Index(cmd) => match cmd { - index::Subcommands::Entries => prepare_and_run( + index::Subcommands::Entries { + no_attributes, + attributes_from_index, + statistics, + } => prepare_and_run( "index-entries", verbose, progress, progress_keep_open, None, - move |_progress, out, _err| { - core::repository::index::entries(repository(Mode::LenientWithGitInstallConfig)?, out, format) + move |_progress, out, err| { + core::repository::index::entries( + repository(Mode::LenientWithGitInstallConfig)?, + out, + err, + core::repository::index::entries::Options { + format, + attributes: if no_attributes { + None + } else { + Some(if attributes_from_index { + core::repository::index::entries::Attributes::Index + } else { + core::repository::index::entries::Attributes::WorktreeAndIndex + }) + }, + statistics, + }, + ) }, ), index::Subcommands::FromTree { @@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode { (false, false) => verify::Mode::HashCrc32, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clap() { + use clap::CommandFactory; + Args::command().debug_assert(); + } +} diff --git a/src/plumbing/options/free.rs b/src/plumbing/options/free.rs index 00641f33b28..2869fb0cedf 100644 --- a/src/plumbing/options/free.rs +++ b/src/plumbing/options/free.rs @@ -105,7 +105,7 @@ pub mod pack { /// Possible values are "none" and "tree-traversal". Default is "none". expansion: Option, - #[clap(long, default_value_t = 3, requires = "nondeterministic-count")] + #[clap(long, default_value_t = 3, requires = "nondeterministic_count")] /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting /// to the globally configured threads. /// diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index d2b8d7805d2..107da1b414f 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -361,11 +361,11 @@ pub mod commit { /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. Describe { /// Use annotated tag references only, not all tags. - #[clap(long, short = 't', conflicts_with("all-refs"))] + #[clap(long, short = 't', conflicts_with("all_refs"))] annotated_tags: bool, /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. - #[clap(long, short = 'a', conflicts_with("annotated-tags"))] + #[clap(long, short = 'a', conflicts_with("annotated_tags"))] all_refs: bool, /// Only follow the first parent when traversing the commit graph. @@ -472,7 +472,19 @@ pub mod index { #[derive(Debug, clap::Subcommand)] pub enum Subcommands { /// Print all entries to standard output - Entries, + Entries { + /// Do not visualize excluded entries or attributes per path. + #[clap(long)] + no_attributes: bool, + /// Load attribute and ignore files from the index, don't look at the worktree. + /// + /// This is to see what IO for probing attribute/ignore files does to performance. + #[clap(long, short = 'i', conflicts_with = "no_attributes")] + attributes_from_index: bool, + /// Print various statistics to stderr + #[clap(long, short = 's')] + statistics: bool, + }, /// Create an index from a tree-ish. #[clap(visible_alias = "read-tree")] FromTree { From df28b7d3fb7f114c862ba5559ab3974ce752cb65 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 25 Apr 2023 15:09:18 +0200 Subject: [PATCH 18/18] remove ignored archive to assure tests run as intended --- .../generated-archives/make_ignore_and_attributes_setup.tar.xz | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz diff --git a/gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz b/gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz deleted file mode 100644 index 691803c8af9..00000000000 --- a/gix-worktree/tests/fixtures/generated-archives/make_ignore_and_attributes_setup.tar.xz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:38df6900cdb0ad620e158a3cc19325053658b9cfcb8987779af634813d957f6c -size 11088