Skip to main content

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;