Skip to content

Commit 9d7e28d

Browse files
committed
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.
1 parent 42d4590 commit 9d7e28d

File tree

2 files changed

+188
-1
lines changed

2 files changed

+188
-1
lines changed

gix-command/src/lib.rs

+82-1
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

+106
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";

0 commit comments

Comments
 (0)