diff --git a/crates/moments-core/src/presentation/github.rs b/crates/moments-core/src/presentation/github.rs index c09b9a8..5932b7b 100644 --- a/crates/moments-core/src/presentation/github.rs +++ b/crates/moments-core/src/presentation/github.rs @@ -91,24 +91,58 @@ type Reshaped = ( fn push(repo: Option<&str>, p: Option<&Value>) -> Reshaped { let repo = repo.unwrap_or("(unknown repo)"); - let size = p - .and_then(|v| v.get("distinct_size").or_else(|| v.get("size"))) + let distinct_size = p + .and_then(|v| v.get("distinct_size")) .and_then(Value::as_i64) .unwrap_or(0); + let forced = p + .and_then(|v| v.get("forced")) + .and_then(Value::as_bool) + .unwrap_or(false); + let created = p + .and_then(|v| v.get("created")) + .and_then(Value::as_bool) + .unwrap_or(false); let branch = p .and_then(|v| v.get("ref")) .and_then(Value::as_str) .map(ref_branch) .unwrap_or(""); - let title = vec![ - TitleSegment::text(format!( - "pushed {size} commit{} to ", - if size == 1 { "" } else { "s" } - )), - repo_link(repo), - TitleSegment::text(format!(":{branch}")), - ]; + // Branch-creation pushes have distinct_size = 0 (the commits already + // existed on another branch) and a different intent than a code push. + // Force-pushes and ordinary pushes both render with the GitPush icon + // but are phrased differently. + let (icon, title) = if created { + ( + TimelineIcon::GitBranchCreate, + vec![ + TitleSegment::text(format!("created branch {branch} in ")), + repo_link(repo), + ], + ) + } else if distinct_size == 0 { + let verb = if forced { "force-pushed" } else { "pushed to" }; + ( + TimelineIcon::GitPush, + vec![ + TitleSegment::text(format!("{verb} ")), + repo_link(repo), + TitleSegment::text(format!(":{branch}")), + ], + ) + } else { + let verb = if forced { "force-pushed" } else { "pushed" }; + let plural = if distinct_size == 1 { "" } else { "s" }; + ( + TimelineIcon::GitPush, + vec![ + TitleSegment::text(format!("{verb} {distinct_size} commit{plural} to ")), + repo_link(repo), + TitleSegment::text(format!(":{branch}")), + ], + ) + }; let commits: Vec = p .and_then(|v| v.get("commits")) @@ -148,7 +182,7 @@ fn push(repo: Option<&str>, p: Option<&Value>) -> Reshaped { Some(TimelineBody::Commits { commits }) }; - (TimelineIcon::GitPush, title, None, body) + (icon, title, None, body) } fn pull_request(repo: Option<&str>, p: Option<&Value>) -> Reshaped { @@ -555,6 +589,80 @@ mod tests { } } + fn render(item: &TimelineItem) -> String { + item.title + .iter() + .map(|s| match s { + TitleSegment::Text { text } => text.clone(), + TitleSegment::Link { text, .. } => text.clone(), + }) + .collect() + } + + #[test] + fn push_branch_creation_uses_create_icon() { + let raw = json!({ + "actor": { "login": "grenade" }, + "repo": { "name": "grenade/vortex" }, + "payload": { + "ref": "refs/heads/fix-double-panic", + "size": 0, + "distinct_size": 0, + "created": true, + "forced": false, + "commits": [] + } + }); + let item = reshape(&ev("PushEvent", raw)); + assert_eq!(item.icon, TimelineIcon::GitBranchCreate); + let r = render(&item); + assert!( + r.contains("created branch fix-double-panic in grenade/vortex"), + "got: {r}" + ); + } + + #[test] + fn force_push_with_commits_says_force_pushed() { + let raw = json!({ + "actor": { "login": "grenade" }, + "repo": { "name": "grenade/x" }, + "payload": { + "ref": "refs/heads/main", + "size": 1, + "distinct_size": 1, + "created": false, + "forced": true, + "commits": [{ "sha": "deadbeefcafe1234", "message": "rebase" }] + } + }); + let item = reshape(&ev("PushEvent", raw)); + assert_eq!(item.icon, TimelineIcon::GitPush); + let r = render(&item); + assert!(r.contains("force-pushed 1 commit to grenade/x:main"), "got: {r}"); + } + + #[test] + fn empty_push_omits_commit_count() { + let raw = json!({ + "actor": { "login": "grenade" }, + "repo": { "name": "grenade/x" }, + "payload": { + "ref": "refs/heads/main", + "size": 0, + "distinct_size": 0, + "created": false, + "forced": false, + "commits": [] + } + }); + let item = reshape(&ev("PushEvent", raw)); + assert_eq!(item.icon, TimelineIcon::GitPush); + let r = render(&item); + assert!(r.contains("pushed to grenade/x:main"), "got: {r}"); + assert!(!r.contains("0 commit"), "should not say '0 commits': {r}"); + } + #[test] fn merged_pr_uses_merge_icon() { let raw = json!({ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b05a91d..56be828 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -18,13 +18,10 @@ const RANGE_MIN = new Date('2010-01-01T00:00:00Z').getTime(); const RANGE_MAX = Date.now(); const externalLinks: { url: string; alt: string }[] = [ - { url: 'https://instagram.com/rob_thij', alt: 'instagram' }, - { url: 'https://www.facebook.com/rob.thijssen', alt: 'facebook' }, { url: 'https://linkedin.com/in/thijssen/', alt: 'linkedin' }, { url: 'https://stackoverflow.com/users/68115/grenade', alt: 'stackoverflow' }, { url: 'https://github.com/grenade', alt: 'github' }, { url: 'https://git.lair.cafe/grenade', alt: 'gitea' }, - { url: 'https://steelhorseadventures.com', alt: 'steel horse adventures' }, ]; export default function App() {