Skip to content

Mark which patch on a ticket is the latest, PR or attachment - #140

Merged
juanmaguitar merged 2 commits into
juanmaguitar/trac-attachmentsfrom
juanmaguitar/latest-patch
Aug 7, 2026
Merged

Mark which patch on a ticket is the latest, PR or attachment#140
juanmaguitar merged 2 commits into
juanmaguitar/trac-attachmentsfrom
juanmaguitar/latest-patch

Conversation

@juanmaguitar

Copy link
Copy Markdown
Collaborator

Part of #110. Refs #109, #11. Stacked on #139 (base is that branch), which stacks on #136#135#123. Review order: #123#135#136#139 → this.

Why

A contributor arriving at a ticket wants to try the latest fix. The panel listed PRs and Trac attachments as two separate sections, but nothing said which one — across both — was the most recent. Usually it's a PR (visible immediately); but sometimes the newest fix is a .diff uploaded to Trac, and that was buried under the attachments button with no signal.

What this does

  • The newest known patch carries a "Latest" pill, whether it's a PR row or a Trac-attachment row.
  • When the newest is an attachment, a note says so explicitly: "The most recent patch on this ticket is a file attachment, not a pull request."
  • Because attachments load on demand, "latest" is judged across what's loaded: PRs alone until the contributor opens attachments (the normal case), then both. Opening attachments isn't forced on every ticket — the verdict simply completes once they're looked at.

How

The comparison is a pure module (src/latest-patch.cjs), unit tested. A PR is dated by updatedAt, an attachment by its scraped upload time. A .txt never competes; a relative-only date ("15 months ago") can't win. The attachment timestamp is anchored to UTC by hand rather than parsed in the machine's local zone, so two contributors in different timezones see the same patch marked latest — a consistent answer is the whole point.

Testing

  • test/latest-patch.test.cjs — PR-wins (normal), attachment-wins-once-loaded (the case the feature exists for), .txt excluded, relative-date can't win, attachments-only, empty→null, and the UTC-determinism of the date parse (PM/AM and the 12-o'clock edge).
  • npm test / npm run test:electron: 238/238 both runtimes. npm run lint: clean.

Self-review (per AGENTS.md)

Ran the review with the judgement pass on fresh context. It returned 0 [fix here] · 1 [follow-up] — the follow-up was the timezone skew (PR date is UTC ISO, attachment date was parsed in local time), which two contributors could see differently. Fixed before this PR by anchoring the attachment parse to UTC, with a determinism test. The reviewer verified the key/equality logic (exactly one row marked, right row), NaN/.txt exclusion, null-safety, and stale-state reset on ticket switch.

🤖 Generated with Claude Code

Copilot AI left a comment

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.

Pull request overview

Marks the latest loaded patch across linked PRs and Trac attachments.

Changes:

  • Adds UTC-based patch-date comparison logic.
  • Displays “Latest” pills and an attachment notice.
  • Adds unit coverage for source and date edge cases.

Review: 2 [fix here] findings: long titles or filenames can clip the “Latest” pill.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/latest-patch.cjs Selects the latest applicable patch.
src/renderer/index.jsx Renders latest-patch indicators.
test/latest-patch.test.cjs Tests selection and timestamp parsing.
Suppressed comments (1)

src/renderer/index.jsx:2431

  • [UI · 🔵 low · fix here] The filename and pill share the same nowrap, overflow-hidden container, so a sufficiently long attachment filename hides the “Latest” pill. Keep the pill as a non-shrinking sibling and apply ellipsis only to the filename portion so the marker remains visible.
                          {latestPill(latestPatch?.kind === 'attachment' && latestPatch.key === att.url)}

