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
//! pre-commit-usual-name — warn the first time an author commits under a
//! given name/email, so a misconfigured `user.name` is noticed at the first
//! commit rather than after twenty.
//!
//! Warning only: it never blocks a commit.
use crate::check::Outcome;
use crate::git;
use crate::ui::{highlight, warning_sign};
pub fn run(_args: &[std::ffi::OsString]) -> Outcome {
// An empty repo has no history to compare against.
if !git::succeeds(&["log", "-1"]) {
return Outcome::Passed;
}
let name = git::stdout(&["config", "user.name"]).unwrap_or_default();
let email = git::stdout(&["config", "user.email"]).unwrap_or_default();
let full = format!("{name} <{email}>");
// FIXED-STRING containment, never a pattern: a real name can carry regex
// metacharacters (O'Brien, "Foo (Bar)") and as a pattern those would
// misfire — `(dev)` is a group, and would match an author who never
// committed. This is why the shell version used rg -F / grep -F.
let seen = git::stdout(&["shortlog", "-s", "-n", "-e", "--all"])
.is_some_and(|log| log.contains(&full));
// A new identity is the one thing this check exists to notice, so it is
// `Warned` and not `Passed` — the commit proceeds either way, but the
// dashboard should not count a fresh identity as a clean run.
if !seen {
println!(
"{} It is the first time you commit as {}",
warning_sign(),
highlight(&full)
);
return Outcome::Warned;
}
Outcome::Passed
}
#[cfg(test)]
mod tests {
/// The property that matters, isolated from git: containment is literal.
fn seen(log: &str, full: &str) -> bool {
log.contains(full)
}
#[test]
fn matches_an_existing_author_literally() {
let log = " 12\ttest all mighty <test@domain.test>\n 3\tOther <o@x.test>";
assert!(seen(log, "test all mighty <test@domain.test>"));
assert!(!seen(log, "test mighty <test@domain.test>"));
}
/// As a REGEX, "test (dev) all mighty" would match "test dev all mighty"
/// via the group — the false negative that lets a misconfigured identity
/// pass unnoticed. Containment cannot do that.
#[test]
fn regex_metacharacters_stay_literal() {
let log = " 1\ttest dev all mighty <t@x.test>";
assert!(!seen(log, "test (dev) all mighty <t@x.test>"));
let log2 = " 1\ttest (dev) all mighty <t@x.test>";
assert!(seen(log2, "test (dev) all mighty <t@x.test>"));
}
}