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
//! 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(); });
}
/// Summary controls stay out of the release executable.
#[cfg(test)]
#[path = "landed_summary_tests.rs"]
mod tests;