Skip to content

Commit d75159c

Browse files
committed
Merge branch 'fix-1103'
2 parents 3ff1827 + c712850 commit d75159c

File tree

7 files changed

+188
-43
lines changed

7 files changed

+188
-43
lines changed

Cargo.lock

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

gix-command/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ mod prepare {
3434

3535
/// Builder
3636
impl Prepare {
37-
/// If called, the command will not be executed directly, but with `sh`.
37+
/// If called, the command will not be executed directly, but with `sh`, but ony if the
38+
/// command passed to [`prepare`](super::prepare()) requires this.
3839
///
3940
/// This also allows to pass shell scripts as command, or use commands that contain arguments which are subsequently
4041
/// parsed by `sh`.

gix-command/tests/command.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,65 @@
11
use gix_testtools::Result;
22

33
mod prepare {
4+
#[cfg(windows)]
5+
const SH: &str = "sh";
6+
#[cfg(not(windows))]
7+
const SH: &str = "/bin/sh";
8+
49
fn quoted(input: &[&str]) -> String {
510
input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ")
611
}
12+
13+
#[test]
14+
fn empty() {
15+
let cmd = std::process::Command::from(gix_command::prepare(""));
16+
assert_eq!(format!("{cmd:?}"), "\"\"");
17+
}
718
#[test]
819
fn single_and_multiple_arguments() {
920
let cmd = std::process::Command::from(gix_command::prepare("ls").arg("first").args(["second", "third"]));
1021
assert_eq!(format!("{cmd:?}"), quoted(&["ls", "first", "second", "third"]));
1122
}
23+
24+
#[test]
25+
fn single_and_multiple_arguments_as_part_of_command() {
26+
let cmd = std::process::Command::from(gix_command::prepare("ls first second third"));
27+
assert_eq!(
28+
format!("{cmd:?}"),
29+
quoted(&["ls first second third"]),
30+
"without shell, this is an invalid command"
31+
);
32+
}
33+
34+
#[test]
35+
fn single_and_multiple_arguments_as_part_of_command_with_shell() {
36+
let cmd = std::process::Command::from(gix_command::prepare("ls first second third").with_shell());
37+
assert_eq!(
38+
format!("{cmd:?}"),
39+
quoted(&[SH, "-c", "ls first second third", "--"]),
40+
"with shell, this works as it performs word splitting"
41+
);
42+
}
43+
44+
#[test]
45+
fn single_and_complex_arguments_as_part_of_command_with_shell() {
46+
let cmd = std::process::Command::from(gix_command::prepare("ls --foo \"a b\"").arg("additional").with_shell());
47+
assert_eq!(
48+
format!("{cmd:?}"),
49+
format!("\"{SH}\" \"-c\" \"ls --foo \\\"a b\\\" \\\"$@\\\"\" \"--\" \"additional\""),
50+
"with shell, this works as it performs word splitting"
51+
);
52+
}
53+
54+
#[test]
55+
fn tilde_path_and_multiple_arguments_as_part_of_command_with_shell() {
56+
let cmd = std::process::Command::from(gix_command::prepare("~/bin/exe --foo \"a b\"").with_shell());
57+
assert_eq!(
58+
format!("{cmd:?}"),
59+
format!("\"{SH}\" \"-c\" \"~/bin/exe --foo \\\"a b\\\"\" \"--\""),
60+
"this always needs a shell as we need tilde expansion"
61+
);
62+
}
1263
}
1364

1465
mod spawn {

gix-credentials/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ gix-trace = { version = "^0.1.3", path = "../gix-trace" }
2828
thiserror = "1.0.32"
2929
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
3030
bstr = { version = "1.3.0", default-features = false, features = ["std"]}
31+
shell-words = "1.0"
3132

3233

3334

gix-credentials/src/program/mod.rs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ pub enum Kind {
1212
/// A custom credentials helper, as identified just by the name with optional arguments
1313
ExternalName {
1414
/// The name like `foo` along with optional args, like `foo --arg --bar="a b"`, with arguments using `sh` shell quoting rules.
15-
/// The program executed will be `git-credential-foo` if `name_and_args` starts with `foo`.
15+
/// The program executed will be `git-credential-foo [args]` if `name_and_args` starts with `foo [args]`.
16+
/// Note that a shell is only used if it's needed.
1617
name_and_args: BString,
1718
},
1819
/// A custom credentials helper, as identified just by the absolute path to the program and optional arguments. The program is executed through a shell.
@@ -53,7 +54,6 @@ impl Program {
5354
if gix_path::is_absolute(path) {
5455
Kind::ExternalPath { path_and_args: input }
5556
} else {
56-
input.insert_str(0, "git credential-");
5757
Kind::ExternalName { name_and_args: input }
5858
}
5959
};
@@ -65,33 +65,41 @@ impl Program {
6565
}
6666
from_custom_definition_inner(input.into())
6767
}
68-
}
69-
70-
/// Builder
71-
impl Program {
72-
/// By default `stderr` of programs is inherited and typically displayed in the terminal.
73-
pub fn suppress_stderr(mut self) -> Self {
74-
self.stderr = false;
75-
self
76-
}
77-
}
7868

79-
impl Program {
80-
pub(crate) fn start(
81-
&mut self,
82-
action: &helper::Action,
83-
) -> std::io::Result<(std::process::ChildStdin, Option<std::process::ChildStdout>)> {
84-
assert!(self.child.is_none(), "BUG: must not call `start()` twice");
69+
/// Convert the program into the respective command, suitable to invoke `action`.
70+
pub fn to_command(&self, action: &helper::Action) -> std::process::Command {
71+
let git_program = cfg!(windows).then(|| "git.exe").unwrap_or("git");
8572
let mut cmd = match &self.kind {
8673
Kind::Builtin => {
87-
let mut cmd = Command::new(cfg!(windows).then(|| "git.exe").unwrap_or("git"));
74+
let mut cmd = Command::new(git_program);
8875
cmd.arg("credential").arg(action.as_arg(false));
8976
cmd
9077
}
91-
Kind::ExternalShellScript(for_shell)
92-
| Kind::ExternalName {
93-
name_and_args: for_shell,
78+
Kind::ExternalName { name_and_args } => {
79+
let mut args = name_and_args.clone();
80+
args.insert_str(0, "credential-");
81+
args.insert_str(0, " ");
82+
args.insert_str(0, git_program);
83+
let split_args = if args.find_byteset(b"\\|&;<>()$`\n*?[#~%").is_none() {
84+
args.to_str()
85+
.ok()
86+
.and_then(|args| shell_words::split(args).ok().map(Vec::into_iter))
87+
} else {
88+
None
89+
};
90+
match split_args {
91+
Some(mut args) => {
92+
let mut cmd = Command::new(args.next().expect("non-empty input"));
93+
cmd.args(args).arg(action.as_arg(true));
94+
cmd
95+
}
96+
None => gix_command::prepare(gix_path::from_bstr(args.as_ref()).into_owned())
97+
.arg(action.as_arg(true))
98+
.with_shell()
99+
.into(),
100+
}
94101
}
102+
Kind::ExternalShellScript(for_shell)
95103
| Kind::ExternalPath {
96104
path_and_args: for_shell,
97105
} => gix_command::prepare(gix_path::from_bstr(for_shell.as_bstr()).as_ref())
@@ -106,6 +114,26 @@ impl Program {
106114
Stdio::null()
107115
})
108116
.stderr(if self.stderr { Stdio::inherit() } else { Stdio::null() });
117+
cmd
118+
}
119+
}
120+
121+
/// Builder
122+
impl Program {
123+
/// By default `stderr` of programs is inherited and typically displayed in the terminal.
124+
pub fn suppress_stderr(mut self) -> Self {
125+
self.stderr = false;
126+
self
127+
}
128+
}
129+
130+
impl Program {
131+
pub(crate) fn start(
132+
&mut self,
133+
action: &helper::Action,
134+
) -> std::io::Result<(std::process::ChildStdin, Option<std::process::ChildStdout>)> {
135+
assert!(self.child.is_none(), "BUG: must not call `start()` twice");
136+
let mut cmd = self.to_command(action);
109137
gix_trace::debug!(cmd = ?cmd, "launching credential helper");
110138
let mut child = cmd.spawn()?;
111139
let stdin = child.stdin.take().expect("stdin to be configured");
Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,102 @@
1-
use gix_credentials::{program::Kind, Program};
1+
use gix_credentials::{helper, program::Kind, Program};
2+
3+
#[cfg(windows)]
4+
const GIT: &str = "git.exe";
5+
#[cfg(not(windows))]
6+
const GIT: &str = "git";
7+
8+
#[cfg(windows)]
9+
const SH: &str = "sh";
10+
#[cfg(not(windows))]
11+
const SH: &str = "/bin/sh";
212

313
#[test]
4-
fn script() {
5-
assert!(
6-
matches!(Program::from_custom_definition("!exe").kind, Kind::ExternalShellScript(script) if script == "exe")
14+
fn empty() {
15+
let prog = Program::from_custom_definition("");
16+
assert!(matches!(&prog.kind, Kind::ExternalName { name_and_args } if name_and_args == ""));
17+
assert_eq!(
18+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
19+
format!(r#""{GIT}" "credential-" "store""#),
20+
"not useful, but allowed, would have to be caught elsewhere"
21+
);
22+
}
23+
24+
#[test]
25+
fn simple_script_in_path() {
26+
let prog = Program::from_custom_definition("!exe");
27+
assert!(matches!(&prog.kind, Kind::ExternalShellScript(script) if script == "exe"));
28+
assert_eq!(
29+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
30+
r#""exe" "store""#,
31+
"it didn't detect anything shell-scripty, and thus doesn't use a shell"
732
);
833
}
934

1035
#[test]
1136
fn name_with_args() {
1237
let input = "name --arg --bar=\"a b\"";
13-
let expected = "git credential-name --arg --bar=\"a b\"";
14-
assert!(
15-
matches!(Program::from_custom_definition(input).kind, Kind::ExternalName{name_and_args} if name_and_args == expected)
38+
let prog = Program::from_custom_definition(input);
39+
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
40+
assert_eq!(
41+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
42+
format!(r#""{GIT}" "credential-name" "--arg" "--bar=a b" "store""#)
43+
);
44+
}
45+
46+
#[test]
47+
fn name_with_special_args() {
48+
let input = "name --arg --bar=~/folder/in/home";
49+
let prog = Program::from_custom_definition(input);
50+
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
51+
assert_eq!(
52+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
53+
format!(r#""{SH}" "-c" "{GIT} credential-name --arg --bar=~/folder/in/home \"$@\"" "--" "store""#)
1654
);
1755
}
1856

1957
#[test]
2058
fn name() {
2159
let input = "name";
22-
let expected = "git credential-name";
23-
assert!(
24-
matches!(Program::from_custom_definition(input).kind, Kind::ExternalName{name_and_args} if name_and_args == expected)
60+
let prog = Program::from_custom_definition(input);
61+
assert!(matches!(&prog.kind, Kind::ExternalName{name_and_args} if name_and_args == input));
62+
assert_eq!(
63+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
64+
format!(r#""{GIT}" "credential-name" "store""#),
65+
"we detect that this can run without shell, which is also more portable on windows"
2566
);
2667
}
2768

2869
#[test]
29-
fn path_with_args() {
70+
fn path_with_args_that_definitely_need_shell() {
3071
let input = "/abs/name --arg --bar=\"a b\"";
31-
assert!(
32-
matches!(Program::from_custom_definition(input).kind, Kind::ExternalPath{path_and_args} if path_and_args == input)
72+
let prog = Program::from_custom_definition(input);
73+
assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input));
74+
assert_eq!(
75+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
76+
format!(r#""{SH}" "-c" "/abs/name --arg --bar=\"a b\" \"$@\"" "--" "store""#)
3377
);
3478
}
3579

3680
#[test]
37-
fn path() {
81+
fn path_without_args() {
3882
let input = "/abs/name";
39-
assert!(
40-
matches!(Program::from_custom_definition(input).kind, Kind::ExternalPath{path_and_args} if path_and_args == input)
83+
let prog = Program::from_custom_definition(input);
84+
assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input));
85+
assert_eq!(
86+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
87+
r#""/abs/name" "store""#,
88+
"no shell is used"
89+
);
90+
}
91+
92+
#[test]
93+
fn path_with_simple_args() {
94+
let input = "/abs/name a b";
95+
let prog = Program::from_custom_definition(input);
96+
assert!(matches!(&prog.kind, Kind::ExternalPath{path_and_args} if path_and_args == input));
97+
assert_eq!(
98+
format!("{:?}", prog.to_command(&helper::Action::Store("egal".into()))),
99+
format!(r#""{SH}" "-c" "/abs/name a b \"$@\"" "--" "store""#),
100+
"a shell is used as well because there are arguments, and we don't do splitting ourselves. On windows, this can be a problem."
41101
);
42102
}

gix/tests/repository/config/config_snapshot/credential_helpers.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,7 @@ mod baseline {
8181
.programs
8282
.iter()
8383
.map(|p| match &p.kind {
84-
gix_credentials::program::Kind::ExternalName { name_and_args } => name_and_args
85-
.strip_prefix(b"git credential-")
86-
.expect("resolved name")
87-
.into(),
84+
gix_credentials::program::Kind::ExternalName { name_and_args } => name_and_args.to_owned(),
8885
_ => panic!("need name helper"),
8986
})
9087
.collect();

0 commit comments

Comments
 (0)