Skip to main content

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;