1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! What:
//! The pieces of a service commit that need no loop: the request for a commit Git built, the request for a commit the service
//! built from the embedded message, and what a landed commit leaves for the person.
//! Why:
//! `service_commit.rs` holds the retry loop; these steps take what the loop knows as parameters and use nothing from it, so
//! they are a file the loop uses.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! requestForGitBuilt(...); requestForEmbedded(...); reportLanding(...)
//! ```
/// The marker lines a committed file still holds, and the line about an operation left open.
use crate::conflict_markers::{open_operation_line, paths_with_markers};
/// The kind of operation a commit concludes.
use crate::transaction_journal::Conclusion;
use crate::commit_transaction::{CommitInvocation, TransactionStop};
use crate::landed_summary::{rebuilt_note, short_name, subject_of, summary_line};
use crate::service_prepare::{NativeInputs, Ready};
use crate::service_protocol::CommitRequest;
use crate::worktree_completion::install_added_worktree_files;
use std::io::Write;
use std::path::PathBuf;
use crate::service_commit_input_notes_and_embedded_commit::{Embedded, print_note, print_summary};
/// What:
/// The request for a commit that Git built in the shadow: the built commit is named whole and the service lands it.
/// Why:
/// The service moves the branch to a commit it did not make, so the request carries no changes and no message.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const requestForGitBuilt = (ready, inputs, id, builtCommit) => ({ id, ...identity(ready, inputs), changes: [], commit: builtCommit, cleanup: ready.cleanup });
/// ```
pub(crate) fn request_for_git_built(invocation: &CommitInvocation<'_>, ready: &Ready, native_inputs: &NativeInputs, id: &str, built_commit: String) -> CommitRequest {
return CommitRequest {
id: id.to_string(),
repository: ready.repository.clone(),
common_dir: ready.common_dir.clone(),
git_dir: native_inputs.capture.layout.git_dir.clone(),
reference: ready.reference.clone(),
base: ready.base.clone(),
// The service names the index entries from the commit itself, so no list of changes travels.
changes: Vec::new(),
// The commit is Git's whole: message, identities and tree are in it.
message: String::new(),
author: None,
committer: String::new(),
amend: invocation.region.amend,
tree: None,
commit: Some(built_commit),
cleanup: ready.cleanup.clone(),
index: native_inputs.capture.layout.real_index_path.clone(),
captured: ready.captured_at,
};
}
/// What:
/// The request for a commit the service builds from the changes and the message embedded in the command line.
/// Why:
/// The service builds the commit itself, so the request carries the changes, the message and the identities.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const requestForEmbedded = (ready, inputs, id, made, tree) => ({ id, ...identity(ready, inputs), changes: ready.changes, message: made.message });
/// ```
pub(crate) fn request_for_embedded(ready: &Ready, native_inputs: &NativeInputs, id: &str, made: &Embedded, tree: &Option<String>) -> CommitRequest {
return CommitRequest {
id: id.to_string(),
repository: ready.repository.clone(),
common_dir: ready.common_dir.clone(),
git_dir: native_inputs.capture.layout.git_dir.clone(),
reference: ready.reference.clone(),
base: ready.base.clone(),
changes: ready.changes.clone().unwrap_or_default(),
message: made.message.clone(),
author: Some(made.author.clone()),
committer: made.committer.clone(),
amend: tree.is_some(),
tree: tree.clone(),
commit: None,
cleanup: None,
index: native_inputs.capture.layout.real_index_path.clone(),
captured: ready.captured_at,
};
}
/// What:
/// What a landed commit leaves for the person: the saved message file removed, the summary line of a commit the service built,
/// the note about builds that did not land, the warnings about conflict markers that were committed, the line about an open
/// merge or cherry-pick, and the files a policy added in the worktree.
/// Why:
/// They follow the landing and change nothing the landing decided, so the commit loop hands them to one function.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// reportLanding(invocation, ready, nativeInputs, commit, firstParent, fixedMessage, embeddedInUse, native, unlanded)
/// ```
pub(crate) fn report_landing(
invocation: &CommitInvocation<'_>,
ready: &Ready,
native_inputs: &NativeInputs,
commit: &str,
first_parent: &str,
fixed_message: Option<&PathBuf>,
embedded_in_use: Option<&Embedded>,
native: bool,
unlanded: &[String],
) -> Result<(), TransactionStop> {
if let Some(file) = fixed_message {
// The commit was made from the saved message; the file has done its work.
let _removed = std::fs::remove_file(file);
}
// A commit the service built itself has had no `git commit` to print its summary, so one line is printed once the
// branch moved, and it names the commit that landed. A commit Git built was printed by Git, as Git prints it, with
// the options the person typed and no option of the wrapper's.
if let Some(made) = embedded_in_use
&& !made.quiet
{
let name: String = short_name(
invocation.context,
invocation.cwd,
native_inputs.capture.layout.object_directory.as_path(),
native_inputs.capture.layout.config_snapshot.as_ref(),
commit,
);
print_summary(summary_line(ready.reference.as_str(), first_parent, name.as_str(), subject_of(made.message.as_str()).as_str()).as_str());
}
// A commit Git built again printed a summary for each build; one line names the builds that did not land and the one that did.
if native && !unlanded.is_empty() {
let mut lost: Vec<String> = Vec::new();
for full in unlanded {
lost.push(abbreviated_name(invocation, native_inputs, full.as_str()));
}
print_note(rebuilt_note(lost.as_slice(), abbreviated_name(invocation, native_inputs, commit).as_str()).as_str());
}
// The owner: "Committing a conflicted path is fine. Warn and commit anyway, even if conflict markers will be committed."
// The claim is made for a path whose committed blob holds marker lines, read from the blob and the marker size
// the attributes give; a path whose blob holds none is resolved, and nothing is said about markers for it.
for path in paths_with_markers(invocation.context, ready.repository.as_path(), commit, ready.committed_conflicted.as_slice()) {
let name: String = String::from_utf8_lossy(path.as_slice()).into_owned();
// Terminal output error: a warning is advisory; a failed write must not fail a commit that landed.
let _warned = writeln!(
std::io::stderr(),
"monochromatic-git-policy-cli: warning: {name} still has conflict markers, and they were committed. To fix it, remove the \
markers from {name} and make a follow-up commit: git commit -m \"<message>\" -- {name}"
);
}
// A commit of named paths leaves the merge or cherry-pick open, whether or not markers were committed.
if let Some(kind) = ready.leaves_open {
let _noted = write!(std::io::stderr(), "{}", open_operation_line(kind == Conclusion::Merge));
}
// The corrections a policy made reach the worktree once the commit has landed, as the earlier landing did.
install_added_worktree_files(
invocation.context,
invocation.cwd,
ready.repository.as_path(),
ready.worktree_records.as_slice(),
Some(native_inputs.capture.layout.object_directory.as_path()),
)
.map_err(message_stop)?;
return Ok(());
}
/// What:
/// A failure with a message as the stop that ends the commit with that message.
/// Why:
/// `map_err` hands over the failure itself.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const messageStop = (error: { 0: string }) => new TransactionStop(error[0]);
/// ```
pub(crate) fn message_stop(error: crate::recovery_error::RecoveryError) -> TransactionStop {
return TransactionStop::Message(error.0);
}
/// What:
/// The name Git gives a commit in this repository as it is now, as short as Git writes it.
/// Why:
/// The notes after a landing name commits by their short names, asked of Git once per name.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const abbreviatedName = (full: string) => shortName(context, cwd, objectDirectory, config, full);
/// ```
pub(crate) fn abbreviated_name(invocation: &CommitInvocation<'_>, native_inputs: &NativeInputs, full: &str) -> String {
return short_name(
invocation.context,
invocation.cwd,
native_inputs.capture.layout.object_directory.as_path(),
native_inputs.capture.layout.config_snapshot.as_ref(),
full,
);
}