Skip to content

Commit 5ef8ad1

Browse files
Fix attributing subtree bump commits to a single release
This changes the discovery strategy for by-release commits. Instead of trying to guess the delta by looking at the commits landed on master from one commit to the next, we instead find all commits reachable from the new tag and all commits reachable from the previous tag, and delta that. This avoids the (previously buggy) situation where a commit moves from a submodule to in-tree (i.e., when moving things to subtrees) which caused us to count it for that release. This is because the commit was newly reachable in the mainline repository, even though it was pre-existing in the AuthorMap of the previous version's submodules. The new implementation is somewhat slower as we're doing significantly more tree-walking, but ultimately still fairly fast. We don't care too much about our performance right now.
1 parent 8139b26 commit 5ef8ad1

File tree

1 file changed

+83
-73
lines changed

1 file changed

+83
-73
lines changed

src/main.rs

Lines changed: 83 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,23 @@ impl AuthorMap {
6969
.extend(set);
7070
}
7171
}
72+
73+
#[must_use]
74+
fn difference(&self, other: &AuthorMap) -> AuthorMap {
75+
let mut new = AuthorMap::new();
76+
new.map.reserve(self.map.len());
77+
for (author, set) in self.map.iter() {
78+
if let Some(other_set) = other.map.get(&author) {
79+
let diff: HashSet<_> = set.difference(other_set).cloned().collect();
80+
if !diff.is_empty() {
81+
new.map.insert(author.clone(), diff);
82+
}
83+
} else {
84+
new.map.insert(author.clone(), set.clone());
85+
}
86+
}
87+
new
88+
}
7289
}
7390

7491
fn git(args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
@@ -165,6 +182,12 @@ impl fmt::Display for VersionTag {
165182
}
166183
}
167184

