Skip to main content

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;