ba27ce44
Add collect_linked_shas helper to commit_link module
alex emery 2026-04-12 06:42
Walks an issue's DAG and returns every SHA appearing in an IssueCommitLink event. Used by scan_and_link as the per-issue dedup source of truth, cached on first match per sync.
diff --git a/src/commit_link.rs b/src/commit_link.rs index 0e1955d..cd5f631 100644 --- a/src/commit_link.rs +++ b/src/commit_link.rs @@ -2,6 +2,29 @@ //! //! See: docs/superpowers/specs/2026-04-12-commit-issue-link-design.md use std::collections::HashSet; use git2::Repository; use crate::dag; use crate::error::Error; use crate::event::Action; /// Walk an issue's event DAG and return every commit SHA that has an /// `IssueCommitLink` event attached. Called lazily on first match per issue /// during `scan_and_link`; the result is cached in the orchestrator's /// `HashMap<RefName, HashSet<String>>`. pub fn collect_linked_shas(repo: &Repository, issue_ref: &str) -> Result<HashSet<String>, Error> { let events = dag::walk_events(repo, issue_ref)?; let mut shas = HashSet::new(); for (_oid, event) in events { if let Action::IssueCommitLink { commit } = event.action { shas.insert(commit); } } Ok(shas) } /// Parse `Issue:` trailers from a commit message. /// /// Returns the list of trailer values in order of appearance. Follows git's diff --git a/tests/commit_link_test.rs b/tests/commit_link_test.rs index bd07976..dabf514 100644 --- a/tests/commit_link_test.rs +++ b/tests/commit_link_test.rs @@ -181,3 +181,35 @@ fn parser_rejects_empty_value() { let msg = "subject\n\nIssue: "; assert_eq!(parse_issue_trailers(msg), Vec::<String>::new()); } use git_collab::commit_link::collect_linked_shas; #[test] fn collect_linked_shas_empty_for_fresh_issue() { let _config = ScopedTestConfig::new(); let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path(), &alice()); let (ref_name, _id) = open_issue(&repo, &alice(), "bug"); let shas = collect_linked_shas(&repo, &ref_name).unwrap(); assert!(shas.is_empty()); } #[test] fn collect_linked_shas_returns_all_linked_commits_including_duplicates() { let _config = ScopedTestConfig::new(); let dir = TempDir::new().unwrap(); let repo = init_repo(dir.path(), &alice()); let (ref_name, _id) = open_issue(&repo, &alice(), "bug"); add_commit_link(&repo, &ref_name, &alice(), &sha(0xaa)); add_commit_link(&repo, &ref_name, &alice(), &sha(0xbb)); // Even a duplicate DAG entry (cross-machine race) is surfaced here — // this is the "source of truth" for whether we need to emit. add_commit_link(&repo, &ref_name, &alice(), &sha(0xaa)); let shas = collect_linked_shas(&repo, &ref_name).unwrap(); assert_eq!(shas.len(), 2); assert!(shas.contains(&sha(0xaa))); assert!(shas.contains(&sha(0xbb))); }