Skip to content

Commit f09ea13

Browse files
committed
Merge branch 'improve-filters'
2 parents f528ae8 + 8434aab commit f09ea13

File tree

24 files changed

+402
-28
lines changed

24 files changed

+402
-28
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-archive/tests/archive.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,6 @@ mod from_tree {
303303
}
304304

305305
fn noop_pipeline() -> gix_filter::Pipeline {
306-
gix_filter::Pipeline::new(&Default::default(), Default::default())
306+
gix_filter::Pipeline::new(&Default::default(), Default::default(), Default::default())
307307
}
308308
}

gix-command/src/lib.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
#![deny(rust_2018_idioms, missing_docs)]
33
#![forbid(unsafe_code)]
44

5+
use bstr::BString;
56
use std::ffi::OsString;
7+
use std::path::PathBuf;
68

79
/// A structure to keep settings to use when invoking a command via [`spawn()`][Prepare::spawn()], after creating it with [`prepare()`].
810
pub struct Prepare {
911
/// The command to invoke (either with or without shell depending on `use_shell`.
1012
pub command: OsString,
13+
/// Additional information to be passed to the spawned command.
14+
pub context: Option<Context>,
1115
/// The way standard input is configured.
1216
pub stdin: std::process::Stdio,
1317
/// The way standard output is configured.
@@ -35,6 +39,37 @@ pub struct Prepare {
3539
pub allow_manual_arg_splitting: bool,
3640
}
3741

42+
/// Additional information that is relevant to spawned processes, which typically receive
43+
/// a wealth of contextual information when spawned from `git`.
44+
///
45+
/// See [the git source code](https://github.com/git/git/blob/cfb8a6e9a93adbe81efca66e6110c9b4d2e57169/git.c#L191)
46+
/// for details.
47+
#[derive(Debug, Default, Clone)]
48+
pub struct Context {
49+
/// The `.git` directory that contains the repository.
50+
///
51+
/// If set, it will be used to set the the `GIT_DIR` environment variable.
52+
pub git_dir: Option<PathBuf>,
53+
/// Set the `GIT_WORK_TREE` environment variable with the given path.
54+
pub worktree_dir: Option<PathBuf>,
55+
/// If `true`, set `GIT_NO_REPLACE_OBJECTS` to `1`, which turns off object replacements, or `0` otherwise.
56+
/// If `None`, the variable won't be set.
57+
pub no_replace_objects: Option<bool>,
58+
/// Set the `GIT_NAMESPACE` variable with the given value, effectively namespacing all
59+
/// operations on references.
60+
pub ref_namespace: Option<BString>,
61+
/// If `true`, set `GIT_LITERAL_PATHSPECS` to `1`, which makes globs literal and prefixes as well, or `0` otherwise.
62+
/// If `None`, the variable won't be set.
63+
pub literal_pathspecs: Option<bool>,
64+
/// If `true`, set `GIT_GLOB_PATHSPECS` to `1`, which lets wildcards not match the `/` character, and equals the `:(glob)` prefix.
65+
/// If `false`, set `GIT_NOGLOB_PATHSPECS` to `1` which lets globs match only themselves.
66+
/// If `None`, the variable won't be set.
67+
pub glob_pathspecs: Option<bool>,
68+
/// If `true`, set `GIT_ICASE_PATHSPECS` to `1`, to let patterns match case-insensitively, or `0` otherwise.
69+
/// If `None`, the variable won't be set.
70+
pub icase_pathspecs: Option<bool>,
71+
}
72+
3873
mod prepare {
3974
use std::{
4075
ffi::OsString,
@@ -43,7 +78,7 @@ mod prepare {
4378

4479
use bstr::ByteSlice;
4580

46-
use crate::Prepare;
81+
use crate::{Context, Prepare};
4782

4883
/// Builder
4984
impl Prepare {
@@ -67,6 +102,15 @@ mod prepare {
67102
self
68103
}
69104

105+
/// Set additional `ctx` to be used when spawning the process.
106+
///
107+
/// Note that this is a must for most kind of commands that `git` usually spawns,
108+
/// as at least they need to know the correct `git` repository to function.
109+
pub fn with_context(mut self, ctx: Context) -> Self {
110+
self.context = Some(ctx);
111+
self
112+
}
113+
70114
/// Use a shell, but try to split arguments by hand if this be safely done without a shell.
71115
///
72116
/// If that's not the case, use a shell instead.
@@ -164,6 +208,36 @@ mod prepare {
164208
.stderr(prep.stderr)
165209
.envs(prep.env)
166210
.args(prep.args);
211+
if let Some(ctx) = prep.context {
212+
if let Some(git_dir) = ctx.git_dir {
213+
cmd.env("GIT_DIR", &git_dir);
214+
}
215+
if let Some(worktree_dir) = ctx.worktree_dir {
216+
cmd.env("GIT_WORK_TREE", worktree_dir);
217+
}
218+
if let Some(value) = ctx.no_replace_objects {
219+
cmd.env("GIT_NO_REPLACE_OBJECTS", usize::from(value).to_string());
220+
}
221+
if let Some(namespace) = ctx.ref_namespace {
222+
cmd.env("GIT_NAMESPACE", gix_path::from_bstring(namespace));
223+
}
224+
if let Some(value) = ctx.literal_pathspecs {
225+
cmd.env("GIT_LITERAL_PATHSPECS", usize::from(value).to_string());
226+
}
227+
if let Some(value) = ctx.glob_pathspecs {
228+
cmd.env(
229+
if value {
230+
"GIT_GLOB_PATHSPECS"
231+
} else {
232+
"GIT_NOGLOB_PATHSPECS"
233+
},
234+
"1",
235+
);
236+
}
237+
if let Some(value) = ctx.icase_pathspecs {
238+
cmd.env("GIT_ICASE_PATHSPECS", usize::from(value).to_string());
239+
}
240+
}
167241
cmd
168242
}
169243
}
@@ -176,9 +250,16 @@ mod prepare {
176250
/// - `stdin` is null to prevent blocking unexpectedly on consumption of stdin
177251
/// - `stdout` is captured for consumption by the caller
178252
/// - `stderr` is inherited to allow the command to provide context to the user
253+
///
254+
/// ### Warning
255+
///
256+
/// When using this method, be sure that the invoked program doesn't rely on the current working dir and/or
257+
/// environment variables to know its context. If so, call instead [`Prepare::with_context()`] to provide
258+
/// additional information.
179259
pub fn prepare(cmd: impl Into<OsString>) -> Prepare {
180260
Prepare {
181261
command: cmd.into(),
262+
context: None,
182263
stdin: std::process::Stdio::null(),
183264
stdout: std::process::Stdio::piped(),
184265
stderr: std::process::Stdio::inherit(),

gix-command/tests/command.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,111 @@
11
use gix_testtools::Result;
22

3+
mod context {
4+
use gix_command::Context;
5+
6+
fn winfix(expected: impl Into<String>) -> String {
7+
// Unclear why it's not debug-printing the env on windows.
8+
if cfg!(windows) {
9+
"\"\"".into()
10+
} else {
11+
expected.into()
12+
}
13+
}
14+
15+
#[test]
16+
fn git_dir_sets_git_dir_env_and_cwd() {
17+
let ctx = Context {
18+
git_dir: Some(".".into()),
19+
..Default::default()
20+
};
21+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
22+
assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_DIR="." """#));
23+
}
24+
25+
#[test]
26+
fn worktree_dir_sets_env_only() {
27+
let ctx = Context {
28+
worktree_dir: Some(".".into()),
29+
..Default::default()
30+
};
31+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
32+
assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_WORK_TREE="." """#));
33+
}
34+
35+
#[test]
36+
fn no_replace_objects_sets_env_only() {
37+
for value in [false, true] {
38+
let expected = usize::from(value);
39+
let ctx = Context {
40+
no_replace_objects: Some(value),
41+
..Default::default()
42+
};
43+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
44+
assert_eq!(
45+
format!("{cmd:?}"),
46+
winfix(format!(r#"GIT_NO_REPLACE_OBJECTS="{expected}" """#))
47+
);
48+
}
49+
}
50+
51+
#[test]
52+
fn ref_namespace_sets_env_only() {
53+
let ctx = Context {
54+
ref_namespace: Some("namespace".into()),
55+
..Default::default()
56+
};
57+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
58+
assert_eq!(format!("{cmd:?}"), winfix(r#"GIT_NAMESPACE="namespace" """#));
59+
}
60+
61+
#[test]
62+
fn literal_pathspecs_sets_env_only() {
63+
for value in [false, true] {
64+
let expected = usize::from(value);
65+
let ctx = Context {
66+
literal_pathspecs: Some(value),
67+
..Default::default()
68+
};
69+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
70+
assert_eq!(
71+
format!("{cmd:?}"),
72+
winfix(format!(r#"GIT_LITERAL_PATHSPECS="{expected}" """#))
73+
);
74+
}
75+
}
76+
77+
#[test]
78+
fn glob_pathspecs_sets_env_only() {
79+
for (value, expected) in [
80+
(false, "GIT_NOGLOB_PATHSPECS=\"1\""),
81+
(true, "GIT_GLOB_PATHSPECS=\"1\""),
82+
] {
83+
let ctx = Context {
84+
glob_pathspecs: Some(value),
85+
..Default::default()
86+
};
87+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
88+
assert_eq!(format!("{cmd:?}"), winfix(format!(r#"{expected} """#)));
89+
}
90+
}
91+
92+
#[test]
93+
fn icase_pathspecs_sets_env_only() {
94+
for value in [false, true] {
95+
let expected = usize::from(value);
96+
let ctx = Context {
97+
icase_pathspecs: Some(value),
98+
..Default::default()
99+
};
100+
let cmd = std::process::Command::from(gix_command::prepare("").with_context(ctx));
101+
assert_eq!(
102+
format!("{cmd:?}"),
103+
winfix(format!(r#"GIT_ICASE_PATHSPECS="{expected}" """#))
104+
);
105+
}
106+
}
107+
}
108+
3109
mod prepare {
4110
#[cfg(windows)]
5111
const SH: &str = "sh";

gix-filter/src/driver/apply.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub struct Context<'a, 'b> {
6363
pub blob: Option<gix_hash::ObjectId>,
6464
}
6565

66+
/// Apply operations to filter programs.
6667
impl State {
6768
/// Apply `operation` of `driver` to the bytes read from `src` and return a reader to immediately consume the output
6869
/// produced by the filter. `rela_path` is the repo-relative path of the entry to handle.

gix-filter/src/driver/delayed.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ pub mod fetch {
4545
}
4646
}
4747

48+
/// Operations related to delayed filtering.
4849
impl State {
4950
/// Return a list of delayed paths for `process` that can then be obtained with [`fetch_delayed()`][Self::fetch_delayed()].
5051
///

gix-filter/src/driver/init.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub enum Error {
2424
},
2525
}
2626

27+
/// Lifecycle
2728
impl State {
2829
/// Obtain a process as defined in `driver` suitable for a given `operation. `rela_path` may be used to substitute the current
2930
/// file for use in the invoked `SingleFile` process.
@@ -40,7 +41,7 @@ impl State {
4041
let client = match self.running.remove(process) {
4142
Some(c) => c,
4243
None => {
43-
let (child, cmd) = spawn_driver(process.clone())?;
44+
let (child, cmd) = spawn_driver(process.clone(), &self.context)?;
4445
process::Client::handshake(child, "git-filter", &[2], &["clean", "smudge", "delay"]).map_err(
4546
|err| Error::ProcessHandshake {
4647
source: err,
@@ -79,20 +80,25 @@ impl State {
7980
None => return Ok(None),
8081
};
8182

82-
let (child, command) = spawn_driver(cmd)?;
83+
let (child, command) = spawn_driver(cmd, &self.context)?;
8384
Ok(Some(Process::SingleFile { child, command }))
8485
}
8586
}
8687
}
8788
}
8889

89-
fn spawn_driver(cmd: BString) -> Result<(std::process::Child, std::process::Command), Error> {
90+
fn spawn_driver(
91+
cmd: BString,
92+
context: &gix_command::Context,
93+
) -> Result<(std::process::Child, std::process::Command), Error> {
9094
let mut cmd: std::process::Command = gix_command::prepare(gix_path::from_bstr(cmd).into_owned())
9195
.with_shell()
96+
.with_context(context.clone())
9297
.stdin(Stdio::piped())
9398
.stdout(Stdio::piped())
9499
.stderr(Stdio::inherit())
95100
.into();
101+
gix_trace::debug!(cmd = ?cmd, "launching filter driver");
96102
let child = match cmd.spawn() {
97103
Ok(child) => child,
98104
Err(err) => {

gix-filter/src/driver/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,27 @@ pub struct State {
7070
/// Note that these processes are expected to shut-down once their stdin/stdout are dropped, so nothing else
7171
/// needs to be done to clean them up after drop.
7272
running: HashMap<BString, process::Client>,
73+
74+
/// The context to pass to spawned filter programs.
75+
pub context: gix_command::Context,
76+
}
77+
78+
/// Initialization
79+
impl State {
80+
/// Create a new instance using `context` to inform launched processes about their environment.
81+
pub fn new(context: gix_command::Context) -> Self {
82+
Self {
83+
running: Default::default(),
84+
context,
85+
}
86+
}
7387
}
7488

7589
impl Clone for State {
7690
fn clone(&self) -> Self {
7791
State {
7892
running: Default::default(),
93+
context: self.context.clone(),
7994
}
8095
}
8196
}

gix-filter/src/driver/process/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,8 @@ impl Client {
185185
self.send_command_and_meta(command, meta)?;
186186
while let Some(data) = self.out.read_line() {
187187
let line = data??;
188-
if let Some(line) = line.as_bstr() {
189-
inspect_line(line);
188+
if let Some(line) = line.as_text() {
189+
inspect_line(line.as_bstr());
190190
}
191191
}
192192
self.out.reset_with(&[gix_packetline::PacketLineRef::Flush]);

gix-filter/src/pipeline/mod.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,22 @@ const ATTRS: [&str; 6] = ["crlf", "ident", "filter", "eol", "text", "working-tre
5252

5353
/// Lifecycle
5454
impl Pipeline {
55-
/// Create a new pipeline with configured `drivers` (which should be considered safe to invoke) as well as a way to initialize
56-
/// our attributes with `collection`.
55+
/// Create a new pipeline with configured `drivers` (which should be considered safe to invoke) with `context` as well as
56+
/// a way to initialize our attributes with `collection`.
5757
/// `eol_config` serves as fallback to understand how to convert line endings if no line-ending attributes are present.
5858
/// `crlf_roundtrip_check` corresponds to the git-configuration of `core.safecrlf`.
5959
/// `object_hash` is relevant for the `ident` filter.
60-
pub fn new(collection: &gix_attributes::search::MetadataCollection, options: Options) -> Self {
60+
pub fn new(
61+
collection: &gix_attributes::search::MetadataCollection,
62+
context: gix_command::Context,
63+
options: Options,
64+
) -> Self {
6165
let mut attrs = gix_attributes::search::Outcome::default();
6266
attrs.initialize_with_selection(collection, ATTRS);
6367
Pipeline {
6468
attrs,
6569
context: Context::default(),
66-
processes: driver::State::default(),
70+
processes: driver::State::new(context),
6771
options,
6872
bufs: Default::default(),
6973
}
@@ -80,7 +84,7 @@ impl Pipeline {
8084
impl Default for Pipeline {
8185
fn default() -> Self {
8286
let collection = Default::default();
83-
Pipeline::new(&collection, Default::default())
87+
Pipeline::new(&collection, Default::default(), Default::default())
8488
}
8589
}
8690

0 commit comments

Comments
 (0)