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
//! `messages` analysis — code-maat parity. Match commit messages against
//! a user-supplied regex; count one row per (file, matching-commit). High
//! match counts surface files repeatedly touched by commits matching the
//! regex — useful for "where do my bug-fix commits land?" or "which files
//! get the most refactor mentions?".
//!
//! ## Activation
//!
//! Requires `--expression-to-match REGEX` (the `Options.message_regex`
//! field). Calling `run_messages` without a regex returns an error.
//!
//! Research basis: see `docs/research-foundations.md` entry "messages"
//! (Mockus & Votta, ICSM 2000 — identifying reasons for software
//! changes from commit messages; Hindle et al., MSR 2011 — automated
//! topic naming).
use duckdb::params;
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MessagesRow {
pub entity: String,
pub matches: u32,
}
const SQL: &str = "
SELECT c.path AS entity, COUNT(*)::INTEGER AS matches
FROM changes c
JOIN commits m ON m.rev = c.rev
WHERE regexp_matches(m.message, ?)
GROUP BY c.path
ORDER BY matches DESC, entity ASC
LIMIT ?
";
#[tracing::instrument(name = "messages", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_messages(db: &FactsDb, opts: &Options) -> Result<Vec<MessagesRow>> {
let expr = opts.message_regex.as_deref().ok_or_else(|| {
CodeLoreError::Analysis(
"messages analysis requires --expression-to-match REGEX (commit-message filter)".into(),
)
})?;
// Validate the regex eagerly so we fail fast with a clear error before
// SQL prepare. DuckDB's regexp_matches uses RE2 flavor — close enough
// to Rust regex that valid Rust regexes work in DuckDB (modulo
// backreferences and lookaround which neither supports anyway).
let _ = regex::Regex::new(expr).map_err(|e| {
CodeLoreError::Analysis(format!("invalid --expression-to-match regex {expr:?}: {e}"))
})?;
let row_limit: i64 = opts.rows_limit.map_or(i64::MAX, i64::from);
crate::analyses::lineage::materialize_if_needed(db, opts)?;
let sql = crate::analyses::lineage::rewrite(SQL, opts);
crate::analyses::query::explain_if_requested(
db,
&sql,
params![expr, row_limit],
"messages",
opts,
)?;
crate::analyses::query::query_map_collect(db, &sql, params![expr, row_limit], "messages", |r| {
Ok(MessagesRow {
entity: r.get::<_, String>(0)?,
matches: u32::try_from(r.get::<_, i64>(1)?).unwrap_or(u32::MAX),
})
})
}