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