Repository navigation
feat(web): show how much of a PR is code vs tests, mocks, and generated files #22189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| import { toChangeBreakdown } from "./pullRequestChangeBreakdown.ts"; | ||
|
|
||
| describe("toChangeBreakdown", () => { | ||
| it("sums line counts per kind", () => { | ||
| expect( | ||
| toChangeBreakdown({ | ||
| truncated: false, | ||
| files: [ | ||
| { path: "src/a.ts", additions: 10, deletions: 2 }, | ||
| { path: "src/b.ts", additions: 5, deletions: 1 }, | ||
| { path: "src/a.test.ts", additions: 40, deletions: 0 }, | ||
| { path: "pnpm-lock.yaml", additions: 300, deletions: 120 }, | ||
| ], | ||
| }), | ||
| ).toEqual({ | ||
| code: { additions: 15, deletions: 3 }, | ||
| tests: { additions: 40, deletions: 0 }, | ||
| mocks: { additions: 0, deletions: 0 }, | ||
| generated: { additions: 300, deletions: 120 }, | ||
| }); | ||
| }); | ||
|
|
||
| it("gives no breakdown for a partial file list", () => { | ||
| expect( | ||
| toChangeBreakdown({ | ||
| truncated: true, | ||
| files: [{ path: "src/a.ts", additions: 1, deletions: 0 }], | ||
| }), | ||
| ).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { PullRequestChangeBreakdown } from "@t3tools/contracts"; | ||
| import { classifyChangedFile } from "@t3tools/shared/changedFileKind"; | ||
| import type { ProviderFileStats } from "@t3tools/source-control-core/server/PullRequestProvider"; | ||
|
|
||
| export function toChangeBreakdown(stats: ProviderFileStats): PullRequestChangeBreakdown { | ||
| if (stats.truncated) return null; | ||
| const empty = () => ({ additions: 0, deletions: 0 }); | ||
| const breakdown = { code: empty(), tests: empty(), mocks: empty(), generated: empty() }; | ||
| for (const file of stats.files) { | ||
| const bucket = breakdown[classifyChangedFile(file.path)]; | ||
| bucket.additions += file.additions; | ||
| bucket.deletions += file.deletions; | ||
| } | ||
| return breakdown; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { formatDiffCount } from "@t3tools/client-runtime/diff-count"; | ||
| import type { EnvironmentId, PullRequestChangeBreakdown, PullRequestRef } from "@t3tools/contracts"; | ||
| import { useState } from "react"; | ||
|
|
||
| import { pullRequestEnvironment } from "~/state/pullRequests"; | ||
| import { useEnvironmentQuery } from "~/state/query"; | ||
|
|
||
| import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | ||
| import { PullRequestDiffStat } from "./pullRequestPresentation"; | ||
|
|
||
| const KIND_LABELS = { | ||
| code: "Code", | ||
| tests: "Tests", | ||
| mocks: "Mocks", | ||
| generated: "Generated", | ||
| } satisfies Record<keyof NonNullable<PullRequestChangeBreakdown>, string>; | ||
|
|
||
| /** | ||
| * The pull request's diff stat, with a hover split into code, tests, mocks, and generated files. | ||
| * The split is read on first pointer or focus intent, so a page nobody hovers costs the host | ||
| * nothing. | ||
| */ | ||
| export function PullRequestChangeBreakdownStat({ | ||
| environmentId, | ||
| reference, | ||
| additions, | ||
| deletions, | ||
| className, | ||
| }: { | ||
| environmentId: EnvironmentId; | ||
| reference: PullRequestRef; | ||
| additions: number; | ||
| deletions: number; | ||
| className: string; | ||
| }) { | ||
| const [requested, setRequested] = useState(false); | ||
| const query = useEnvironmentQuery( | ||
| requested ? pullRequestEnvironment.changeBreakdown({ environmentId, input: reference }) : null, | ||
|
Comment on lines
+36
to
+38
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win Reset lazy loading when the pull request changes. After a user hovers one pull request, 🤖 Prompt for AI Agents |
||
| ); | ||
| const breakdown = query.data; | ||
| if (additions === 0 && deletions === 0) return null; | ||
| return ( | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| onPointerEnter={() => setRequested(true)} | ||
| onFocus={() => setRequested(true)} | ||
| render={ | ||
| <span className="inline-flex"> | ||
| <PullRequestDiffStat | ||
| additions={additions} | ||
| deletions={deletions} | ||
| className={className} | ||
| /> | ||
| </span> | ||
| } | ||
|
Comment on lines
+47
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Make the breakdown trigger keyboard-focusable.
🤖 Prompt for AI Agents |
||
| /> | ||
| {breakdown ? ( | ||
| <TooltipPopup side="bottom"> | ||
| <div className="grid grid-cols-[auto_auto_auto] gap-x-4 gap-y-1 py-0.5 font-mono tabular-nums"> | ||
| {(Object.keys(KIND_LABELS) as Array<keyof typeof KIND_LABELS>).map((kind) => { | ||
| const { additions, deletions } = breakdown[kind]; | ||
| return additions === 0 && deletions === 0 ? null : ( | ||
| <BreakdownRow | ||
| key={kind} | ||
| label={KIND_LABELS[kind]} | ||
| additions={additions} | ||
| deletions={deletions} | ||
| /> | ||
| ); | ||
| })} | ||
| <div className="col-span-3 border-t border-border" /> | ||
| <BreakdownRow label="Total" additions={additions} deletions={deletions} /> | ||
| </div> | ||
| </TooltipPopup> | ||
| ) : query.isPending ? ( | ||
| <TooltipPopup side="bottom">Loading breakdown</TooltipPopup> | ||
| ) : null} | ||
| </Tooltip> | ||
| ); | ||
| } | ||
|
|
||
| function BreakdownRow({ | ||
| label, | ||
| additions, | ||
| deletions, | ||
| }: { | ||
| label: string; | ||
| additions: number; | ||
| deletions: number; | ||
| }) { | ||
| return ( | ||
| <> | ||
| <span className="font-sans">{label}</span> | ||
| <span className="text-right text-diff-addition-foreground"> | ||
| +{formatDiffCount(additions)} | ||
| </span> | ||
| <span className="text-right text-diff-deletion">-{formatDiffCount(deletions)}</span> | ||
| </> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Allow the full mobile breakdown to display.
When all four categories have changes, the joined text can exceed two lines on a narrow screen.
numberOfLines={2}then hides later categories, including Generated. Remove the line limit or use a layout that keeps every nonzero category visible.🤖 Prompt for AI Agents