Skip to main content

git_policy_cli/landing/
service_native.rs

1//! What:
2//!  The commits real Git builds for the commit service: an editor, a signature, and every command line the service does
3//!  not build itself.
4//! Why:
5//!  Git owns the editor, the signer and the message semantics of whatever the service cannot embed, so this module points
6//!  real `git commit` at a thin scratch repository (its `HEAD` and branch at the base, the policy-settled private index,
7//!  the real configuration included, every hook off) whose new objects go straight into the real object store. Nothing is
8//!  locked and nothing is registered: the scratch repository sits inside the commit's own scratch directory, is removed
9//!  with it, and the commit it makes is only an object until the service moves the branch to it by compare-and-swap
10//!  (`doc/planning/cli-git-resident-service.md`, "Commits Git builds").
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const { commit, parents } = await buildWithGit({ context, cwd, capture, scratch, commitIndex, arguments });
15//! ```
16
17use crate::capture_invocation::InvocationCapture;
18use crate::commit_args::{resolve_private_commit_args, split_commit_invocation, without_all_flag};
19use crate::commit_preparation::{PreparationRequest, read_prepared_commit, run_native_commit};
20use crate::commit_transaction::TransactionStop;
21use crate::global_arguments::global_layout;
22use crate::shadow_create::create_shadow_repository_carrying;
23use crate::transaction_git::GitContext;
24use crate::transaction_journal::TransactionMode;
25use std::ffi::OsString;
26use std::path::Path;
27
28/// What the scratch repository's `git commit` made.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct GitBuilt {
31    /// The commit, already in the real object store.
32    pub commit: String,
33    /// Its parents in order.
34    pub parents: Vec<String>,
35    /// Its message as stored: what the person typed, after Git's clean-up.
36    pub message: String,
37}
38
39/// The message after the header block of a stored commit.
40pub fn message_of(raw: &[u8]) -> String {
41    let start: usize = raw.windows(2).position(|window| { return window == b"\n\n"; }).map_or(raw.len(), |at| { return at + 2; });
42    return String::from_utf8_lossy(&raw[start..]).into_owned();
43}
44
45/// Everything the build needs from the preparation that settled the private index.
46pub struct BuildInput<'a> {
47    /// How to run the real Git.
48    pub context: &'a GitContext,
49    /// The directory the command was given in.
50    pub cwd: &'a Path,
51    /// The invocation capture: the branch, the base, the object directory.
52    pub capture: &'a InvocationCapture,
53    /// The commit's own scratch directory, which holds the scratch repository and is removed with it.
54    pub scratch: &'a Path,
55    /// The private index the policies settled.
56    pub commit_index: &'a Path,
57    /// The arguments after the policies' fixed transforms, global options included.
58    pub arguments: &'a [OsString],
59    /// The positional pathspecs of the command line, which the private index already holds.
60    pub pathspecs: &'a [String],
61    /// Whether the line commits named paths or the index.
62    pub mode: TransactionMode,
63    /// Whether `-a` staged tracked changes into the private index.
64    pub staged_all: bool,
65    /// Whether the private index already holds a selection made with `--include`, `--patch` or `--interactive`, so that Git
66    ///  is not given those options again.
67    pub selected_privately: bool,
68}
69
70/// The parents named by a stored commit's header block.
71pub fn parents_of(raw: &[u8]) -> Vec<String> {
72    let header_end: usize = raw.windows(2).position(|window| { return window == b"\n\n"; }).unwrap_or(raw.len());
73    return String::from_utf8_lossy(&raw[..header_end])
74        .lines()
75        .filter_map(|line| { return line.strip_prefix("parent ").map(str::to_string); })
76        .collect();
77}
78
79/// What:
80///  The kept global options and the commit options of the arguments, with the pathspecs and the selection options the
81///  private index already holds removed.
82/// Why:
83///   Real `git commit` must commit the private index as it stands, so it is given neither the paths nor `-a`.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// privateInvocation({ arguments, pathspecs, mode, stagedAll })
88/// ```
89pub fn private_invocation(
90    arguments: &[OsString],
91    pathspecs: &[String],
92    mode: TransactionMode,
93    staged_all: bool,
94    selected_privately: bool,
95) -> (Vec<String>, Vec<String>) {
96    let text: Vec<String> = arguments.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect();
97    let private_args: Vec<String> = resolve_private_commit_args(text.as_slice(), pathspecs, mode, selected_privately);
98    let owned: Vec<OsString> = private_args.iter().map(OsString::from).collect();
99    let index: usize = global_layout(owned.as_slice()).prefix_len;
100    let (globals, commit_args): (Vec<String>, Vec<String>) = split_commit_invocation(private_args.as_slice(), index);
101    return if staged_all { (globals, without_all_flag(commit_args.as_slice())) } else { (globals, commit_args) };
102}
103
104/// What:
105///  Runs real `git commit` against the scratch repository and reads the commit it made.
106/// Why:
107///   Git's own output, editor and signer reach the person's terminal as they would from a plain `git commit`; a nonzero
108///  status of Git is the command's status and nothing lands.
109///
110/// In TS you'd write (pseudocode):
111/// ```ts
112/// await buildWithGit(input)
113/// ```
114///
115/// # Errors
116///
117/// `TransactionStop::NativeExit` with Git's status when Git declined or was aborted (an empty message, an editor that
118///  failed, a signer that failed); `TransactionStop::Message` when Git could not be started.
119pub fn build_with_git(input: &BuildInput<'_>) -> Result<GitBuilt, TransactionStop> {
120    let shadow = input.scratch.join("shadow");
121    // Named paths while another session's merge, cherry-pick or revert is unfinished: real Git refuses that as a partial commit,
122    //  and a commit never fails for another session's unfinished work, so the scratch repository holds none of that state and
123    //  builds an ordinary commit of the paths; the worktree's state stays for its owner to conclude on the new head.
124    let git_dir: &Path = input.capture.layout.git_dir.as_path();
125    let carry_conclusion: bool = !(input.mode == TransactionMode::ExplicitPath && ["MERGE_HEAD", "CHERRY_PICK_HEAD"].iter().any(|name| { return git_dir.join(name).exists(); }));
126    create_shadow_repository_carrying(input.context, input.cwd, input.capture, shadow.as_path(), input.scratch, carry_conclusion)?;
127    // The wrapper adds no option of its own to the person's command line: Git prints what it prints, and `--quiet` reaches it only
128    //  when the person typed it.
129    let (globals, commit_args): (Vec<String>, Vec<String>) = private_invocation(input.arguments, input.pathspecs, input.mode, input.staged_all, input.selected_privately);
130    let request: PreparationRequest<'_> = PreparationRequest {
131        capture: input.capture,
132        shadow: shadow.as_path(),
133        commit_index: input.commit_index,
134        global_args: globals.as_slice(),
135        commit_args: commit_args.as_slice(),
136        object_directory: Some(input.capture.layout.object_directory.as_path()),
137    };
138    let status: i32 = run_native_commit(input.context, input.cwd, &request).map_err(|error| { return TransactionStop::Message(error.to_string()); })?;
139    if status != 0 {
140        return Err(TransactionStop::NativeExit(status));
141    }
142    let prepared = read_prepared_commit(input.context, shadow.as_path())?;
143    return Ok(GitBuilt { parents: parents_of(prepared.raw.as_slice()), message: message_of(prepared.raw.as_slice()), commit: prepared.oid });
144}
145
146/// Controls for building with Git stay out of the release executable.
147#[cfg(test)]
148#[path = "service_native_tests.rs"]
149mod tests;