Skip to main content

git_policy_cli/
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 super::capture_invocation::InvocationCapture;
18use super::commit_args::{resolve_private_commit_args, split_commit_invocation, without_all_flag};
19use super::commit_preparation::{PreparationRequest, read_prepared_commit, run_native_commit};
20use super::commit_transaction::TransactionStop;
21use super::global_arguments::global_layout;
22use super::shadow_create::create_shadow_repository_at;
23use super::transaction_git::GitContext;
24use super::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    /// Whether Git is told to print no summary, because the caller prints one line for the commit that lands.
69    pub quiet: bool,
70}
71
72/// The parents named by a stored commit's header block.
73pub fn parents_of(raw: &[u8]) -> Vec<String> {
74    let header_end: usize = raw.windows(2).position(|window| { return window == b"\n\n"; }).unwrap_or(raw.len());
75    return String::from_utf8_lossy(&raw[..header_end])
76        .lines()
77        .filter_map(|line| { return line.strip_prefix("parent ").map(str::to_string); })
78        .collect();
79}
80
81/// What:
82///  The kept global options and the commit options of the arguments, with the pathspecs and the selection options the
83///  private index already holds removed.
84/// Why:
85///   Real `git commit` must commit the private index as it stands, so it is given neither the paths nor `-a`.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// privateInvocation({ arguments, pathspecs, mode, stagedAll })
90/// ```
91pub fn private_invocation(
92    arguments: &[OsString],
93    pathspecs: &[String],
94    mode: TransactionMode,
95    staged_all: bool,
96    selected_privately: bool,
97) -> (Vec<String>, Vec<String>) {
98    let text: Vec<String> = arguments.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect();
99    let private_args: Vec<String> = resolve_private_commit_args(text.as_slice(), pathspecs, mode, selected_privately);
100    let owned: Vec<OsString> = private_args.iter().map(OsString::from).collect();
101    let index: usize = global_layout(owned.as_slice()).prefix_len;
102    let (globals, commit_args): (Vec<String>, Vec<String>) = split_commit_invocation(private_args.as_slice(), index);
103    return if staged_all { (globals, without_all_flag(commit_args.as_slice())) } else { (globals, commit_args) };
104}
105
106/// What:
107///  Runs real `git commit` against the scratch repository and reads the commit it made.
108/// Why:
109///   Git's own output, editor and signer reach the person's terminal as they would from a plain `git commit`; a nonzero
110///  status of Git is the command's status and nothing lands.
111///
112/// In TS you'd write (pseudocode):
113/// ```ts
114/// await buildWithGit(input)
115/// ```
116///
117/// # Errors
118///
119/// `TransactionStop::NativeExit` with Git's status when Git declined or was aborted (an empty message, an editor that
120///  failed, a signer that failed); `TransactionStop::Message` when Git could not be started.
121pub fn build_with_git(input: &BuildInput<'_>) -> Result<GitBuilt, TransactionStop> {
122    let shadow = input.scratch.join("shadow");
123    create_shadow_repository_at(input.context, input.cwd, input.capture, shadow.as_path(), input.scratch)?;
124    let (globals, mut commit_args): (Vec<String>, Vec<String>) = private_invocation(input.arguments, input.pathspecs, input.mode, input.staged_all, input.selected_privately);
125    if input.quiet {
126        commit_args.push(String::from("--quiet"));
127    }
128    let request: PreparationRequest<'_> = PreparationRequest {
129        capture: input.capture,
130        shadow: shadow.as_path(),
131        commit_index: input.commit_index,
132        global_args: globals.as_slice(),
133        commit_args: commit_args.as_slice(),
134        object_directory: Some(input.capture.layout.object_directory.as_path()),
135    };
136    let status: i32 = run_native_commit(input.context, input.cwd, &request).map_err(|error| { return TransactionStop::Message(error.to_string()); })?;
137    if status != 0 {
138        return Err(TransactionStop::NativeExit(status));
139    }
140    let prepared = read_prepared_commit(input.context, shadow.as_path())?;
141    return Ok(GitBuilt { parents: parents_of(prepared.raw.as_slice()), message: message_of(prepared.raw.as_slice()), commit: prepared.oid });
142}
143
144/// Controls for building with Git stay out of the release executable.
145#[cfg(test)]
146#[path = "service_native_tests.rs"]
147mod tests;