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
//! What:
//! The one summary line a landed commit prints, the way `git commit` prints it: `[<branch> <name>] <subject>`, with
//! `(root-commit)` for a commit without a parent and `detached HEAD` for a detached head, the name abbreviated as Git
//! abbreviates it.
//! Why:
//! The line is for a commit the service built itself, where no `git commit` ran to print one. It is printed once, after the
//! service moved the branch, so the name is the commit that landed; a commit that real Git builds prints Git's own summary,
//! because the wrapper adds no `--quiet` to it. The abbreviation honors `core.abbrev` and otherwise follows the size of the
//! repository (`object_abbreviation.rs`), and Git is asked only when the configuration or the object store is not
//! something that reader models.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! console.log(summaryLine({ reference, root, name: shortName(commit), subject: subjectOf(message) }));
//! ```
use super::config_snapshot::ConfigSnapshot;
use super::diagnostic_log::warn;
use super::service_protocol::is_zero_object;
use super::transaction_git::{GitContext, GitRequest, run_git_checked};
use std::path::Path;
/// The characters a name keeps when nothing could say how Git would abbreviate it.
const LAST_RESORT_LENGTH: usize = 7;
/// The key whose value sets the length.
const ABBREVIATION_KEY: &str = "core.abbrev";
/// What:
/// The subject of a commit message: its first paragraph on one line.
/// Why:
/// Git's `%s` joins the lines of the first paragraph with spaces, and `git commit` prints that.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// message.trim().split(/\n\s*\n/)[0].split('\n').map((line) => line.trim()).join(' ')
/// ```
pub fn subject_of(message: &str) -> String {
let mut words: Vec<&str> = Vec::new();
for line in message.lines().skip_while(|line| { return line.trim().is_empty(); }) {
if line.trim().is_empty() {
break;
}
words.push(line.trim());
}
return words.join(" ");
}
/// What:
/// The line, from the branch reference, whether the commit has no parent, the abbreviated name and the subject.
/// Why:
/// The reference is `HEAD` for a detached head, which Git words as `detached HEAD`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `[${branch}${root ? ' (root-commit)' : ''} ${name}] ${subject}`
/// ```
pub fn summary_line(reference: &str, first_parent: &str, name: &str, subject: &str) -> String {
let branch: &str = if reference == "HEAD" { "detached HEAD" } else { reference.trim_start_matches("refs/heads/") };
let root: &str = if first_parent.is_empty() || is_zero_object(first_parent) { " (root-commit)" } else { "" };
return format!("[{branch}{root} {name}] {subject}");
}
/// The last value `core.abbrev` has in the snapshot.
fn configured(snapshot: &ConfigSnapshot) -> Option<Option<Vec<u8>>> {
return snapshot.entries().iter().rev().find(|entry| { return entry.key == ABBREVIATION_KEY; }).map(|entry| { return entry.value.clone(); });
}
/// Git's own answer for the name, for the shapes the in-process reader declines.
fn asked_of_git(context: &GitContext, cwd: &Path, commit: &str) -> Option<String> {
let request: GitRequest = GitRequest::new(cwd, &["rev-parse", "--verify", "--short", commit]);
return match run_git_checked(context, &request) {
Ok(output) => Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string()).filter(|text| { return !text.is_empty(); }),
Err(failure) => {
warn("landedSummary", format!("asking Git for the abbreviated name failed: {failure}").as_str());
None
}
};
}
/// What:
/// The abbreviated name of a landed commit.
/// Why:
/// Without starting Git when the configuration was read and the object store is plain; otherwise Git answers once. The
/// last resort is seven characters, which is what Git itself falls back to in a small repository.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// shortName({ context, cwd, objectDirectory, config, commit }) // '30b2211a4c'
/// ```
#[cfg(unix)]
pub fn short_name(context: &GitContext, cwd: &Path, object_directory: &Path, config: Option<&ConfigSnapshot>, commit: &str) -> String {
use super::object_abbreviation::{Setting, abbreviate, setting_of};
let from_files: Option<String> = config.and_then(|snapshot| {
let setting: Setting = match configured(snapshot) {
Some(value) => setting_of(value.as_deref()),
None => Setting::Auto,
};
return abbreviate(object_directory, setting, commit);
});
return from_files
.or_else(|| { return asked_of_git(context, cwd, commit); })
.unwrap_or_else(|| { return commit.get(..LAST_RESORT_LENGTH).unwrap_or(commit).to_string(); });
}
/// Without the in-process reader Git answers, and the last resort is the same.
#[cfg(not(unix))]
pub fn short_name(context: &GitContext, cwd: &Path, _object_directory: &Path, _config: Option<&ConfigSnapshot>, commit: &str) -> String {
return asked_of_git(context, cwd, commit).unwrap_or_else(|| { return commit.get(..LAST_RESORT_LENGTH).unwrap_or(commit).to_string(); });
}
/// What:
/// The one line the wrapper adds after a commit that real Git built was built again because the branch moved.
/// Why:
/// Git printed a summary for each build, and the earlier ones name commits that never landed; the line says which names
/// those are and which commit did land, so a person does not copy the wrong one. Git's own output is left as it is.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// rebuiltNote(['1a2b3c4'], '5d6e7f8') // 'cli-git: the branch moved during the commit, so it was built again; 1a2b3c4 did not land, 5d6e7f8 did.'
/// ```
pub fn rebuilt_note(unlanded: &[String], landed: &str) -> String {
let names: String = match unlanded.split_last() {
None => String::new(),
Some((only, [])) => only.clone(),
Some((last, front)) => format!("{} and {last}", front.join(", ")),
};
return format!("cli-git: the branch moved during the commit, so it was built again; {names} did not land, {landed} did.");
}
/// Summary controls stay out of the release executable.
#[cfg(test)]
#[path = "landed_summary_tests.rs"]
mod tests;