1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! What:
//! Run a read-only real-Git query and capture its output as raw bytes.
//! Why:
//! Repository facts (top level,
//! Git directory) come from Git itself,
//! and the
//! paths it prints need not be UTF-8.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const { stdout } = await spawn(gitPath, ['rev-parse', '--git-dir'], { stdout: 'pipe' });
//! ```
/// Import the shared child-command builder.
use super::forwarding::git_command;
/// What:
/// `OsString` is owned operating-system text of raw OS bytes (sibling `String`
/// must be UTF-8).
/// Why:
/// Query arguments include the caller's unchanged global options.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string[] argv, but byte-preserving.
/// ```
use std::ffi::OsString;
/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw OS bytes.
use std::path::{Path, PathBuf};
/// `Stdio` selects where a child's stream is connected.
use std::process::Stdio;
/// What:
/// What one metadata query produced.
/// `Vec<u8>` is an owned byte list (`u8` is one byte);
/// sibling `String` would
/// require UTF-8.
/// Why:
/// Git prints paths as raw bytes,
/// and a failed query is an ordinary answer
/// ("not a repository"),
/// not an error.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type MetadataOutput = { success: boolean; code: number | null; stdout: Buffer; stderr: Buffer };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataOutput {
/// Whether Git exited with status zero.
pub success: bool,
/// Git's exit code,
/// or nothing when a signal ended it.
/// Some queries answer through
/// their exit code alone (`git diff-index --quiet` exits 1 for "differs").
pub code: Option<i32>,
/// Exact bytes Git wrote to standard output,
/// including what it printed before failing.
pub stdout: Vec<u8>,
/// Exact bytes Git wrote to standard error.
pub stderr: Vec<u8>,
}
/// What:
/// Run real Git with captured output and no standard input.
/// `std::io::Result<T>` is `Result<T, std::io::Error>`.
/// Why:
/// A query must never consume the caller's stdin (which belongs to the
/// forwarded command) or write to the caller's streams.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function runMetadataGit(realGit, args, overlay): Promise<MetadataOutput>;
/// ```
pub fn run_metadata_git(
real_git: &Path,
arguments: &[OsString],
overlay: &[(OsString, OsString)],
) -> std::io::Result<MetadataOutput> {
// What: `.stdin(Stdio::null())` connects the child's input to nothing; `.output()`
// captures stdout and stderr and waits. A trailing `?` returns a start failure.
// Why: Only "Git could not be started" is an error here.
//
// In TS you'd write (pseudocode):
// ```ts
// const result = await spawn(realGit, args, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
// ```
let output: std::process::Output = git_command(real_git, arguments, overlay)
.stdin(Stdio::null())
.output()?;
// `Ok(...)` is the success variant carrying the captured result.
return Ok(MetadataOutput {
success: output.status.success(),
code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
});
}
/// What:
/// Remove exactly one final line break (`\n` or `\r\n`) from Git output.
/// `&[u8]` borrows bytes;
/// the result borrows a shorter view of the same bytes.
/// Why:
/// Git terminates each printed value with one line break;
/// anything before it,
/// including other line breaks,
/// is part of the value.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function stripGitLine(output: Buffer): Buffer;
/// ```
pub fn strip_git_line(output: &[u8]) -> &[u8] {
// What: `.strip_suffix(..)` is `Some(shorter)` when the bytes end with the suffix.
// Why: The two-byte ending is tried first so its `\r` is not left behind.
//
// In TS you'd write (pseudocode):
// ```ts
// if (output.endsWith('\r\n')) return output.slice(0, -2);
// ```
if let Some(stripped) = output.strip_suffix(b"\r\n") {
return stripped;
}
if let Some(stripped) = output.strip_suffix(b"\n") {
return stripped;
}
return output;
}
/// What:
/// Turn path bytes printed by Git into a path,
/// without decoding on Unix.
/// `Option<PathBuf>` is "an owned path or nothing".
/// `#[cfg(unix)]` and
/// `#[cfg(not(unix))]` each compile one of the two inner blocks.
/// Why:
/// Unix paths are arbitrary bytes and must round-trip exactly;
/// other systems can
/// only build a path from valid UTF-8.
/// An empty value is never a path anywhere.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function pathFromGitBytes(bytes: Buffer): string | undefined;
/// ```
pub fn path_from_git_bytes(bytes: &[u8]) -> Option<PathBuf> {
if bytes.is_empty() {
// `None` is the "absent" variant.
return None;
}
#[cfg(unix)]
{
// The trait adds `from_vec`, which wraps raw bytes as OS text unchanged.
use std::os::unix::ffi::OsStringExt;
// `.to_vec()` copies the borrowed bytes into an owned list.
return Some(PathBuf::from(OsString::from_vec(bytes.to_vec())));
}
#[cfg(not(unix))]
{
// `match` unpacks the decoding: text becomes a path, other bytes are not one.
match std::str::from_utf8(bytes) {
Ok(text) => return Some(PathBuf::from(text)),
Err(_) => return None,
}
}
}
/// Byte-level controls stay out of the release executable.
#[cfg(test)]
#[path = "git_metadata_tests.rs"]
mod tests;