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