git_policy_cli/repository_location.rs
1//! What:
2//! Ask real Git,
3//! in one query,
4//! which repository an invocation selects and where
5//! inside its worktree the command runs.
6//! Why:
7//! The configuration root,
8//! the require-root policy and the worktree policies all need
9//! answers Git itself computes from `-C`,
10//! `--git-dir`,
11//! `--work-tree` and its
12//! environment.
13//! One `git rev-parse` prints all of them,
14//! so an invocation starts one
15//! Git process for these facts instead of one per fact,
16//! and the wrapper never
17//! re-implements Git's repository discovery or its `-C` chaining.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! // const { identity, prefix } = parseLocationOutput(await git(locationQueryArguments(globalPrefix)));
22//! ```
23
24/// What:
25/// `use` brings names from sibling files into this file;
26/// `super::` means "the parent
27/// module",
28/// where every sibling file of this crate is declared.
29/// Why:
30/// The identity part of the answer is read by the existing identity reader;
31/// this
32/// module adds the one extra line.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// import { parseIdentityOutput } from './worktree_identity.ts';
37/// ```
38use super::git_metadata::{path_from_git_bytes, strip_git_line};
39use super::worktree_identity::{IDENTITY_QUERY, WorktreeIdentity, parse_identity_output};
40/// What:
41/// `OsString` is owned operating-system text of raw bytes.
42/// Sibling the reader might
43/// expect:
44/// `String`,
45/// which must be valid UTF-8.
46/// Why:
47/// The caller's global options are replayed to Git unchanged.
48///
49/// In TS you'd write (pseudocode):
50/// ```ts
51/// // string[] argv, but byte-preserving.
52/// ```
53use std::ffi::OsString;
54/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw bytes.
55use std::path::{Path, PathBuf};
56
57/// What:
58/// The `rev-parse` option that prints the path from the worktree top level to the
59/// directory the command runs in.
60/// `&str` is borrowed text baked into the program.
61/// Why:
62/// Git prints an empty line at the top level,
63/// so "is the command run from the
64/// root" is Git's own answer,
65/// with every `-C` and symbolic link already applied.
66///
67/// In TS you'd write (pseudocode):
68/// ```ts
69/// const PREFIX_OPTION = '--show-prefix';
70/// ```
71pub const PREFIX_OPTION: &str = "--show-prefix";
72
73/// What:
74/// Everything the one query answers.
75/// A `struct` is a record with named fields;
76/// `Vec<u8>` is an owned list of bytes (`u8` is one byte;
77/// sibling `String` would
78/// require UTF-8).
79/// `#[derive(...)]` asks the compiler to generate cloning,
80/// debug
81/// printing and `==`.
82/// Why:
83/// The prefix is kept as the bytes Git printed,
84/// because a directory name need not
85/// be UTF-8.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// type RepositoryLocation = { identity: WorktreeIdentity; prefix: Buffer };
90/// ```
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct RepositoryLocation {
93 /// Which repository shape the invocation selects.
94 pub identity: WorktreeIdentity,
95 /// Path from the worktree top level to the command's directory,
96 /// with Git's trailing
97 /// slash;
98 /// empty at the top level,
99 /// and empty where the identity has no worktree.
100 pub prefix: Vec<u8>,
101}
102
103/// Named predicate for finding the last line break;
104/// `&u8` borrows one byte.
105fn is_line_feed(byte: &u8) -> bool {
106 return *byte == b'\n';
107}
108
109/// What:
110/// Interpret the location query's exit state and output.
111/// `&[u8]` borrows the raw
112/// output;
113/// `std::io::Result<T>` is `Result<T, std::io::Error>`,
114/// "a value or an
115/// input/output error".
116/// Why:
117/// On success the last line is the prefix and everything before it is the identity
118/// answer.
119/// On failure Git stopped before the prefix,
120/// so the output is the identity
121/// answer of a place without a worktree.
122/// Any other shape is rejected,
123/// not guessed.
124///
125/// In TS you'd write (pseudocode):
126/// ```ts
127/// function parseLocationOutput(success: boolean, stdout: Buffer): RepositoryLocation; // throws
128/// ```
129pub fn parse_location_output(success: bool, stdout: &[u8]) -> std::io::Result<RepositoryLocation> {
130 if !success {
131 // A trailing `?` returns the reader's error to our caller, or unwraps the identity.
132 let identity: WorktreeIdentity = parse_identity_output(false, stdout)?;
133 // `Ok(...)` is the success variant; `Vec::<u8>::new()` is an empty owned byte list.
134 return Ok(RepositoryLocation {
135 identity,
136 prefix: Vec::<u8>::new(),
137 });
138 }
139 let body: &[u8] = strip_git_line(stdout);
140 // `.iter().rposition(f)` is the index of the last byte for which `f` is true, or nothing.
141 let Some(last_break) = body.iter().rposition(is_line_feed) else {
142 // `Err(...)` is the failure variant; `std::io::Error::other` wraps a message.
143 return Err(std::io::Error::other(
144 "cli-git could not locate the command inside the Git worktree: git rev-parse \
145 printed no line for the path below the top level.",
146 ));
147 };
148 // `&body[..=n]` borrows up to and including index `n`; `&body[n..]` borrows from `n` on.
149 let identity: WorktreeIdentity = parse_identity_output(true, &body[..=last_break])?;
150 return Ok(RepositoryLocation {
151 identity,
152 // `.to_vec()` copies the borrowed bytes into an owned list.
153 prefix: body[last_break + 1..].to_vec(),
154 });
155}
156
157/// What:
158/// Build the real-Git argument list of the location query:
159/// the caller's global
160/// options,
161/// the identity options,
162/// then the prefix option.
163/// `&[OsString]` borrows the
164/// arguments before the subcommand;
165/// `Vec<OsString>` is the owned result.
166/// Why:
167/// Replaying the complete global prefix lets Git apply `-C` chains through symbolic
168/// links,
169/// `--git-dir`,
170/// `--work-tree` and its environment exactly as it will for the
171/// forwarded command.
172/// The prefix option is last because Git stops at
173/// `--show-toplevel` where there is no worktree,
174/// before it would print a prefix.
175///
176/// In TS you'd write (pseudocode):
177/// ```ts
178/// const args = [...globalPrefix, ...IDENTITY_QUERY, '--show-prefix'];
179/// ```
180pub fn location_query_arguments(global_prefix: &[OsString]) -> Vec<OsString> {
181 // `.to_vec()` copies the borrowed prefix into an owned, growable argument list.
182 let mut arguments: Vec<OsString> = global_prefix.to_vec();
183 // `for part in IDENTITY_QUERY` visits each compiled-in option of the identity query.
184 for part in IDENTITY_QUERY {
185 arguments.push(OsString::from(part));
186 }
187 arguments.push(OsString::from(PREFIX_OPTION));
188 return arguments;
189}
190
191/// What:
192/// The directory the command runs in:
193/// the worktree top level plus the prefix.
194/// `&Path` borrows the top level;
195/// the result is an owned path.
196/// Why:
197/// Diagnostics name the directory,
198/// and the require-root decision compares it with
199/// the top level.
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// const effectiveDirectory = prefix === '' ? root : join(root, prefix);
204/// ```
205pub fn effective_directory(worktree_root: &Path, prefix: &[u8]) -> PathBuf {
206 // Git ends a prefix with a slash; `.strip_suffix` is `Some(shorter)` when it is there,
207 // and `.unwrap_or(prefix)` keeps the bytes as they are otherwise.
208 let relative_bytes: &[u8] = prefix.strip_suffix(b"/").unwrap_or(prefix);
209 // `match` unpacks "a path or nothing": an empty prefix is no path at all.
210 match path_from_git_bytes(relative_bytes) {
211 // `.join` appends the relative path and returns an owned path.
212 Some(relative) => return worktree_root.join(relative),
213 // `.to_path_buf()` copies the borrowed path into an owned one.
214 None => return worktree_root.to_path_buf(),
215 }
216}
217
218/// Output-shape and disposable-repository controls stay out of the release executable.
219#[cfg(test)]
220#[path = "repository_location_tests.rs"]
221mod tests;