From 42d45906d67c6930cb0abb567e59834ac9ae1478 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 08:56:04 +0100 Subject: [PATCH 1/6] fix: cleanup trailing newlines when handling delayed files. (#1129) Otherwise we would have seen newlines in filepaths, which then would not match any file in the list of expected paths. --- gix-filter/src/driver/process/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gix-filter/src/driver/process/client.rs b/gix-filter/src/driver/process/client.rs index 942817807f8..912b1629091 100644 --- a/gix-filter/src/driver/process/client.rs +++ b/gix-filter/src/driver/process/client.rs @@ -185,8 +185,8 @@ impl Client { self.send_command_and_meta(command, meta)?; while let Some(data) = self.out.read_line() { let line = data??; - if let Some(line) = line.as_bstr() { - inspect_line(line); + if let Some(line) = line.as_text() { + inspect_line(line.as_bstr()); } } self.out.reset_with(&[gix_packetline::PacketLineRef::Flush]); From 9d7e28d542b5dbacce28920232b99c454f1eab4e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 16:30:31 +0100 Subject: [PATCH 2/6] feat: Add `Context` to provide much more context just like `git` sets it. (#1129) `git` sets a bunch of context-carrying environment variables which `gix` shouldn't only (and optionally) read, but also pass on to spawned processes. With `Context` it's now possible to gather all of this information and set it at once. With a minimal context, one will also set the `git_dir`, particularly important on servers, which work with many different repositories, or a clone operation which may be in the context of one directory, but affects another. --- gix-command/src/lib.rs | 83 ++++++++++++++++++++++++++- gix-command/tests/command.rs | 106 +++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/gix-command/src/lib.rs b/gix-command/src/lib.rs index 40bf5e81845..a6277379136 100644 --- a/gix-command/src/lib.rs +++ b/gix-command/src/lib.rs @@ -2,12 +2,16 @@ #![deny(rust_2018_idioms, missing_docs)] #![forbid(unsafe_code)] +use bstr::BString; use std::ffi::OsString; +use std::path::PathBuf; /// A structure to keep settings to use when invoking a command via [`spawn()`][Prepare::spawn()], after creating it with [`prepare()`]. pub struct Prepare { /// The command to invoke (either with or without shell depending on `use_shell`. pub command: OsString, + /// Additional information to be passed to the spawned command. + pub context: Option, /// The way standard input is configured. pub stdin: std::process::Stdio, /// The way standard output is configured. @@ -35,6 +39,37 @@ pub struct Prepare { pub allow_manual_arg_splitting: bool, } +/// Additional information that is relevant to spawned processes, which typically receive +/// a wealth of contextual information when spawned from `git`. +/// +/// See [the git source code](https://github.com/git/git/blob/cfb8a6e9a93adbe81efca66e6110c9b4d2e57169/git.c#L191) +/// for details. +#[derive(Debug, Default, Clone)] +pub struct Context { + /// The `.git` directory that contains the repository. + /// + /// If set, it will be used to set the the `GIT_DIR` environment variable. + pub git_dir: Option, + /// Set the `GIT_WORK_TREE` environment variable with the given path. + pub worktree_dir: Option, + /// If `true`, set `GIT_NO_REPLACE_OBJECTS` to `1`, which turns off object replacements, or `0` otherwise. + /// If `None`, the variable won't be set. + pub no_replace_objects: Option, + /// Set the `GIT_NAMESPACE` variable with the given value, effectively namespacing all + /// operations on references. + pub ref_namespace: Option, + /// If `true`, set `GIT_LITERAL_PATHSPECS` to `1`, which makes globs literal and prefixes as well, or `0` otherwise. + /// If `None`, the variable won't be set. + pub literal_pathspecs: Option, + /// If `true`, set `GIT_GLOB_PATHSPECS` to `1`, which lets wildcards not match the `/` character, and equals the `:(glob)` prefix. + /// If `false`, set `GIT_NOGLOB_PATHSPECS` to `1` which lets globs match only themselves. + /// If `None`, the variable won't be set. + pub glob_pathspecs: Option, + /// If `true`, set `GIT_ICASE_PATHSPECS` to `1`, to let patterns match case-insensitively, or `0` otherwise. + /// If `None`, the variable won't be set. + pub icase_pathspecs: Option, +} + mod prepare { use std::{ ffi::OsString, @@ -43,7 +78,7 @@ mod prepare { use bstr::ByteSlice; - use crate::Prepare; + use crate::{Context, Prepare}; /// Builder impl Prepare { @@ -67,6 +102,15 @@ mod prepare { self } + /// Set additional `ctx` to be used when spawning the process. + /// + /// Note that this is a must for most kind of commands that `git` usually spawns, + /// as at least they need to know the correct `git` repository to function. + pub fn with_context(mut self, ctx: Context) -> Self { + self.context = Some(ctx); + self + } + /// Use a shell, but try to split arguments by hand if this be safely done without a shell. /// /// If that's not the case, use a shell instead. @@ -164,6 +208,36 @@ mod prepare { .stderr(prep.stderr) .envs(prep.env) .args(prep.args); + if let Some(ctx) = prep.context { + if let Some(git_dir) = ctx.git_dir { + cmd.env("GIT_DIR", &git_dir); + } + if let Some(worktree_dir) = ctx.worktree_dir { + cmd.env("GIT_WORK_TREE", worktree_dir); + } + if let Some(value) = ctx.no_replace_objects { + cmd.env("GIT_NO_REPLACE_OBJECTS", usize::from(value).to_string()); + } + if let Some(namespace) = ctx.ref_namespace { + cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace)); + } + if let Some(value) = ctx.literal_pathspecs { + cmd.env("GIT_LITERAL_PATHSPECS", usize::from(value).to_string()); + } + if let Some(value) = ctx.glob_pathspecs { + cmd.env( + if value { + "GIT_GLOB_PATHSPECS" + } else { + "GIT_NOGLOB_PATHSPECS" + }, + "1", + ); + } + if let Some(value) = ctx.icase_pathspecs { + cmd.env("GIT_ICASE_PATHSPECS", usize::from(value).to_string()); + } + } cmd } } @@ -176,9 +250,16 @@ mod prepare { /// - `stdin` is null to prevent blocking unexpectedly on consumption of stdin /// - `stdout` is captured for consumption by the caller /// - `stderr` is inherited to allow the command to provide context to the user +/// +/// ### Warning +/// +/// When using this method, be sure that the invoked program doesn't rely on the current working dir and/or +/// environment variables to know its context. If so, call instead [`Prepare::with_context()`] to provide +/// additional information. pub fn prepare(cmd: impl Into) -> Prepare { Prepare { command: cmd.into(), + context: None, stdin: std::process::Stdio::null(), stdout: std::process::Stdio::piped(), stderr: std::process::Stdio::inherit(), diff --git a/gix-command/tests/command.rs b/gix-command/tests/command.rs index 645f7fb87a6..d9cb3bb9686 100644 --- a/gix-command/tests/command.rs +++ b/gix-command/tests/command.rs @@ -1,5 +1,111 @@ use gix_testtools::Result; +mod context { + use gix_command::Context; + + fn winfix(expected: impl Into) -> String { + // Unclear why it's not debug-printing the env on windows. + if cfg!(windows) { + "\"\"".into() + } else { + expected.into() + } + } + + #[test] + fn git_dir_sets_git_dir_env_and_cwd() { + let ctx = Context { + git_dir: Some(".".into()), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_DIR="." """#)); + } + + #[test] + fn worktree_dir_sets_env_only() { + let ctx = Context { + worktree_dir: Some(".".into()), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_WORK_TREE="." """#)); + } + + #[test] + fn no_replace_objects_sets_env_only() { + for value in [false, true] { + let expected = usize::from(value); + let ctx = Context { + no_replace_objects: Some(value), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!( + format!("{cmd:?}"), + winfix(format!(r#"GIT_NO_REPLACE_OBJECTS="{expected}" """#)) + ); + } + } + + #[test] + fn ref_namespace_sets_env_only() { + let ctx = Context { + ref_namespace: Some("namespace".into()), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_NAMESPACE="namespace" """#)); + } + + #[test] + fn literal_pathspecs_sets_env_only() { + for value in [false, true] { + let expected = usize::from(value); + let ctx = Context { + literal_pathspecs: Some(value), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!( + format!("{cmd:?}"), + winfix(format!(r#"GIT_LITERAL_PATHSPECS="{expected}" """#)) + ); + } + } + + #[test] + fn glob_pathspecs_sets_env_only() { + for (value, expected) in [ + (false, "GIT_NOGLOB_PATHSPECS=\"1\""), + (true, "GIT_GLOB_PATHSPECS=\"1\""), + ] { + let ctx = Context { + glob_pathspecs: Some(value), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!(format!("{cmd:?}"), winfix(format!(r#"{expected} """#))); + } + } + + #[test] + fn icase_pathspecs_sets_env_only() { + for value in [false, true] { + let expected = usize::from(value); + let ctx = Context { + icase_pathspecs: Some(value), + ..Default::default() + }; + let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx)); + assert_eq!( + format!("{cmd:?}"), + winfix(format!(r#"GIT_ICASE_PATHSPECS="{expected}" """#)) + ); + } + } +} + mod prepare { #[cfg(windows)] const SH: &str = "sh"; From 969ff0f6091196134307554a54e5cf82734d9252 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 16:59:23 +0100 Subject: [PATCH 3/6] fix!: pass crucial context to help spawning filter processes by adding `context` to `Pipeline::new()`. (#1129) Otherwise, they might now know which repository to apply to, leading to errors. --- gix-filter/src/driver/apply.rs | 1 + gix-filter/src/driver/delayed.rs | 1 + gix-filter/src/driver/init.rs | 12 +++++++++--- gix-filter/src/driver/mod.rs | 15 +++++++++++++++ gix-filter/src/pipeline/mod.rs | 14 +++++++++----- gix-filter/tests/pipeline/mod.rs | 1 + 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/gix-filter/src/driver/apply.rs b/gix-filter/src/driver/apply.rs index 576369a0716..25c8d3199b8 100644 --- a/gix-filter/src/driver/apply.rs +++ b/gix-filter/src/driver/apply.rs @@ -63,6 +63,7 @@ pub struct Context<'a, 'b> { pub blob: Option, } +/// Apply operations to filter programs. impl State { /// Apply `operation` of `driver` to the bytes read from `src` and return a reader to immediately consume the output /// produced by the filter. `rela_path` is the repo-relative path of the entry to handle. diff --git a/gix-filter/src/driver/delayed.rs b/gix-filter/src/driver/delayed.rs index bb525a23ba4..4599ac61098 100644 --- a/gix-filter/src/driver/delayed.rs +++ b/gix-filter/src/driver/delayed.rs @@ -45,6 +45,7 @@ pub mod fetch { } } +/// Operations related to delayed filtering. impl State { /// Return a list of delayed paths for `process` that can then be obtained with [`fetch_delayed()`][Self::fetch_delayed()]. /// diff --git a/gix-filter/src/driver/init.rs b/gix-filter/src/driver/init.rs index c7fab986ea7..616d4d7193b 100644 --- a/gix-filter/src/driver/init.rs +++ b/gix-filter/src/driver/init.rs @@ -24,6 +24,7 @@ pub enum Error { }, } +/// Lifecycle impl State { /// Obtain a process as defined in `driver` suitable for a given `operation. `rela_path` may be used to substitute the current /// file for use in the invoked `SingleFile` process. @@ -40,7 +41,7 @@ impl State { let client = match self.running.remove(process) { Some(c) => c, None => { - let (child, cmd) = spawn_driver(process.clone())?; + let (child, cmd) = spawn_driver(process.clone(), &self.context)?; process::Client::handshake(child, "git-filter", &[2], &["clean", "smudge", "delay"]).map_err( |err| Error::ProcessHandshake { source: err, @@ -79,20 +80,25 @@ impl State { None => return Ok(None), }; - let (child, command) = spawn_driver(cmd)?; + let (child, command) = spawn_driver(cmd, &self.context)?; Ok(Some(Process::SingleFile { child, command })) } } } } -fn spawn_driver(cmd: BString) -> Result<(std::process::Child, std::process::Command), Error> { +fn spawn_driver( + cmd: BString, + context: &gix_command::Context, +) -> Result<(std::process::Child, std::process::Command), Error> { let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstr(cmd).into_owned()) .with_shell() + .with_context(context.clone()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .into(); + gix_trace::debug!(cmd = ?cmd, "launching filter driver"); let child = match cmd.spawn() { Ok(child) => child, Err(err) => { diff --git a/gix-filter/src/driver/mod.rs b/gix-filter/src/driver/mod.rs index 326adbd274d..43d3edb2f8b 100644 --- a/gix-filter/src/driver/mod.rs +++ b/gix-filter/src/driver/mod.rs @@ -70,12 +70,27 @@ pub struct State { /// Note that these processes are expected to shut-down once their stdin/stdout are dropped, so nothing else /// needs to be done to clean them up after drop. running: HashMap, + + /// The context to pass to spawned filter programs. + pub context: gix_command::Context, +} + +/// Initialization +impl State { + /// Create a new instance using `context` to inform launched processes about their environment. + pub fn new(context: gix_command::Context) -> Self { + Self { + running: Default::default(), + context, + } + } } impl Clone for State { fn clone(&self) -> Self { State { running: Default::default(), + context: self.context.clone(), } } } diff --git a/gix-filter/src/pipeline/mod.rs b/gix-filter/src/pipeline/mod.rs index 0193a45200c..c5efbb5a019 100644 --- a/gix-filter/src/pipeline/mod.rs +++ b/gix-filter/src/pipeline/mod.rs @@ -52,18 +52,22 @@ const ATTRS: [&str; 6] = ["crlf", "ident", "filter", "eol", "text", "working-tre /// Lifecycle impl Pipeline { - /// Create a new pipeline with configured `drivers` (which should be considered safe to invoke) as well as a way to initialize - /// our attributes with `collection`. + /// Create a new pipeline with configured `drivers` (which should be considered safe to invoke) with `context` as well as + /// a way to initialize our attributes with `collection`. /// `eol_config` serves as fallback to understand how to convert line endings if no line-ending attributes are present. /// `crlf_roundtrip_check` corresponds to the git-configuration of `core.safecrlf`. /// `object_hash` is relevant for the `ident` filter. - pub fn new(collection: &gix_attributes::search::MetadataCollection, options: Options) -> Self { + pub fn new( + collection: &gix_attributes::search::MetadataCollection, + context: gix_command::Context, + options: Options, + ) -> Self { let mut attrs = gix_attributes::search::Outcome::default(); attrs.initialize_with_selection(collection, ATTRS); Pipeline { attrs, context: Context::default(), - processes: driver::State::default(), + processes: driver::State::new(context), options, bufs: Default::default(), } @@ -80,7 +84,7 @@ impl Pipeline { impl Default for Pipeline { fn default() -> Self { let collection = Default::default(); - Pipeline::new(&collection, Default::default()) + Pipeline::new(&collection, Default::default(), Default::default()) } } diff --git a/gix-filter/tests/pipeline/mod.rs b/gix-filter/tests/pipeline/mod.rs index 1a10043da9b..ad84b7fb775 100644 --- a/gix-filter/tests/pipeline/mod.rs +++ b/gix-filter/tests/pipeline/mod.rs @@ -59,6 +59,7 @@ fn pipeline( let (drivers, encodings_with_roundtrip_check, crlf_roundtrip_check, eol_config) = init(); let pipe = gix_filter::Pipeline::new( cache.attributes_collection(), + Default::default(), gix_filter::pipeline::Options { drivers, eol_config, From 3b71ca571e2469a10f64e1730c2de26ea88dc294 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 19:43:28 +0100 Subject: [PATCH 4/6] adapt to changes in `gix-filter` --- gix-archive/tests/archive.rs | 2 +- gix-worktree-stream/tests/stream.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gix-archive/tests/archive.rs b/gix-archive/tests/archive.rs index b9bbcaaffda..95c8e09359e 100644 --- a/gix-archive/tests/archive.rs +++ b/gix-archive/tests/archive.rs @@ -303,6 +303,6 @@ mod from_tree { } fn noop_pipeline() -> gix_filter::Pipeline { - gix_filter::Pipeline::new(&Default::default(), Default::default()) + gix_filter::Pipeline::new(&Default::default(), Default::default(), Default::default()) } } diff --git a/gix-worktree-stream/tests/stream.rs b/gix-worktree-stream/tests/stream.rs index 5d4366c728b..9d341ca15ad 100644 --- a/gix-worktree-stream/tests/stream.rs +++ b/gix-worktree-stream/tests/stream.rs @@ -262,6 +262,7 @@ mod from_tree { fn mutating_pipeline(driver: bool) -> gix_filter::Pipeline { gix_filter::Pipeline::new( &Default::default(), + Default::default(), gix_filter::pipeline::Options { drivers: if driver { vec![driver_with_process()] } else { vec![] }, eol_config: gix_filter::eol::Configuration { From 0b3eb141bee59ffc17c973a8d126efaa52edb9b3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 11:03:12 +0100 Subject: [PATCH 5/6] fix: assure the correct repository is used for checkouts after clone. (#1129) If this is not the case, it's possible for filters to run in the context of potential parent repositories, which then can have all kinds of issues. In case of `git-lfs`, for instance, it would try to download objects from the wrong repository. --- Cargo.lock | 1 + gix/Cargo.toml | 3 ++- gix/src/config/cache/access.rs | 6 ++++- gix/src/config/mod.rs | 38 ++++++++++++++++++++++++++++++++ gix/src/filter.rs | 8 ++++++- gix/src/open/repository.rs | 8 ++----- gix/src/repository/config/mod.rs | 20 +++++++++++++++++ gix/src/repository/mod.rs | 2 ++ gix/src/repository/worktree.rs | 7 ++++-- 9 files changed, 82 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 419568e6f64..c8bc6add481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1223,6 +1223,7 @@ dependencies = [ "gix-actor 0.28.0", "gix-archive", "gix-attributes 0.20.0", + "gix-command", "gix-commitgraph", "gix-config", "gix-credentials", diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 6c3c19715b6..5f566b71a9e 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -81,7 +81,7 @@ worktree-mutation = ["attributes", "dep:gix-worktree-state"] excludes = ["dep:gix-ignore", "dep:gix-worktree", "index"] ## Query attributes and excludes. Enables access to pathspecs, worktree checkouts, filter-pipelines and submodules. -attributes = ["excludes", "dep:gix-filter", "dep:gix-pathspec", "dep:gix-attributes", "dep:gix-submodule", "gix-worktree?/attributes"] +attributes = ["excludes", "dep:gix-filter", "dep:gix-pathspec", "dep:gix-attributes", "dep:gix-submodule", "gix-worktree?/attributes", "dep:gix-command"] ## Add support for mailmaps, as way of determining the final name of commmiters and authors. mailmap = ["dep:gix-mailmap"] @@ -254,6 +254,7 @@ gix-commitgraph = { version = "^0.22.0", path = "../gix-commitgraph" } gix-pathspec = { version = "^0.4.0", path = "../gix-pathspec", optional = true } gix-submodule = { version = "^0.5.0", path = "../gix-submodule", optional = true } gix-status = { version = "^0.2.0", path = "../gix-status", optional = true } +gix-command = { version = "^0.2.10", path = "../gix-command", optional = true } gix-worktree-stream = { version = "^0.6.0", path = "../gix-worktree-stream", optional = true } gix-archive = { version = "^0.6.0", path = "../gix-archive", default-features = false, optional = true } diff --git a/gix/src/config/cache/access.rs b/gix/src/config/cache/access.rs index ec3e7e1b424..5031501d3f1 100644 --- a/gix/src/config/cache/access.rs +++ b/gix/src/config/cache/access.rs @@ -197,7 +197,11 @@ impl Cache { let capabilities = self.fs_capabilities()?; let filters = { let collection = Default::default(); - let mut filters = gix_filter::Pipeline::new(&collection, crate::filter::Pipeline::options(repo)?); + let mut filters = gix_filter::Pipeline::new( + &collection, + repo.command_context()?, + crate::filter::Pipeline::options(repo)?, + ); if let Ok(mut head) = repo.head() { let ctx = filters.driver_context_mut(); ctx.ref_name = head.referent_name().map(|name| name.as_bstr().to_owned()); diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index f48575c174f..4324734bef7 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -152,6 +152,25 @@ pub mod checkout_options { Attributes(#[from] super::attribute_stack::Error), #[error(transparent)] FilterPipelineOptions(#[from] crate::filter::pipeline::options::Error), + #[error(transparent)] + CommandContext(#[from] crate::config::command_context::Error), + } +} + +/// +#[cfg(feature = "attributes")] +pub mod command_context { + use crate::config; + + /// The error produced when collecting all information relevant to spawned commands, + /// obtained via [Repository::command_context()](crate::Repository::command_context()). + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + PathSpec(#[from] gix_pathspec::defaults::from_environment::Error), + #[error(transparent)] + Boolean(#[from] config::boolean::Error), } } @@ -544,3 +563,22 @@ pub(crate) struct Cache { environment: crate::open::permissions::Environment, // TODO: make core.precomposeUnicode available as well. } + +/// Utillities shared privately across the crate, for lack of a better place. +pub(crate) mod shared { + use crate::config; + use crate::config::cache::util::ApplyLeniency; + use crate::config::tree::Core; + + pub fn is_replace_refs_enabled( + config: &gix_config::File<'static>, + lenient: bool, + mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, + ) -> Result, config::boolean::Error> { + config + .boolean_filter_by_key("core.useReplaceRefs", &mut filter_config_section) + .map(|b| Core::USE_REPLACE_REFS.enrich_error(b)) + .transpose() + .with_leniency(lenient) + } +} diff --git a/gix/src/filter.rs b/gix/src/filter.rs index c3fda49eb23..d18fc06fa57 100644 --- a/gix/src/filter.rs +++ b/gix/src/filter.rs @@ -32,6 +32,8 @@ pub mod pipeline { name: BString, source: gix_config::value::Error, }, + #[error(transparent)] + CommandContext(#[from] config::command_context::Error), } } @@ -111,7 +113,11 @@ impl<'repo> Pipeline<'repo> { /// Create a new instance by extracting all necessary information and configuration from a `repo` along with `cache` for accessing /// attributes. The `index` is used for some filters which may access it under very specific circumstances. pub fn new(repo: &'repo Repository, cache: gix_worktree::Stack) -> Result { - let pipeline = gix_filter::Pipeline::new(cache.attributes_collection(), Self::options(repo)?); + let pipeline = gix_filter::Pipeline::new( + cache.attributes_collection(), + repo.command_context()?, + Self::options(repo)?, + ); Ok(Pipeline { inner: pipeline, cache, diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index a9150a1ee54..e1cffddc85d 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -8,7 +8,7 @@ use super::{Error, Options}; use crate::{ config, config::{ - cache::{interpolate_context, util::ApplyLeniency}, + cache::interpolate_context, tree::{gitoxide, Core, Key, Safe}, }, open::Permissions, @@ -324,11 +324,7 @@ fn replacement_objects_refs_prefix( lenient: bool, mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, ) -> Result, Error> { - let is_disabled = config - .boolean_filter_by_key("core.useReplaceRefs", &mut filter_config_section) - .map(|b| Core::USE_REPLACE_REFS.enrich_error(b)) - .transpose() - .with_leniency(lenient) + let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section) .map_err(config::Error::ConfigBoolean)? .unwrap_or(true); diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index 618ccf0f63b..00888a33e0a 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -79,6 +79,26 @@ impl crate::Repository { Ok(opts) } + /// Return the context to be passed to any spawned program that is supposed to interact with the repository, like + /// hooks or filters. + #[cfg(feature = "attributes")] + pub fn command_context(&self) -> Result { + Ok(gix_command::Context { + git_dir: self.git_dir().to_owned().into(), + worktree_dir: self.work_dir().map(ToOwned::to_owned), + no_replace_objects: config::shared::is_replace_refs_enabled( + &self.config.resolved, + self.config.lenient_config, + self.filter_config_section(), + )? + .map(|enabled| !enabled), + ref_namespace: None, + literal_pathspecs: None, + glob_pathspecs: None, + icase_pathspecs: None, + }) + } + /// The kind of object hash the repository is configured to use. pub fn object_hash(&self) -> gix_hash::Kind { self.config.object_hash diff --git a/gix/src/repository/mod.rs b/gix/src/repository/mod.rs index e3742894b37..6b9e284cc05 100644 --- a/gix/src/repository/mod.rs +++ b/gix/src/repository/mod.rs @@ -124,6 +124,8 @@ pub mod worktree_stream { AttributesCache(#[from] crate::config::attribute_stack::Error), #[error(transparent)] FilterPipeline(#[from] crate::filter::pipeline::options::Error), + #[error(transparent)] + CommandContext(#[from] crate::config::command_context::Error), #[error("Needed {id} to be a tree to turn into a workspace stream, got {actual}")] NotATree { id: gix_hash::ObjectId, diff --git a/gix/src/repository/worktree.rs b/gix/src/repository/worktree.rs index 9e6edeea751..198c1eb5e6e 100644 --- a/gix/src/repository/worktree.rs +++ b/gix/src/repository/worktree.rs @@ -79,8 +79,11 @@ impl crate::Repository { let mut cache = self .attributes_only(&index, gix_worktree::stack::state::attributes::Source::IdMapping)? .detach(); - let pipeline = - gix_filter::Pipeline::new(cache.attributes_collection(), crate::filter::Pipeline::options(self)?); + let pipeline = gix_filter::Pipeline::new( + cache.attributes_collection(), + self.command_context()?, + crate::filter::Pipeline::options(self)?, + ); let objects = self.objects.clone().into_arc().expect("TBD error handling"); let stream = gix_worktree_stream::from_tree( id, From 8434aab5fb6ce32be2bf3b20e38c28c780bd5db9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 25 Nov 2023 19:51:22 +0100 Subject: [PATCH 6/6] feat: add `gitoxide.core.refsNamespace` key and respect the `GIT_NAMESPACE` environment variable. It's also provided as context value. --- gix/src/config/cache/init.rs | 19 ++++++++++++---- gix/src/config/cache/util.rs | 12 ++++++++++ gix/src/config/mod.rs | 14 ++++++++++-- gix/src/config/tree/sections/gitoxide.rs | 28 ++++++++++++++++++++++++ gix/src/open/repository.rs | 1 + gix/src/repository/config/mod.rs | 20 +++++++++++++---- gix/tests/gix-init.rs | 15 ++++++++++++- 7 files changed, 98 insertions(+), 11 deletions(-) diff --git a/gix/src/config/cache/init.rs b/gix/src/config/cache/init.rs index b39c8f22b5a..7f6bc0ab3fc 100644 --- a/gix/src/config/cache/init.rs +++ b/gix/src/config/cache/init.rs @@ -144,6 +144,7 @@ impl Cache { use util::config_bool; let reflog = util::query_refupdates(&config, lenient_config)?; + let refs_namespace = util::query_refs_namespace(&config, lenient_config)?; let ignore_case = config_bool(&config, &Core::IGNORE_CASE, "core.ignoreCase", false, lenient_config)?; let use_multi_pack_index = config_bool( &config, @@ -167,6 +168,7 @@ impl Cache { pack_cache_bytes, object_cache_bytes, reflog, + refs_namespace, is_bare, ignore_case, hex_len, @@ -223,10 +225,12 @@ impl Cache { self.object_kind_hint = object_kind_hint; } let reflog = util::query_refupdates(config, self.lenient_config)?; + let refs_namespace = util::query_refs_namespace(config, self.lenient_config)?; self.hex_len = hex_len; self.ignore_case = ignore_case; self.reflog = reflog; + self.refs_namespace = refs_namespace; self.user_agent = Default::default(); self.personas = Default::default(); @@ -299,6 +303,7 @@ impl crate::Repository { fn apply_changed_values(&mut self) { self.refs.write_reflog = util::reflog_or_default(self.config.reflog, self.work_dir().is_some()); + self.refs.namespace = self.config.refs_namespace.clone(); } } @@ -413,10 +418,16 @@ fn apply_environment_overrides( "gitoxide", Some(Cow::Borrowed("core".into())), git_prefix, - &[{ - let key = &gitoxide::Core::SHALLOW_FILE; - (env(key), key.name) - }], + &[ + { + let key = &gitoxide::Core::SHALLOW_FILE; + (env(key), key.name) + }, + { + let key = &gitoxide::Core::REFS_NAMESPACE; + (env(key), key.name) + }, + ], ), ( "gitoxide", diff --git a/gix/src/config/cache/util.rs b/gix/src/config/cache/util.rs index 4032b2cb177..4c1d6c69399 100644 --- a/gix/src/config/cache/util.rs +++ b/gix/src/config/cache/util.rs @@ -55,6 +55,18 @@ pub(crate) fn query_refupdates( .map_err(Into::into) } +pub(crate) fn query_refs_namespace( + config: &gix_config::File<'static>, + lenient_config: bool, +) -> Result, config::refs_namespace::Error> { + let key = "gitoxide.core.refsNamespace"; + config + .string_by_key(key) + .map(|ns| gitoxide::Core::REFS_NAMESPACE.try_into_refs_namespace(ns)) + .transpose() + .with_leniency(lenient_config) +} + pub(crate) fn reflog_or_default( config_reflog: Option, has_worktree: bool, diff --git a/gix/src/config/mod.rs b/gix/src/config/mod.rs index 4324734bef7..33c97fac687 100644 --- a/gix/src/config/mod.rs +++ b/gix/src/config/mod.rs @@ -76,6 +76,8 @@ pub enum Error { ConfigUnsigned(#[from] unsigned_integer::Error), #[error(transparent)] ConfigTypedString(#[from] key::GenericErrorWithValue), + #[error(transparent)] + RefsNamespace(#[from] refs_namespace::Error), #[error("Cannot handle objects formatted as {:?}", .name)] UnsupportedObjectFormat { name: BString }, #[error(transparent)] @@ -167,10 +169,10 @@ pub mod command_context { #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - PathSpec(#[from] gix_pathspec::defaults::from_environment::Error), #[error(transparent)] Boolean(#[from] config::boolean::Error), + #[error(transparent)] + ParseBool(#[from] gix_config::value::Error), } } @@ -430,6 +432,12 @@ pub mod refspec { pub type Error = super::key::Error; } +/// +pub mod refs_namespace { + /// The error produced when failing to parse a refspec from the configuration. + pub type Error = super::key::Error; +} + /// pub mod ssl_version { /// The error produced when failing to parse a refspec from the configuration. @@ -526,6 +534,8 @@ pub(crate) struct Cache { pub use_multi_pack_index: bool, /// The representation of `core.logallrefupdates`, or `None` if the variable wasn't set. pub reflog: Option, + /// The representation of `gitoxide.core.refsNamespace`, or `None` if the variable wasn't set. + pub refs_namespace: Option, /// The configured user agent for presentation to servers. pub(crate) user_agent: OnceCell, /// identities for later use, lazy initialization. diff --git a/gix/src/config/tree/sections/gitoxide.rs b/gix/src/config/tree/sections/gitoxide.rs index 648b35ffec5..f1eb7a23bd6 100644 --- a/gix/src/config/tree/sections/gitoxide.rs +++ b/gix/src/config/tree/sections/gitoxide.rs @@ -74,6 +74,20 @@ mod subsections { #[derive(Copy, Clone, Default)] pub struct Core; + /// The `gitoxide.allow.protocolFromUser` key. + pub type RefsNamespace = keys::Any; + + impl RefsNamespace { + /// Derive the negotiation algorithm identified by `name`, case-sensitively. + pub fn try_into_refs_namespace( + &'static self, + name: std::borrow::Cow<'_, crate::bstr::BStr>, + ) -> Result { + gix_ref::namespace::expand(name.as_ref()) + .map_err(|err| crate::config::key::Error::from_value(self, name.into_owned()).with_source(err)) + } + } + impl Core { /// The `gitoxide.core.defaultPackCacheMemoryLimit` key. pub const DEFAULT_PACK_CACHE_MEMORY_LIMIT: keys::UnsignedInteger = @@ -100,6 +114,11 @@ mod subsections { /// It controls whether or not long running filter driver processes can use the 'delay' capability. pub const FILTER_PROCESS_DELAY: keys::Boolean = keys::Boolean::new_boolean("filterProcessDelay", &Gitoxide::CORE); + + /// The `gitoxide.core.refsNamespace` key. + pub const REFS_NAMESPACE: RefsNamespace = + keys::Any::new_with_validate("refsNamespace", &Gitoxide::CORE, super::validate::RefsNamespace) + .with_environment_override("GIT_NAMESPACE"); } impl Section for Core { @@ -114,6 +133,7 @@ mod subsections { &Self::USE_STDEV, &Self::SHALLOW_FILE, &Self::FILTER_PROCESS_DELAY, + &Self::REFS_NAMESPACE, ] } @@ -496,4 +516,12 @@ pub mod validate { Ok(()) } } + + pub struct RefsNamespace; + impl Validate for RefsNamespace { + fn validate(&self, value: &BStr) -> Result<(), Box> { + super::Core::REFS_NAMESPACE.try_into_refs_namespace(value.into())?; + Ok(()) + } + } } diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index e1cffddc85d..6c3f07f4280 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -273,6 +273,7 @@ impl ThreadSafeRepository { } refs.write_reflog = config::cache::util::reflog_or_default(config.reflog, worktree_dir.is_some()); + refs.namespace = config.refs_namespace.clone(); let replacements = replacement_objects_refs_prefix(&config.resolved, lenient_config, filter_config_section)? .and_then(|prefix| { let _span = gix_trace::detail!("find replacement objects"); diff --git a/gix/src/repository/config/mod.rs b/gix/src/repository/config/mod.rs index 00888a33e0a..197544bdcc3 100644 --- a/gix/src/repository/config/mod.rs +++ b/gix/src/repository/config/mod.rs @@ -83,6 +83,18 @@ impl crate::Repository { /// hooks or filters. #[cfg(feature = "attributes")] pub fn command_context(&self) -> Result { + use crate::config::cache::util::ApplyLeniency; + use crate::config::tree::gitoxide; + use crate::config::tree::Key; + + let boolean = |key: &dyn Key| { + self.config + .resolved + .boolean("gitoxide", Some("pathspec".into()), key.name()) + .transpose() + .with_leniency(self.config.lenient_config) + }; + Ok(gix_command::Context { git_dir: self.git_dir().to_owned().into(), worktree_dir: self.work_dir().map(ToOwned::to_owned), @@ -92,10 +104,10 @@ impl crate::Repository { self.filter_config_section(), )? .map(|enabled| !enabled), - ref_namespace: None, - literal_pathspecs: None, - glob_pathspecs: None, - icase_pathspecs: None, + ref_namespace: self.refs.namespace.as_ref().map(|ns| ns.as_bstr().to_owned()), + literal_pathspecs: boolean(&gitoxide::Pathspec::LITERAL)?, + glob_pathspecs: boolean(&gitoxide::Pathspec::GLOB)?.or(boolean(&gitoxide::Pathspec::NOGLOB)?), + icase_pathspecs: boolean(&gitoxide::Pathspec::ICASE)?, }) } diff --git a/gix/tests/gix-init.rs b/gix/tests/gix-init.rs index 3f15bb9022b..531f89e9297 100644 --- a/gix/tests/gix-init.rs +++ b/gix/tests/gix-init.rs @@ -49,7 +49,8 @@ mod with_overrides { .set("GIT_NOGLOB_PATHSPECS", "pathspecs-noglob") .set("GIT_ICASE_PATHSPECS", "pathspecs-icase") .set("GIT_TERMINAL_PROMPT", "42") - .set("GIT_SHALLOW_FILE", "shallow-file-env"); + .set("GIT_SHALLOW_FILE", "shallow-file-env") + .set("GIT_NAMESPACE", "namespace-env"); let mut opts = gix::open::Options::isolated() .cli_overrides([ "http.userAgent=agent-from-cli", @@ -62,6 +63,7 @@ mod with_overrides { "gitoxide.ssh.commandWithoutShellFallback=ssh-command-fallback-cli", "gitoxide.http.proxyAuthMethod=proxy-auth-method-cli", "gitoxide.core.shallowFile=shallow-file-cli", + "gitoxide.core.refsNamespace=namespace-cli", ]) .config_overrides([ "http.userAgent=agent-from-api", @@ -74,6 +76,7 @@ mod with_overrides { "gitoxide.ssh.commandWithoutShellFallback=ssh-command-fallback-api", "gitoxide.http.proxyAuthMethod=proxy-auth-method-api", "gitoxide.core.shallowFile=shallow-file-api", + "gitoxide.core.refsNamespace=namespace-api", ]); opts.permissions.env.git_prefix = Permission::Allow; opts.permissions.env.http_transport = Permission::Allow; @@ -96,6 +99,16 @@ mod with_overrides { cow_bstr("shallow-file-env") ] ); + assert_eq!( + config + .strings_by_key("gitoxide.core.refsNamespace") + .expect("at least one value"), + [ + cow_bstr("namespace-cli"), + cow_bstr("namespace-api"), + cow_bstr("namespace-env") + ] + ); assert_eq!( config.strings_by_key("http.userAgent").expect("at least one value"), [