185+
impl std::hash::Hash for VersionTag {
186+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
187+
self.version.hash(state);
188+
}
189+
}
190+
168191
impl cmp::Eq for VersionTag {}
169192
impl cmp::PartialEq for VersionTag {
170193
fn eq(&self, other: &Self) -> bool {
@@ -233,11 +256,13 @@ fn commit_coauthors(commit: &Commit) -> Vec<Author> {
233256
}
234257

235258
for line in msg.lines().rev() {
236-
if let Some(caps) = RE.captures(line) {
237-
coauthors.push(Author {
238-
name: caps["name"].to_string(),
239-
email: caps["email"].to_string(),
240-
});
259+
if line.starts_with("Co-authored-by") {
260+
if let Some(caps) = RE.captures(line) {
261+
coauthors.push(Author {
262+
name: caps["name"].to_string(),
263+
email: caps["email"].to_string(),
264+
});
265+
}
241266
}
242267
}
243268
}
@@ -378,6 +403,7 @@ fn build_author_map_(
378403
for oid in walker {
379404
let oid = oid?;
380405
let commit = repo.find_commit(oid)?;
406+
381407
let mut commit_authors = Vec::new();
382408
if !is_rollup_commit(&commit) {
383409
// We ignore the author of rollup-merge commits, and account for
@@ -424,6 +450,44 @@ fn mailmap_from_repo(repo: &git2::Repository) -> Result<Mailmap, Box<dyn std::er
424450
Mailmap::from_string(file)
425451
}
426452

453+
fn up_to_release(
454+
repo: &Repository,
455+
reviewers: &Reviewers,
456+
mailmap: &Mailmap,
457+
to: &VersionTag,
458+
) -> Result<AuthorMap, Box<dyn std::error::Error>> {
459+
let to_commit = repo.find_commit(to.commit).map_err(|e| {
460+
ErrorContext(
461+
format!(
462+
"find_commit: repo={}, commit={}",
463+
repo.path().display(),
464+
to.commit
465+
),
466+
Box::new(e),
467+
)
468+
})?;
469+
let modules = get_submodules(&repo, &to_commit)?;
470+
471+
let mut author_map = build_author_map(&repo, &reviewers, &mailmap, "", &to.raw_tag)
472+
.map_err(|e| ErrorContext(format!("Up to {}", to), e))?;
473+
474+
for module in &modules {
475+
if let Ok(path) = update_repo(&module.repository) {
476+
let subrepo = Repository::open(&path)?;
477+
let submap = build_author_map(
478+
&subrepo,
479+
&reviewers,
480+
&mailmap,
481+
"",
482+
&module.commit.to_string(),
483+
)?;
484+
author_map.extend(submap);
485+
}
486+
}
487+
488+
Ok(author_map)
489+
}
490+
427491
fn generate_thanks() -> Result<BTreeMap<VersionTag, AuthorMap>, Box<dyn std::error::Error>> {
428492
let path = update_repo("https://github.com/rust-lang/rust.git")?;
429493
let repo = git2::Repository::open(&path)?;
@@ -467,6 +531,8 @@ fn generate_thanks() -> Result<BTreeMap<VersionTag, AuthorMap>, Box<dyn std::err
467531

468532
let mut version_map = BTreeMap::new();
469533

534+
let mut cache = HashMap::new();
535+
470536
for (idx, version) in versions.iter().enumerate() {
471537
let previous = if let Some(v) = idx.checked_sub(1).map(|idx| &versions[idx]) {
472538
v
@@ -478,75 +544,19 @@ fn generate_thanks() -> Result<BTreeMap<VersionTag, AuthorMap>, Box<dyn std::err
478544

479545
eprintln!("Processing {:?} to {:?}", previous, version);
480546

481-
let mut modules: HashMap<PathBuf, (Option<&Submodule>, Option<&Submodule>)> =
482-
HashMap::new();
483-
let previous_commit = repo.find_commit(previous.commit).map_err(|e| {
484-
ErrorContext(
485-
format!(
486-
"find_commit: repo={}, commit={}",
487-
repo.path().display(),
488-
previous.commit
489-
),
490-
Box::new(e),
491-
)
492-
})?;
493-
let previous_modules = get_submodules(&repo, &previous_commit)?;
494-
for module in &previous_modules {
495-
if let Ok(path) = update_repo(&module.repository) {
496-
modules.insert(path, (Some(module), None));
497-
}
498-
}
499-
500-
let current_commit = repo.find_commit(version.commit)?;
501-
let current_modules = get_submodules(&repo, &current_commit)?;
502-
for module in &current_modules {
503-
if let Ok(path) = update_repo(&module.repository) {
504-
modules.entry(path).or_insert((None, None)).1 = Some(module);
505-
}
506-
}
507-
508-
let mut author_map = build_author_map(
509-
&repo,
510-
&reviewers,
511-
&mailmap,
512-
&previous.raw_tag,
513-
&version.raw_tag,
514-
)
515-
.map_err(|e| ErrorContext(format!("From {} to {}", previous, version), e))?;
516-
517-
for (path, (pre, cur)) in &modules {
518-
match (pre, cur) {
519-
(Some(previous), Some(current)) => {
520-
let subrepo = Repository::open(&path)?;
521-
let submap = build_author_map(
522-
&subrepo,
523-
&reviewers,
524-
&mailmap,
525-
&previous.commit.to_string(),
526-
&current.commit.to_string(),
527-
)?;
528-
author_map.extend(submap);
529-
}
530-
(None, Some(current)) => {
531-
let subrepo = Repository::open(&path)?;
532-
let submap = build_author_map(
533-
&subrepo,
534-
&reviewers,
535-
&mailmap,
536-
"",
537-
&current.commit.to_string(),
538-
)?;
539-
author_map.extend(submap);
540-
}
541-
(None, None) => unreachable!(),
542-
(Some(_), None) => {
543-
// nothing, if submodule was deleted then we presume no changes since then
544-
// affect us
545-
}
546-
}
547-
}
547+
cache.insert(
548+
version,
549+
up_to_release(&repo, &reviewers, &mailmap, &version)?,
550+
);
551+
let previous = match cache.remove(&previous) {
552+
Some(v) => v,
553+
None => up_to_release(&repo, &reviewers, &mailmap, &previous)?,
554+
};
555+
let current = cache.get(&version).unwrap();
548556

549-
version_map.insert(version.clone(), author_map);
557+
// Remove commits reachable from the previous release.
558+
let only_current = current.difference(&previous);
559+
version_map.insert(version.clone(), only_current);
550560
}
551561

552562
Ok(version_map)

0 commit comments

Comments
 (0)