-
Notifications
You must be signed in to change notification settings - Fork 6k
feat(web): pull request files can be marked as viewed #7721
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
Merged
maria-rcks
merged 96 commits into
pingdotgg:main
from
TrogonStack:yordis/feat-pr-files-viewed-upstream
Sep 17, 2026
Merged
Changes from all commits
Commits
Show all changes
96 commits
Select commit
Hold shift + click to select a range
d2d5bba
feat(web): pull request files can be marked as viewed
yordis cf5ac4c
fix(server): an overpriced write guess no longer pauses reads until t…
yordis e62d906
fix(web): a failed press no longer takes back a tick the reader made …
yordis 19a679c
fix(web): a viewed tick redraws its file, and a partial count says th…
yordis 447fd19
style: plainer punctuation in the viewed files comments
yordis d085d08
fix(web): pressing the word beside the box no longer folds the file t…
yordis 4891bcd
fix(server): a write no longer guesses at a budget only the host can …
yordis dad689d
fix(web): a failed request no longer answers for a press a later one …
yordis 9628643
fix(web): a viewed tick no longer rebuilds every header on screen
yordis 6b44e51
fix(web): the viewed box says what it is for out loud
yordis 28eea17
Merge upstream main into yordis/feat-pr-files-viewed-upstream
yordis aa7a828
fix(web): a refreshed review re-asks for the ticks
yordis 5f08047
fix(web): a superseded write no longer reports a failure
yordis 3c279bf
fix(web): a dropped connection no longer reports a rejected write
yordis f5c7f62
feat(server): GitLab reviewers can keep their place in a long merge r…
yordis 53253a2
fix(web): a GitLab reader can tell whose viewed marks these are
yordis 1de680d
fix(web): Azure DevOps pull request links open in the app
yordis 0876415
feat(server): Bitbucket reviewers keep their place in a long review
yordis 9146a44
feat(server): Azure DevOps pull requests show their files
yordis bbf04bb
perf(server): a review's ticks stop waiting on the host
yordis cd7cccc
fix(server): a long or unreadable Azure change still renders its diff
yordis 75c0bb8
fix(server): a review's ticks survive what the host could not read
yordis a95bf64
fix(server): a part-read Azure change no longer passes as the whole o…
yordis 644839c
fix(server): a diff given up on no longer costs the whole slice again…
yordis b060805
fix(server): a rate-limited Azure diff no longer reads as a change wi…
yordis 6e9d61c
fix(web): the viewed count no longer pushes the code toolbar off the …
yordis 2db1bd7
Merge upstream main into yordis/feat-pr-files-viewed-upstream
yordis b938067
fix(server): correct viewed-file marks under partial answers and conc…
yordis d5652e0
Merge upstream main into yordis/feat-pr-files-viewed-upstream
yordis e8c53a0
fix(server): keep a viewed mark honest when the host answers short
yordis 6639577
fix(server): give a long review's own history room to arrive
yordis f21e8ee
Merge upstream main into yordis/feat-pr-files-viewed-upstream
yordis 051fa5a
fix(server): refuse a diff cursor Azure's reader never handed out
yordis 8378409
fix(server): key a GitLab blob by the path the host spelled
yordis 6ec0e31
fix(server): ask Azure for a file by its own spelling of the path
yordis a1dc7d4
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis aeafc27
fix(web): an Azure review's preview and its ticks agree on the reposi…
yordis ebd14e0
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis d622f4e
chore(server): stop exporting helpers only their own module reads
yordis 82d8bb6
docs(web): sentBy guards a repress while a request is out, not overla…
yordis f4abc08
perf(web): a tick or a fold no longer rehashes every annotation on th…
yordis 413cfc8
fix(web): the viewed checkbox announces the label a reader can see
yordis 814290e
fix(server): a bitbucket path holding a space no longer reads as view…
yordis 47b1409
perf(server): a run of ticks no longer downloads the bitbucket patch …
yordis 99c4662
perf(server): writing to a github pull request no longer re-reads its…
yordis 382eaee
fix(server): a signed-out cli no longer hides the files a reader mark…
yordis e33e684
fix(server): a workspace refresh re-asks the head what it has of the …
yordis 3ed4814
refactor(server): the pull request service no longer branches on a pr…
yordis 2c471a9
perf(server): an azure change request's files no longer wait on each …
yordis f4845a6
fix(server): reading an azure diff no longer holds every other client…
yordis 00fa358
docs: pull request comments carry the trap rather than restate the code
yordis 8575b70
docs(server): the gitlab revision doc no longer contradicts the code …
yordis 5712869
docs(server): the revision comments name the layer that turns an abse…
yordis d08e3f3
refactor(server): the viewed files feature leaves the pull request se…
yordis 71c8572
docs: the null revision rule has one home rather than nine copies tha…
yordis c65ba1a
docs(server): the bitbucket diff comment matches the punctuation this…
yordis 664237c
chore: merge upstream main into yordis/feat-pr-files-viewed-upstream
yordis bafd063
fix(server): diff bounds and viewed marks survive slow machines and p…
yordis 39d5e04
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis 900698c
fix(server): bound how many az processes the whole build has out at once
yordis 7383233
fix(server): bound what one change request can hold and spawn
yordis cdc24c6
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis cf362b8
fix(server): keep what a reader has open out of reach of cold reads
yordis fac056b
test(server): cover what a live azure organisation answers with
yordis 87aa7cc
fix: keep a viewed mark on the file the host named
yordis 05110fd
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis c1a945f
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis 8addefb
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis 4c404a8
fix(server): hold background viewer lookups behind a rate-limit pause
yordis ecaab33
docs(user): say where viewed marks can be made
yordis 5a5b7bd
perf(server): keep the Azure summary read to one process
yordis e259c2f
refactor(server): cut the review prose out of the new server comments
yordis 63ea078
fix(web): a tick no longer hides a file pushed to since it landed
yordis 9ec8fd4
docs(internals): the revision guide agrees with itself on a failed press
yordis e92c9f6
test(web): the case where a tick hid a push is guarded through the hook
yordis 8f68fbf
perf(server): stop re-reading a bitbucket pull request's patch on eve…
yordis 69e684d
docs(server): thin out the comments around viewed files
yordis c98d31b
test(server): pin the viewer lookup a paused host is not asked to repeat
yordis c835567
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis aa11cbc
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis 0d64740
test(mobile): a cold highlighter is not held to a warm one's tokens
yordis 042e3c6
fix(server): a viewed mark on a wide review stops reporting changes
yordis 0999e68
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis e3ce126
revert(mobile): drop the highlighter test rewrite from this branch
yordis e76dd3a
Merge remote-tracking branch 'upstream/main' into yordis/feat-pr-file…
yordis ac0da07
fix(web): preserve both imports for main integration
maria-rcks 4695b9a
fix(web): reconcile markdown changes with main
maria-rcks bcdb951
fix(review): align conflicting files before main integration
maria-rcks 9e6f370
Merge branch 'main' into yordis/feat-pr-files-viewed-upstream
maria-rcks 4dc9155
fix(review): preserve decoded paths after main integration
maria-rcks b4e5c21
fix(prs): preserve viewed behavior across settings and routed hosts
maria-rcks f28664a
fix(prs): skip linked sync for viewed-only invalidations
maria-rcks 7534968
fix(prs): integrate main and track Forgejo viewed files
maria-rcks 06c10cd
chore: resolve latest main settings conflicts
maria-rcks faf2751
Merge branch 'main' into yordis/feat-pr-files-viewed-upstream
maria-rcks c2b9144
fix(prs): isolate viewed marks by routed repository
maria-rcks File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
apps/server/src/persistence/Migrations/053_PullRequestFilesViewed.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import * as Effect from "effect/Effect"; | ||
| import * as SqlClient from "effect/unstable/sql/SqlClient"; | ||
|
|
||
| export default Effect.gen(function* () { | ||
| const sql = yield* SqlClient.SqlClient; | ||
|
|
||
| // One row per file a reader has cleared on a host that keeps no record of its own. `revision` | ||
| // is what the file was when it was cleared, so a push that changes it is reported as changed | ||
| // rather than silently left ticked, and it is nullable for the reason `PullRequestFileViewedMark` | ||
| // gives. Unticking deletes the row: absent is the resting state, and a table of "not viewed" | ||
| // rows would grow with every diff anybody scrolled past. | ||
| yield* sql` | ||
| CREATE TABLE IF NOT EXISTS pull_request_files_viewed ( | ||
| provider TEXT NOT NULL, | ||
| host TEXT NOT NULL, | ||
| repository TEXT NOT NULL, | ||
| number INTEGER NOT NULL, | ||
| viewer TEXT NOT NULL, | ||
| path TEXT NOT NULL, | ||
| revision TEXT, | ||
| viewed_at TEXT NOT NULL, | ||
| PRIMARY KEY (provider, host, repository, number, viewer, path) | ||
| ) WITHOUT ROWID | ||
| `; | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| import * as Context from "effect/Context"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Layer from "effect/Layer"; | ||
| import * as Schema from "effect/Schema"; | ||
| import * as SqlClient from "effect/unstable/sql/SqlClient"; | ||
| import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | ||
|
|
||
| import { SourceControlProviderKind } from "@t3tools/contracts"; | ||
|
|
||
| import { | ||
| PersistenceDecodeError, | ||
| PersistenceSqlError, | ||
| type PullRequestFilesViewedRepositoryError, | ||
| } from "./Errors.ts"; | ||
|
|
||
| /** | ||
| * Which change request, on which host, for which reader. The host is part of it because the same | ||
| * `group/project` exists on gitlab.com and on a self-managed instance, and the reader because | ||
| * signing in as somebody else must not inherit their ticks. A host that will not say who the | ||
| * reader is leaves it empty, which is one reader rather than none. | ||
| */ | ||
| export const PullRequestFilesViewedScope = Schema.Struct({ | ||
| provider: SourceControlProviderKind, | ||
| host: Schema.String, | ||
| repository: Schema.String, | ||
| number: Schema.Int, | ||
| viewer: Schema.String, | ||
| }); | ||
| export type PullRequestFilesViewedScope = typeof PullRequestFilesViewedScope.Type; | ||
|
|
||
| /** A file this reader cleared, and what it was when they cleared it. */ | ||
| export const PullRequestFileViewedMark = Schema.Struct({ | ||
| path: Schema.String, | ||
| /** | ||
| * The host's own name for that version of the file, opaque here. Empty where the host said it | ||
| * had none to give, which is an answer rather than a gap: a file with no version at the head is | ||
| * one the change request deletes. Null where the host could not say at all, which is no baseline | ||
| * rather than an empty one, and such a mark stays cleared until a press replaces it. | ||
| * | ||
| * This null is the only one this environment invents; the other two are in | ||
| * `docs/internals/pull-request-file-revisions.md`. | ||
| */ | ||
| revision: Schema.NullOr(Schema.String), | ||
| }); | ||
| export type PullRequestFileViewedMark = typeof PullRequestFileViewedMark.Type; | ||
|
|
||
| export interface SetPullRequestFilesViewedInput extends PullRequestFilesViewedScope { | ||
| readonly files: ReadonlyArray<PullRequestFileViewedMark & { readonly viewed: boolean }>; | ||
| /** When the presses landed, as an ISO instant. */ | ||
| readonly viewedAt: string; | ||
| } | ||
|
|
||
| /** | ||
| * How many marks one read of this store carries. Every one of them is a path held in a set and a | ||
| * map for as long as the caller holds the read, so an unbounded read of a change request with | ||
| * thousands of marks in it is paid for again per scope the caller is holding. Matched to what the | ||
| * GitHub reader walks in one go, past what anyone reviews in a sitting. | ||
| */ | ||
| export const MAX_FILES_VIEWED_ROWS = 500; | ||
|
|
||
| /** The marks for one scope, and whether the store had more of them than it carried. */ | ||
| export interface PullRequestFilesViewedPage { | ||
| readonly files: ReadonlyArray<PullRequestFileViewedMark>; | ||
| readonly truncated: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * The marks this environment keeps for hosts that keep none of their own. | ||
| * | ||
| * Only cleared files are rows. Unticking deletes rather than writing a "not viewed" row, so the | ||
| * table holds what a reader has done and not what they have merely scrolled past. | ||
| */ | ||
| export class PullRequestFilesViewedRepository extends Context.Service< | ||
| PullRequestFilesViewedRepository, | ||
| { | ||
| readonly list: ( | ||
| input: PullRequestFilesViewedScope, | ||
| ) => Effect.Effect<PullRequestFilesViewedPage, PullRequestFilesViewedRepositoryError>; | ||
| readonly set: ( | ||
| input: SetPullRequestFilesViewedInput, | ||
| ) => Effect.Effect<void, PullRequestFilesViewedRepositoryError>; | ||
| } | ||
| >()("t3/persistence/PullRequestFilesViewed/PullRequestFilesViewedRepository") {} | ||
|
|
||
| function toSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | ||
| return (cause: unknown): PullRequestFilesViewedRepositoryError => | ||
| Schema.isSchemaError(cause) | ||
| ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) | ||
| : new PersistenceSqlError({ operation: sqlOperation, cause }); | ||
| } | ||
|
|
||
| const make = Effect.gen(function* () { | ||
| const sql = yield* SqlClient.SqlClient; | ||
|
|
||
| const listRows = SqlSchema.findAll({ | ||
| Request: PullRequestFilesViewedScope, | ||
| Result: PullRequestFileViewedMark, | ||
| execute: ({ provider, host, repository, number, viewer }) => | ||
| sql` | ||
| SELECT | ||
| path AS "path", | ||
| revision AS "revision" | ||
| FROM pull_request_files_viewed | ||
| WHERE provider = ${provider} | ||
| AND host = ${host} | ||
| AND repository = ${repository} | ||
| AND number = ${number} | ||
| AND viewer = ${viewer} | ||
| ORDER BY path | ||
| LIMIT ${MAX_FILES_VIEWED_ROWS + 1} | ||
| `, | ||
| }); | ||
|
|
||
| return PullRequestFilesViewedRepository.of({ | ||
| // Ordered by path and read one row past the ceiling, so the same marks come back on every | ||
| // read rather than a window that shuffles, and having more than were carried is known rather | ||
| // than guessed at from a full page. | ||
| list: (input) => | ||
| listRows(input).pipe( | ||
| Effect.map((rows) => ({ | ||
| files: rows.slice(0, MAX_FILES_VIEWED_ROWS), | ||
| truncated: rows.length > MAX_FILES_VIEWED_ROWS, | ||
| })), | ||
| Effect.mapError(toSqlOrDecodeError("listPullRequestFilesViewed", "PullRequestFileViewed")), | ||
| ), | ||
|
|
||
| // One statement per file rather than one for the batch: the batch is what a reader ticked in | ||
| // the last few hundred milliseconds, so it is a handful of rows on a local database, and a | ||
| // mixed batch of clears and un-clears has no single statement anyway. | ||
| set: (input) => | ||
| // One transaction for the batch. A press is a handful of files, and a failure part way | ||
| // through would otherwise leave some of them cleared and the rest not, which the reader | ||
| // sees on the next read as marks they never made. | ||
| sql | ||
| .withTransaction( | ||
| Effect.forEach( | ||
| input.files, | ||
| (file) => | ||
| file.viewed | ||
| ? sql` | ||
| INSERT INTO pull_request_files_viewed ( | ||
| provider, | ||
| host, | ||
| repository, | ||
| number, | ||
| viewer, | ||
| path, | ||
| revision, | ||
| viewed_at | ||
| ) | ||
| VALUES ( | ||
| ${input.provider}, | ||
| ${input.host}, | ||
| ${input.repository}, | ||
| ${input.number}, | ||
| ${input.viewer}, | ||
| ${file.path}, | ||
| ${file.revision}, | ||
| ${input.viewedAt} | ||
| ) | ||
| ON CONFLICT (provider, host, repository, number, viewer, path) | ||
| DO UPDATE SET revision = excluded.revision, viewed_at = excluded.viewed_at | ||
| ` | ||
| : sql` | ||
| DELETE FROM pull_request_files_viewed | ||
| WHERE provider = ${input.provider} | ||
| AND host = ${input.host} | ||
| AND repository = ${input.repository} | ||
| AND number = ${input.number} | ||
| AND viewer = ${input.viewer} | ||
| AND path = ${file.path} | ||
| `, | ||
| { discard: true }, | ||
| ), | ||
| ) | ||
| .pipe( | ||
| Effect.mapError( | ||
| (cause) => new PersistenceSqlError({ operation: "setPullRequestFilesViewed", cause }), | ||
| ), | ||
| ), | ||
| }); | ||
| }); | ||
|
|
||
| export const layer = Layer.effect(PullRequestFilesViewedRepository, make); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.