1use git_core::{CommitObject, GitHash, GitRepo, Result};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[repr(u8)]
14pub enum Role {
15 Author = 0,
17 Committer = 1,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct AttributionEvent {
24 pub commit: GitHash,
26 pub role: Role,
28 pub name: String,
30 pub email: String,
32 pub timestamp: i64,
34 pub tz_offset_secs: i32,
36}
37
38#[must_use]
43pub fn attribution_timeline(commits: &[CommitObject]) -> Vec<AttributionEvent> {
44 let mut events = Vec::with_capacity(commits.len() * 2);
45 for c in commits {
46 for (role, sig) in [(Role::Author, &c.author), (Role::Committer, &c.committer)] {
47 events.push(AttributionEvent {
48 commit: c.hash,
49 role,
50 name: sig.name.clone(),
51 email: sig.email.clone(),
52 timestamp: sig.timestamp,
53 tz_offset_secs: sig.tz_offset_secs,
54 });
55 }
56 }
57 events.sort_by(|a, b| {
58 a.timestamp
59 .cmp(&b.timestamp)
60 .then((a.role as u8).cmp(&(b.role as u8)))
61 });
62 events
63}
64
65#[must_use]
68pub fn distinct_identities(commits: &[CommitObject]) -> Vec<(String, String)> {
69 let mut seen = Vec::new();
70 for c in commits {
71 for sig in [&c.author, &c.committer] {
72 let id = (sig.name.clone(), sig.email.clone());
73 if !seen.contains(&id) {
74 seen.push(id);
75 }
76 }
77 }
78 seen
79}
80
81pub fn attribution_repo(repo: &GitRepo, from: GitHash) -> Result<Vec<AttributionEvent>> {
86 let mut commits = Vec::new();
87 for commit in repo.walk_commits(from) {
88 commits.push(commit?);
89 }
90 Ok(attribution_timeline(&commits))
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use git_core::Signature;
97
98 fn sig(name: &str, ts: i64, tz: i32) -> Signature {
99 Signature {
100 name: name.into(),
101 email: format!("{name}@x"),
102 timestamp: ts,
103 tz_offset_secs: tz,
104 }
105 }
106
107 fn commit(hex: &str, author: Signature, committer: Signature) -> CommitObject {
108 CommitObject {
109 hash: GitHash::from_hex(hex).unwrap(),
110 tree: GitHash::from_hex("89abcdef0123456789abcdef0123456789abcdef").unwrap(),
111 parents: vec![],
112 author,
113 committer,
114 message: "m".into(),
115 is_signed: false,
116 }
117 }
118
119 #[test]
120 fn timeline_is_time_ordered_author_before_committer() {
121 let c1 = commit(
122 "0123456789abcdef0123456789abcdef01234567",
123 sig("alice", 1_000, 0),
124 sig("bob", 2_000, 3600),
125 );
126 let c2 = commit(
127 "1123456789abcdef0123456789abcdef01234567",
128 sig("carol", 1_500, -7200),
129 sig("carol", 1_500, -7200),
130 );
131 let tl = attribution_timeline(&[c1, c2]);
132 let times: Vec<i64> = tl.iter().map(|e| e.timestamp).collect();
135 assert_eq!(times, vec![1_000, 1_500, 1_500, 2_000]);
136 assert_eq!(tl[0].name, "alice");
137 assert_eq!(tl[0].role, Role::Author);
138 assert_eq!(tl[1].role, Role::Author); assert_eq!(tl[2].role, Role::Committer);
140 assert_eq!(tl[3].name, "bob");
141 assert_eq!(tl[3].tz_offset_secs, 3600);
142 }
143
144 #[test]
145 fn distinct_identities_dedup_in_first_seen_order() {
146 let c = commit(
147 "0123456789abcdef0123456789abcdef01234567",
148 sig("alice", 1, 0),
149 sig("bob", 2, 0),
150 );
151 let ids = distinct_identities(std::slice::from_ref(&c));
152 assert_eq!(
153 ids,
154 vec![
155 ("alice".into(), "alice@x".into()),
156 ("bob".into(), "bob@x".into())
157 ]
158 );
159 let solo = commit(
161 "1123456789abcdef0123456789abcdef01234567",
162 sig("alice", 1, 0),
163 sig("alice", 2, 0),
164 );
165 assert_eq!(distinct_identities(&[solo]).len(), 1);
166 }
167}