Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fa02742

Browse files
codexByron
andcommitted
feat: add git utility to call git easily like git(cwd, "log --oneline")
Co-authored-by: Sebastian Thiel <[email protected]>
1 parent 4ed2816 commit fa02742

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

tests/tools/src/lib.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,90 @@ pub fn object_hash() -> gix_hash::Kind {
12441244
object_hash_from_env().unwrap_or_default()
12451245
}
12461246

1247+
/// Run `git` in `current_dir` with shell-like whitespace-separated `arguments`, returning stdout as UTF-8.
1248+
///
1249+
/// Note that Git is run as isolated as possible, just like scripts.
1250+
///
1251+
/// Arguments may be split across multiple lines. Single and double quotes can be used to keep whitespace
1252+
/// within an argument, for example `commit -m 'a message with spaces'`.
1253+
pub fn git(current_dir: impl AsRef<Path>, arguments: &str) -> Result<String> {
1254+
let args = split_git_arguments(arguments)?;
1255+
let cwd = current_dir.as_ref();
1256+
let mut cmd = std::process::Command::new(GIT_PROGRAM);
1257+
let output = configure_command(&mut cmd, object_hash(), args.iter().map(String::as_str), cwd)
1258+
.current_dir(cwd)
1259+
.output()?;
1260+
if !output.status.success() {
1261+
return Err(format!(
1262+
"{cmd:?} failed with status {}\nstdout: {}\nstderr: {}",
1263+
output.status,
1264+
output.stdout.as_bstr(),
1265+
output.stderr.as_bstr()
1266+
)
1267+
.into());
1268+
}
1269+
Ok(String::from_utf8(output.stdout)?)
1270+
}
1271+
1272+
fn split_git_arguments(input: &str) -> Result<Vec<String>> {
1273+
let mut args = Vec::new();
1274+
let mut arg = String::new();
1275+
let mut quote = None;
1276+
let mut has_arg = false;
1277+
let mut chars = input.chars();
1278+
1279+
while let Some(ch) = chars.next() {
1280+
match quote {
1281+
Some('\'') => {
1282+
if ch == '\'' {
1283+
quote = None;
1284+
} else {
1285+
arg.push(ch);
1286+
}
1287+
}
1288+
Some('"') => {
1289+
if ch == '"' {
1290+
quote = None;
1291+
} else if ch == '\\' {
1292+
if let Some(next) = chars.next() {
1293+
arg.push(next);
1294+
}
1295+
} else {
1296+
arg.push(ch);
1297+
}
1298+
}
1299+
Some(_) => unreachable!("only single and double quotes are set"),
1300+
None => {
1301+
if ch.is_whitespace() {
1302+
if has_arg {
1303+
args.push(std::mem::take(&mut arg));
1304+
has_arg = false;
1305+
}
1306+
} else if matches!(ch, '\'' | '"') {
1307+
quote = Some(ch);
1308+
has_arg = true;
1309+
} else if ch == '\\' {
1310+
if let Some(next) = chars.next() {
1311+
arg.push(next);
1312+
}
1313+
has_arg = true;
1314+
} else {
1315+
arg.push(ch);
1316+
has_arg = true;
1317+
}
1318+
}
1319+
}
1320+
}
1321+
1322+
if let Some(quote) = quote {
1323+
return Err(format!("unterminated {quote:?} quote in git arguments").into());
1324+
}
1325+
if has_arg {
1326+
args.push(arg);
1327+
}
1328+
Ok(args)
1329+
}
1330+
12471331
/// Normalize debug-formatted `value` so one snapshot can be reused for SHA-1 and SHA-256 fixtures.
12481332
///
12491333
/// The helper rewrites 40- and 64-character hexadecimal object IDs to stable `Oid(<n>)`

tests/tools/src/tests.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,71 @@ fn invoke_bash_runs_in_given_working_directory() {
137137
);
138138
}
139139

140+
#[test]
141+
fn split_git_arguments_handles_multiline_whitespace() {
142+
assert_eq!(
143+
split_git_arguments(
144+
"log
145+
--graph
146+
--oneline",
147+
)
148+
.expect("valid arguments"),
149+
["log", "--graph", "--oneline"]
150+
);
151+
}
152+
153+
#[test]
154+
fn split_git_arguments_handles_quoted_arguments() {
155+
assert_eq!(
156+
split_git_arguments(
157+
"commit
158+
-m 'subject with spaces'
159+
--author=\"A U Thor <[email protected]>\"",
160+
)
161+
.expect("valid arguments"),
162+
[
163+
"commit",
164+
"-m",
165+
"subject with spaces",
166+
"--author=A U Thor <[email protected]>"
167+
]
168+
);
169+
}
170+
171+
#[test]
172+
fn split_git_arguments_handles_empty_quoted_arguments() {
173+
assert_eq!(
174+
split_git_arguments("diff -- pathspec:''").expect("valid arguments"),
175+
["diff", "--", "pathspec:"]
176+
);
177+
assert_eq!(
178+
split_git_arguments("diff -- ''").expect("valid arguments"),
179+
["diff", "--", ""]
180+
);
181+
}
182+
183+
#[test]
184+
fn split_git_arguments_handles_escaped_whitespace() {
185+
assert_eq!(
186+
split_git_arguments(r"add path\ with\ spaces").expect("valid arguments"),
187+
["add", "path with spaces"]
188+
);
189+
}
190+
191+
#[test]
192+
fn split_git_arguments_concatenates_quoted_and_unquoted_parts() {
193+
assert_eq!(
194+
split_git_arguments(r#"commit -m prefix" quoted "suffix"#).expect("valid arguments"),
195+
["commit", "-m", "prefix quoted suffix"]
196+
);
197+
}
198+
199+
#[test]
200+
fn split_git_arguments_rejects_unterminated_quotes() {
201+
assert!(split_git_arguments("commit -m 'unterminated").is_err());
202+
assert!(split_git_arguments("commit -m \"unterminated").is_err());
203+
}
204+
140205
#[test]
141206
fn normalize_debug_snapshot_returns_replaced_ids_by_placeholder_index() {
142207
let first = gix_hash::ObjectId::from_hex(b"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").expect("valid SHA1");

0 commit comments

Comments
 (0)