From 8c9d0df88dec161631a872645cc4979236f95cd4 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Wed, 8 Apr 2026 22:32:20 -0400 Subject: [PATCH] Add commit history to PR activity timeline Fetch commits via pulls.listCommits and interleave them chronologically with comments in the activity feed. Consecutive commits stack tightly while comments get full spacing. --- apps/dashboard/src/lib/github.functions.ts | 45 ++++- apps/dashboard/src/lib/github.types.ts | 8 + .../_protected/$owner/$repo/pull.$pullId.tsx | 185 +++++++++++++----- 3 files changed, 193 insertions(+), 45 deletions(-) diff --git a/apps/dashboard/src/lib/github.functions.ts b/apps/dashboard/src/lib/github.functions.ts index 4d4bc12..ebb05ee 100644 --- a/apps/dashboard/src/lib/github.functions.ts +++ b/apps/dashboard/src/lib/github.functions.ts @@ -11,6 +11,7 @@ import type { MyIssuesResult, MyPullsResult, PullComment, + PullCommit, PullDetail, PullFile, PullPageData, @@ -631,6 +632,44 @@ async function getPullCommentsResult( }); } +async function getPullCommitsResult( + context: GitHubContext, + data: PullFromRepoInput, +): Promise { + type PullCommitResponse = Awaited< + ReturnType + >["data"][number]; + + return getCachedGitHubRequest({ + context, + resource: "pulls.commits", + params: data, + freshForMs: githubCachePolicy.activity.staleTimeMs, + request: (headers) => + context.octokit.rest.pulls.listCommits({ + owner: data.owner, + repo: data.repo, + pull_number: data.pullNumber, + per_page: 100, + headers, + }), + mapData: (commits) => + commits.map((c) => ({ + sha: c.sha, + message: c.commit.message, + createdAt: c.commit.author?.date ?? c.commit.committer?.date ?? "", + author: c.author + ? { + login: c.author.login, + avatarUrl: c.author.avatar_url, + url: c.author.html_url, + type: c.author.type ?? "User", + } + : null, + })), + }); +} + async function computePullStatus( context: GitHubContext, data: PullFromRepoInput, @@ -764,11 +803,15 @@ async function getPullPageDataResult( freshForMs: githubCachePolicy.detail.staleTimeMs, }); - const comments = await getPullCommentsResult(context, data); + const [comments, commits] = await Promise.all([ + getPullCommentsResult(context, data), + getPullCommitsResult(context, data), + ]); return { detail: mapPullDetail(pull, buildRepositoryRef(data.owner, data.repo)), comments, + commits, }; } diff --git a/apps/dashboard/src/lib/github.types.ts b/apps/dashboard/src/lib/github.types.ts index 1d257d9..81fea7b 100644 --- a/apps/dashboard/src/lib/github.types.ts +++ b/apps/dashboard/src/lib/github.types.ts @@ -152,9 +152,17 @@ export type PullStatus = { canUpdateBranch: boolean; }; +export type PullCommit = { + sha: string; + message: string; + createdAt: string; + author: GitHubActor | null; +}; + export type PullPageData = { detail: PullDetail | null; comments: PullComment[]; + commits: PullCommit[]; }; export type PullFile = { diff --git a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx index 75fa132..acb31c0 100644 --- a/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx +++ b/apps/dashboard/src/routes/_protected/$owner/$repo/pull.$pullId.tsx @@ -28,7 +28,13 @@ import { githubPullPageQueryOptions, githubPullStatusQueryOptions, } from "#/lib/github.query"; -import type { GitHubActor, PullDetail, PullStatus } from "#/lib/github.types"; +import type { + GitHubActor, + PullComment, + PullCommit, + PullDetail, + PullStatus, +} from "#/lib/github.types"; import { githubCachePolicy } from "#/lib/github-cache-policy"; import { useHasMounted } from "#/lib/use-has-mounted"; import { useRegisterTab } from "#/lib/use-register-tab"; @@ -110,6 +116,7 @@ function PullDetailPage() { const pr = pageQuery.data?.detail; const comments = pageQuery.data?.comments; + const commits = pageQuery.data?.commits; const status = statusQuery.data ?? null; useRegisterTab( @@ -241,13 +248,13 @@ function PullDetailPage() { )} - {/* Activity / Comments */} + {/* Activity */}

Activity

- {comments && ( + {comments && commits && ( - {comments.length} + {comments.length + commits.length} )}
@@ -278,46 +285,19 @@ function PullDetailPage() {
)} - {comments && comments.length === 0 && ( -

- No comments yet. -

- )} + {comments && + commits && + comments.length === 0 && + commits.length === 0 && ( +

+ No activity yet. +

+ )} - {comments && comments.length > 0 && ( -
- {comments.map((comment, i) => ( -
-
- {comment.author ? ( - {comment.author.login} - ) : ( -
- )} - - {comment.author?.login ?? "Unknown"} - - - {formatRelativeTime(comment.createdAt)} - -
- - {comment.body} - -
- ))} -
- )} + {/* Status card */} {!pr.isMerged && pr.state !== "closed" && ( @@ -384,7 +364,11 @@ function PullDetailPage() { {/* Participants */} - + {/* Details */} @@ -473,9 +457,11 @@ function DetailRow({ function ParticipantsList({ pr, comments, + commits, }: { pr: PullDetail; comments: Array<{ author: GitHubActor | null }>; + commits: Array<{ author: GitHubActor | null }>; }) { const seen = new Set(); const participants: GitHubActor[] = []; @@ -491,6 +477,9 @@ function ParticipantsList({ for (const comment of comments) { addActor(comment.author); } + for (const commit of commits) { + addActor(commit.author); + } if (participants.length === 0) { return

No participants yet

; @@ -891,6 +880,114 @@ function UpdateBranchButton({ ); } +type TimelineItem = + | { type: "comment"; date: string; data: PullComment } + | { type: "commit"; date: string; data: PullCommit }; + +function ActivityTimeline({ + comments, + commits, +}: { + comments: PullComment[]; + commits: PullCommit[]; +}) { + const items: TimelineItem[] = [ + ...comments.map((c) => ({ + type: "comment" as const, + date: c.createdAt, + data: c, + })), + ...commits.map((c) => ({ + type: "commit" as const, + date: c.createdAt, + data: c, + })), + ].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + + if (items.length === 0) return null; + + return ( +
+ {items.map((item, i) => { + const prevType = i > 0 ? items[i - 1].type : null; + const nextType = i < items.length - 1 ? items[i + 1].type : null; + const isConsecutiveCommit = + item.type === "commit" && prevType === "commit"; + const isLastInCommitRun = + item.type === "commit" && nextType !== "commit"; + + if (item.type === "comment") { + const comment = item.data; + return ( +
+
+ {comment.author ? ( + {comment.author.login} + ) : ( +
+ )} + + {comment.author?.login ?? "Unknown"} + + + {formatRelativeTime(comment.createdAt)} + +
+ + {comment.body} + +
+ ); + } + + const commit = item.data; + const firstLine = commit.message.split("\n")[0]; + return ( +
+
+ +
+ {commit.author ? ( + {commit.author.login} + ) : ( +
+ )} + {firstLine} + + {commit.sha.slice(0, 7)} + + + {formatRelativeTime(commit.createdAt)} + +
+ ); + })} +
+ ); +} + function CommentBox() { const [value, setValue] = useState("");