git_policy_cli/landing_location.rs
1//! What:
2//! Where a forwarded command's landing lock and real index live:
3//! one `git rev-parse` that
4//! reports the git directory,
5//! the index Git would use,
6//! and the transaction registry.
7//! Why:
8//! An index writer coordinates with landings through the registry's landing lock and the real
9//! index's lock.
10//! A command outside a repository,
11//! or one writing another index,
12//! coordinates with
13//! nothing (`src/index-lock/landing-location.ts`).
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! const [gitDir, indexPath, registryRoot] = (await nanoSpawn(gitPath, [...globalArgs, 'rev-parse', ...])).stdout.split('\n');
18//! ```
19
20/// Running real Git for the location query.
21use super::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
22/// `OsString` is owned operating-system text of raw bytes.
23use std::ffi::OsString;
24/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
25use std::path::{Path, PathBuf};
26
27/// What:
28/// The landing lock and index paths of one repository selection.
29/// Why:
30/// `LandingLocation`;
31/// the real index is the worktree's `<git-dir>/index`.
32///
33/// In TS you'd write (pseudocode):
34/// ```ts
35/// type LandingLocation = { cwd: string; gitDir: string; indexPath: string; realIndexPath: string; registryRoot: string };
36/// ```
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct LandingLocation {
39 /// Per-worktree Git directory.
40 pub git_dir: PathBuf,
41 /// Index Git would write,
42 /// honouring `GIT_INDEX_FILE`.
43 pub index_path: PathBuf,
44 /// The worktree's real index,
45 /// `<git-dir>/index`.
46 pub real_index_path: PathBuf,
47 /// Transaction registry directory that holds the landing lock.
48 pub registry_root: PathBuf,
49}
50
51/// What:
52/// The arguments of the layout query,
53/// after the selection's global options.
54/// Why:
55/// One process reports every path the coordination needs,
56/// in a fixed order.
57///
58/// In TS you'd write (pseudocode):
59/// ```ts
60/// ['rev-parse', '--path-format=absolute', '--absolute-git-dir', '--git-path', 'index', '--git-path', 'cli-git-transactions']
61/// ```
62fn layout_arguments() -> Vec<OsString> {
63 return ["rev-parse", "--path-format=absolute", "--absolute-git-dir", "--git-path", "index", "--git-path", "cli-git-transactions"]
64 .into_iter()
65 .map(OsString::from)
66 .collect();
67}
68
69/// What:
70/// Split the layout output into its three fixed lines,
71/// refusing any other shape.
72/// Why:
73/// A missing line would point the lock at the wrong directory.
74///
75/// In TS you'd write (pseudocode):
76/// ```ts
77/// const [gitDir, indexPath, registryRoot] = stdout.split('\n'); if (!registryRoot) throw new TypeError(...);
78/// ```
79pub fn parse_layout(stdout: &[u8]) -> Result<LandingLocation, String> {
80 let text: String = String::from_utf8_lossy(stdout).into_owned();
81 let lines: Vec<&str> = text.split('\n').collect();
82 if lines.len() < 3 || lines[0].is_empty() || lines[1].is_empty() || lines[2].is_empty() {
83 return Err(format!("Unexpected rev-parse layout: {text:?}"));
84 }
85 let git_dir: PathBuf = PathBuf::from(lines[0]);
86 return Ok(LandingLocation {
87 real_index_path: git_dir.join("index"),
88 index_path: PathBuf::from(lines[1]),
89 registry_root: PathBuf::from(lines[2]),
90 git_dir,
91 });
92}
93
94/// What:
95/// Resolve the landing location for the global options of one command.
96/// Why:
97/// `resolveLandingLocation`:
98/// outside a repository the answer is nothing,
99/// not an error.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// async function resolveLandingLocation({ gitPath, globalArgs }): Promise<LandingLocation | typeof LANDING_LOCATION_ABSENT>;
104/// ```
105pub fn resolve_landing_location(
106 context: &GitContext,
107 global_arguments: &[OsString],
108) -> Result<Option<LandingLocation>, String> {
109 // `mut` lets the request carry the layout arguments after the global options.
110 let mut arguments: Vec<OsString> = global_arguments.to_vec();
111 arguments.extend(layout_arguments());
112 let mut request: GitRequest = GitRequest::new(Path::new("."), arguments.as_slice());
113 request.without_prefix = true;
114 match run_git(context, &request) {
115 Ok(output) => return answer_of(&output),
116 Err(error) => return Err(error.to_string()),
117 }
118}
119
120/// What:
121/// The landing location a finished query answers with:
122/// none when Git refused,
123/// otherwise the parsed layout.
124/// Why:
125/// A refusal from Git means the command is not inside a repository.
126///
127/// In TS you'd write (pseudocode):
128/// ```ts
129/// if (error instanceof SubprocessError) return LANDING_LOCATION_ABSENT;
130/// ```
131fn answer_of(output: &GitOutput) -> Result<Option<LandingLocation>, String> {
132 if !output.succeeded() {
133 return Ok(None);
134 }
135 return parse_layout(output.stdout.as_slice()).map(Some);
136}
137
138/// Location controls stay out of the release executable.
139#[cfg(test)]
140#[path = "landing_location_tests.rs"]
141mod tests;