a73x

5bccfd16

Render --- Linked Commits --- section in CLI issue show

alex emery   2026-04-12 07:19

Each line shows the short SHA, commit subject (truncated to 60 chars),
commit author, and then (linked by <event-author>, <timestamp>). When
the commit isn't locally reachable (GC'd, shallow), falls back to a
no-subject variant that still shows the linked-by metadata.

diff --git a/src/lib.rs b/src/lib.rs
index 399889c..b6be19b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -145,6 +145,51 @@ pub fn run(cli: cli::Cli, repo: &Repository) -> Result<(), error::Error> {
                        println!("\n{} ({}):\n{}", c.author.name, c.timestamp, c.body);
                    }
                }
                if !i.linked_commits.is_empty() {
                    println!("\n--- Linked Commits ---");
                    for lc in &i.linked_commits {
                        let short_sha = if lc.commit.len() >= 7 { &lc.commit[..7] } else { &lc.commit };
                        let (subject, commit_author) = match git2::Oid::from_str(&lc.commit)
                            .ok()
                            .and_then(|oid| repo.find_commit(oid).ok())
                        {
                            Some(commit) => {
                                let subject = commit
                                    .summary()
                                    .map(|s| truncate_summary(s, 60))
                                    .unwrap_or_default();
                                let author = commit
                                    .author()
                                    .name()
                                    .unwrap_or("unknown")
                                    .to_string();
                                (Some(subject), Some(author))
                            }
                            None => (None, None),
                        };
                        match (subject, commit_author) {
                            (Some(subject), Some(author)) => {
                                println!(
                                    "· linked {} \"{}\" by {} (linked by {}, {})",
                                    short_sha,
                                    subject,
                                    author,
                                    lc.event_author.name,
                                    lc.event_timestamp,
                                );
                            }
                            _ => {
                                println!(
                                    "· linked {} (commit {} not in local repo) (linked by {}, {})",
                                    short_sha,
                                    short_sha,
                                    lc.event_author.name,
                                    lc.event_timestamp,
                                );
                            }
                        }
                    }
                }
                Ok(())
            }
            IssueCmd::Label { id, label } => {
@@ -663,3 +708,15 @@ fn search(repo: &Repository, query: &str) -> Result<(), error::Error> {

    Ok(())
}

fn truncate_summary(s: &str, max_chars: usize) -> String {
    let mut out = String::new();
    for (count, c) in s.chars().enumerate() {
        if count + 1 > max_chars {
            out.push('…');
            return out;
        }
        out.push(c);
    }
    out
}
diff --git a/tests/sync_test.rs b/tests/sync_test.rs
index 571cb1d..b7a7729 100644
--- a/tests/sync_test.rs
+++ b/tests/sync_test.rs
@@ -1499,3 +1499,49 @@ fn commit_link_scan_dedups_against_remote_originated_events() {
    assert_eq!(bob_issue.linked_commits.len(), 1);
    assert_eq!(bob_issue.linked_commits[0].commit, linked_commit.to_string());
}

#[test]
fn cli_issue_show_renders_linked_commits_section() {
    use common::TestRepo;

    let repo = TestRepo::new("Alice", "alice@example.com");
    let issue_id = repo.issue_open("fix the thing");

    // Resolve the full id via --json so we can format a trailer prefix.
    let full_id = {
        let out = repo.run_ok(&["issue", "show", &issue_id, "--json"]);
        let v: serde_json::Value = serde_json::from_str(&out).unwrap();
        v["id"].as_str().unwrap().to_string()
    };
    let msg = format!("Fix a thing\n\nIssue: {}", &full_id[..8]);
    repo.git(&["commit", "--allow-empty", "-m", &msg]);

    // Set up a bare remote so sync has something to push to.
    let bare = TempDir::new().unwrap();
    Command::new("git")
        .args(["init", "--bare"])
        .current_dir(bare.path())
        .status()
        .unwrap();
    repo.git(&["remote", "add", "origin", bare.path().to_str().unwrap()]);
    repo.git(&["push", "-u", "origin", "main"]);
    repo.run_ok(&["init"]);
    repo.run_ok(&["sync"]);

    let show = repo.run_ok(&["issue", "show", &issue_id]);
    assert!(
        show.contains("--- Linked Commits ---"),
        "expected linked commits section, got:\n{}",
        show
    );
    assert!(
        show.contains("by Alice"),
        "expected commit author rendered, got:\n{}",
        show
    );
    assert!(
        show.contains("(linked by Alice"),
        "expected event author rendered, got:\n{}",
        show
    );
}