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;