Comment thread src/renderer/index.jsx
<div style={{ fontSize: 13, color: '#1d2327', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Button variant="link" onClick={() => window.api.openExternal(pr.url)} style={{ fontSize: 13 }}>#{pr.number}</Button>
{' '}{pr.title}
{latestPill(latestPatch?.kind === 'pr' && latestPatch.key === pr.number)}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b39037e. Each heading is now a flex row: an inner span ellipsizes the title (or filename) alone, and the pill sits beside it as a non-shrinking sibling, so it survives any title length. Same change applied to the attachment row at line 2431.

@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/latest-patch branch from 8df2335 to ce7f921 Compare August 7, 2026 07:58
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/latest-patch branch from ce7f921 to 65ec966 Compare August 7, 2026 09:15
A contributor arriving at a ticket wants to try the latest fix. The panel
listed PRs and Trac attachments as two separate sections, but nothing said
which one — across both — was the most recent. Usually it is a PR, and the PR
list is there immediately; but sometimes the newest fix is a .diff uploaded to
Trac, and that was buried under the attachments button with no signal.

Now the newest known patch carries a "Latest" pill, whether it is a PR row or
an attachment row, and when the newest is an attachment a note says so
explicitly — "the most recent patch on this ticket is a file attachment, not a
pull request". Because attachments load on demand, "latest" is judged across
what is loaded: PRs alone until the contributor opens attachments (the normal
case), then both. Opening the attachments cannot be forced on every ticket, so
the verdict simply completes once they are looked at.

The comparison lives in a pure module (src/latest-patch.cjs), unit tested. A PR
is dated by updatedAt, an attachment by its scraped upload time; a .txt never
competes, and a relative-only date ("15 months ago") cannot win. The attachment
timestamp is anchored to UTC by hand rather than parsed in the machine's local
zone, so two contributors in different timezones see the same patch marked
latest — the point of the feature is a consistent answer.

Refs #109, #11, part of #110.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/latest-patch branch from 65ec966 to b035ff0 Compare August 7, 2026 09:41
The pill shared one nowrap/overflow-hidden box with the PR title and the
attachment filename, so a long enough title ellipsized straight past it —
hiding the only per-row signal exactly on the rows most likely to have it.

Each heading is now a flex row: an inner span ellipsizes the title alone,
and the pill sits beside it as a non-shrinking sibling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar merged commit ae464a9 into trunk Aug 7, 2026
3 checks passed
juanmaguitar added a commit that referenced this pull request Aug 7, 2026
Part of #110. Refs #109, #11. **Stacked on #140** (base is that branch),
atop #139#136#135#123. From hands-on testing of the stack.

## Three fixes reported from testing

1. **The apply error persisted after Cancel.** Applying a PR that no
longer fits ("…has moved on since the patch was written") showed an
error; clicking Cancel dismissed the preview but left the error on
screen. Cancel now clears it too.
2. **The "Try someone else's patch" sub-copy contradicted itself during
a PR preview** — it said "Apply a `.diff`/`.patch` file…" while showing
"PR #4496 changes 1 file". The sub-copy is now hidden whenever a preview
or the apply chain is showing (the preview card speaks for itself), and
the idle copy names pull requests as well as files.
3. **The failure message ran two sentences together** ("…no longer
applies The checkout was not changed."). A period is inserted when the
reason doesn't already end in punctuation.

## One requested addition

**Apply a PR straight from a pasted URL or number**, without it having
to be linked to the ticket. A "Paste a pull request URL or number" input
+ "Apply PR" button in the panel's idle state.

`parsePrRef` (pure, `src/patch-sources.cjs`) accepts a wordpress-develop
PR URL or a bare number and rejects other repos, other hosts, issue URLs
and junk — including crafted `..` paths (URL normalisation collapses
them, then the repo/anchor check rejects). The number rides the
**existing** `previewPr → fetchPrDiff → preview → apply` flow, so a
pasted PR shares the same trust boundary as the linked-PR list:
`fetchPrDiff` hardcodes the repo and strips the number to digits, and
the diff flows through the apply engine's path-traversal defence.

## Testing

- `test/patch-sources.test.cjs` — `parsePrRef`: bare/`#`number, URL with
trailing `/files` and `#…`, scheme-less, wrong-repo rejection, and
issue/foreign-host/junk rejects.
- `npm test` / `npm run test:electron`: **242/242** both runtimes. `npm
run lint`: clean.

## Self-review (per AGENTS.md)

Judgement pass on fresh context: **0 findings** across the five
dimensions. It independently confirmed the `..`-traversal and
foreign-repo/host rejections, that the pasted-number path introduces no
new trust boundary (same `fetchPrDiff` + apply engine), that the
punctuation ternary can't throw (error is truthy-guarded), and that
hiding the sub-copy never strips the idle call-to-action.

Pending your visual confirm in the running app for the three UI fixes
and the paste-a-PR flow — these are your reported issues, verified here
by tests + review.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@juanmaguitar
juanmaguitar deleted the juanmaguitar/latest-patch branch August 11, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants