Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions dashboard/src/locales/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export const messages = {
'tree.path': 'Trees',
'tree.searchPlaceholder': 'Search by tree, branch or tag with a regex',
'treeCompare.backToDetails': 'Back to tree details',
'treeCompare.base': 'Base',
'treeCompare.breadcrumb': 'Compare',
'treeCompare.breakdownTitle': 'Changed results',
'treeCompare.change.appeared': 'Appeared',
Expand All @@ -425,10 +426,10 @@ export const messages = {
'treeCompare.change.unchanged': 'Unchanged',
'treeCompare.changed': 'Changed',
'treeCompare.description':
'Compare pass/fail counts between two revisions on the same tree and branch.',
'treeCompare.detail.missingSide': 'No result on this side',
'Compare pass/fail counts from a base revision to a target revision on the same tree and branch.',
'treeCompare.detail.missingSide': 'No result on this revision',
'treeCompare.drilldownHint':
'Individual builds, boots, and tests that changed between Side A and Side B.',
'Individual builds, boots, and tests that changed from the base revision to the target revision.',
'treeCompare.failures.change': 'Change',
'treeCompare.failures.configArch': 'Config / Arch',
'treeCompare.failures.pathArch': 'Path / Arch',
Expand All @@ -447,24 +448,23 @@ export const messages = {
'Select two different revisions to compare. This tree needs at least two commits with results.',
'treeCompare.openCompare': 'Compare revisions',
'treeCompare.selectRevision': 'Select a revision',
'treeCompare.sideA': 'Side A',
'treeCompare.sideB': 'Side B',
'treeCompare.statusPairFilter.active': 'Active status pair filters',
'treeCompare.statusPairFilter.add': 'Add',
'treeCompare.statusPairFilter.from': 'From',
'treeCompare.statusPairFilter.glossary':
'PASS — completed successfully{br}' +
'FAIL — completed with a failure{br}' +
'INCONCLUSIVE — no definitive pass or fail result{br}' +
'— — absent on this side',
'— — absent on this revision',
'treeCompare.statusPairFilter.remove': 'Remove {from} to {to} filter',
'treeCompare.statusPairFilter.select': 'Select status',
'treeCompare.statusPairFilter.to': 'To',
'treeCompare.suggestion.branchHead': 'Branch head',
'treeCompare.suggestion.previous': 'Previous commit',
'treeCompare.suggestion.swap': 'Swap sides',
'treeCompare.suggestion.swap': 'Swap base and target',
'treeCompare.suggestions': 'Suggestions',
'treeCompare.summaryTitle': 'Tree summary',
'treeCompare.target': 'Target',
'treeDetails.bootsHistory': 'Boots History',
'treeDetails.branch': 'Branch',
'treeDetails.buildsHistory': 'Builds History',
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/pages/TreeCompare/TreeCompareLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function TreeCompareLink({
<Link
to="/tree/$treeName/$branch/compare"
params={{ treeName, branch }}
search={{ hashA: hash, hashB: '', origin }}
search={{ hashA: '', hashB: hash, origin }}
state={s => s}
>
<GitCompareArrows className="mr-2 h-4 w-4" />
Expand Down
14 changes: 7 additions & 7 deletions dashboard/src/pages/TreeCompare/TreeComparePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
mapBootOrTestDiffRows,
mapBuildDiffRows,
readStoredStatusPairs,
resolveCompareHashes,
resolveStatusPairs,
serializeStatusPairs,
writeStoredStatusPairs,
Expand Down Expand Up @@ -86,11 +87,10 @@ const TreeComparePage = (): JSX.Element => {
[commitsQuery.data],
);

const resolvedHashA = hashA || revisions[0]?.hash || '';
const resolvedHashB =
hashB ||
revisions.find(revision => revision.hash !== resolvedHashA)?.hash ||
'';
const { hashA: resolvedHashA, hashB: resolvedHashB } = useMemo(
() => resolveCompareHashes(revisions, hashA, hashB),
[revisions, hashA, hashB],
);

const canCompare = Boolean(resolvedHashA && resolvedHashB);

Expand Down Expand Up @@ -315,7 +315,7 @@ const TreeComparePage = (): JSX.Element => {
params={{
treeName,
branch,
hash: resolvedHashA,
hash: resolvedHashB,
}}
state={s => s}
>
Expand Down Expand Up @@ -347,7 +347,7 @@ const TreeComparePage = (): JSX.Element => {
</div>
<Link
to="/tree/$treeName/$branch/$hash"
params={{ treeName, branch, hash: resolvedHashA }}
params={{ treeName, branch, hash: resolvedHashB }}
className="text-blue text-sm font-medium hover:underline"
state={s => s}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function SideColumn({
logData,
isLoading,
}: {
labelId: 'treeCompare.sideA' | 'treeCompare.sideB';
labelId: 'treeCompare.base' | 'treeCompare.target';
status: CompareItemStatus;
id: string | null;
logType: LogType;
Expand Down Expand Up @@ -154,15 +154,15 @@ export function CompareDetailSheet({
</div>
<div className="grid gap-6 lg:grid-cols-2">
<SideColumn
labelId="treeCompare.sideA"
labelId="treeCompare.base"
status={item.sideA}
id={item.idA}
logType={logType}
logData={logA.data}
isLoading={logA.isLoading}
/>
<SideColumn
labelId="treeCompare.sideB"
labelId="treeCompare.target"
status={item.sideB}
id={item.idB}
logType={logType}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,15 +374,15 @@ export function CompareBuildsFailuresTable({
/>
<SortableHead
className="text-center"
intlKey="treeCompare.sideA"
intlKey="treeCompare.base"
sortKey="sideA"
sort={sort}
onSort={key => setSort(current => cycleSort(current, key))}
/>
<TableHead className="bg-light-gray w-8" />
<SortableHead
className="text-center"
intlKey="treeCompare.sideB"
intlKey="treeCompare.target"
sortKey="sideB"
sort={sort}
onSort={key => setSort(current => cycleSort(current, key))}
Expand Down Expand Up @@ -507,15 +507,15 @@ function PathHardwareTable({
/>
<SortableHead
className="text-center"
intlKey="treeCompare.sideA"
intlKey="treeCompare.base"
sortKey="sideA"
sort={sort}
onSort={key => setSort(current => cycleSort(current, key))}
/>
<TableHead className="bg-light-gray w-8" />
<SortableHead
className="text-center"
intlKey="treeCompare.sideB"
intlKey="treeCompare.target"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are renaming from sideA/sideB, I believe we could use nomenclature closer to what github does with base/compare, instead of base/target

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we are already changing nomenclature, might be worth to change url params as well.
I would go with the proposal from @mentonin of using two dots ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those comments are not blockers. But suggestions.

sortKey="sideB"
sort={sort}
onSort={key => setSort(current => cycleSort(current, key))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ function RevisionCard({
side === 'A' ? 'bg-blue' : 'bg-dim-gray',
)}
>
{side}
{side === 'A' ? 'B' : 'T'}
</span>
<span className="text-dim-black text-sm font-semibold">
<FormattedMessage
id={side === 'A' ? 'treeCompare.sideA' : 'treeCompare.sideB'}
id={side === 'A' ? 'treeCompare.base' : 'treeCompare.target'}
/>
</span>
</div>
Expand Down Expand Up @@ -180,7 +180,7 @@ export function RevisionSelectorBar({
type="button"
onClick={onSwap}
className="bg-medium-gray flex h-10 w-10 items-center justify-center rounded-full"
aria-label="Swap sides"
aria-label="Swap base and target"
>
<ArrowLeftRight className="text-dim-gray h-5 w-5" />
<span className="sr-only">
Expand Down
38 changes: 38 additions & 0 deletions dashboard/src/utils/treeCompareDiff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
mapBuildDiffRows,
compareRowNav,
parseStatusPairs,
resolveCompareHashes,
resolveStatusPairs,
serializeStatusPairs,
toggleChangeTypePairs,
Expand Down Expand Up @@ -44,6 +45,43 @@ describe('deriveCompareChange', () => {
});
});

describe('resolveCompareHashes', () => {
const revisions = [{ hash: 'new' }, { hash: 'mid' }, { hash: 'old' }];

it('defaults to the head on B and the revision before it on A', () => {
expect(resolveCompareHashes(revisions, '', '')).toEqual({
hashA: 'mid',
hashB: 'new',
});
});

it('keeps A one revision older than an explicit B', () => {
expect(resolveCompareHashes(revisions, '', 'mid')).toEqual({
hashA: 'old',
hashB: 'mid',
});
});

it('never defaults B to the same revision as A', () => {
expect(resolveCompareHashes(revisions, 'new', '')).toEqual({
hashA: 'new',
hashB: 'mid',
});
});

it('leaves explicit hashes alone and copes with too few revisions', () => {
expect(resolveCompareHashes(revisions, 'old', 'new')).toEqual({
hashA: 'old',
hashB: 'new',
});
expect(resolveCompareHashes([{ hash: 'only' }], '', '')).toEqual({
hashA: '',
hashB: 'only',
});
expect(resolveCompareHashes([], '', '')).toEqual({ hashA: '', hashB: '' });
});
});

describe('applyStatusPairFilter', () => {
const rows = [
{ id: '1', sideA: 'PASS' as const, sideB: 'FAIL' as const },
Expand Down
21 changes: 20 additions & 1 deletion dashboard/src/utils/treeCompareDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function apiStatusToItemStatus(
return status;
}

/** Mirror backend _CHANGE_COUNT_SELECT categories for A→B transitions. */
/** Mirror backend _CHANGE_COUNT_SELECT categories for base→target transitions. */
export function deriveCompareChange(
statusA: CompareItemStatus,
statusB: CompareItemStatus,
Expand Down Expand Up @@ -300,6 +300,25 @@ export function applyStatusPairFilter<
);
}

/**
* Fill in missing sides so the default comparison runs oldest → newest:
* B defaults to the branch head, A to the revision right before B.
* `revisions` is newest-first.
*/
export function resolveCompareHashes(
revisions: readonly { hash: string }[],
hashA: string,
hashB: string,
): { hashA: string; hashB: string } {
const resolvedB =
hashB || revisions.find(revision => revision.hash !== hashA)?.hash || '';
const indexB = revisions.findIndex(revision => revision.hash === resolvedB);
return {
hashA: hashA || revisions[indexB + 1]?.hash || '',
hashB: resolvedB,
};
}

/** Next/prev over the currently visible (searched/sorted) table rows. */
export function compareRowNav<T extends { id: string }>(
rows: T[],
Expand Down
12 changes: 6 additions & 6 deletions docs/tree-compare.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ High-level overview of the Tree Compare feature: side-by-side comparison of buil

## Purpose

Given a tree name and branch, pick two commit hashes (side A and side B) and answer:
Given a tree name and branch, pick two commit hashes (base and target) and answer:

- How did overall pass/fail/inconclusive counts move from A → B?
- How did overall pass/fail/inconclusive counts move from base → target?
- Which individual builds / boots / tests changed category (regressions, fixes, new failures, etc.)?

Entry point: **Compare revisions** on Tree Details (`TreeCompareLink`), which opens:
Expand All @@ -15,14 +15,14 @@ Entry point: **Compare revisions** on Tree Details (`TreeCompareLink`), which op

## User flow

1. Open compare from Tree Details (current revision pre-fills as side A).
2. Choose / swap revisions via the revision selector (commit history + shortcuts: previous commit, branch head, swap sides).
1. Open compare from Tree Details (current revision pre-fills as **Target**, **Base** defaults to the revision before it, so base → target reads oldest → newest). URL params stay `hashA` (base) and `hashB` (target).
2. Choose / swap revisions via the revision selector (commit history + shortcuts: previous commit, branch head, swap base and target).
3. Read the summary matrix (fixes, regressions, pass/fail/other counts per builds / boots / tests).
4. Drill into **Changed results** tabs (Builds / Boots / Tests). Quick change-type chips add or remove their status pairs; custom From/To pairs can be added too (default: `PASS → FAIL` and `FAIL → PASS`). Last edited pairs are remembered in localStorage when the URL omits `statusPair`.

URL search state owns: `hashA`, `hashB`, `origin`, `currentPageTab`, and optional `statusPair`.

## Change categories (A → B)
## Change categories (base → target)

Statuses are grouped into **PASS**, **FAIL**, and **INCONCLUSIVE** (everything else, including null). Absent on one side is treated as missing (`null` / `—`).

Expand All @@ -34,7 +34,7 @@ Statuses are grouped into **PASS**, **FAIL**, and **INCONCLUSIVE** (everything e
| `stillFailing` | FAIL → FAIL |
| `newPass` | missing/INCONCLUSIVE → PASS |
| `appeared` | missing → INCONCLUSIVE |
| `disappeared` | present on A, missing on B |
| `disappeared` | present on base, missing on target |
| `unchanged` | same status on both sides (except FAIL → FAIL); frontend-only, has no backend count and no filter chip |

Backend SQL aggregates (`_CHANGE_COUNT_SELECT` in `queries/tree.py`) and the frontend `deriveCompareChange` helper must stay in sync.
Expand Down
Loading