Skip to content
Merged
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
93 changes: 92 additions & 1 deletion apps/server/src/pullRequest/GiteaPullRequestApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ layer("GiteaPullRequestApi", (it) => {
});
expect(callAt(1).path).toContain("include_tracking=true");
expect(mockedRequest).toHaveBeenCalledTimes(2);
const api = yield* GiteaPullRequestApi.make;
yield* api.listPullRequests({ ...input, relationshipOnly: true, includeTracking: true });
expect(callAt(2).path).not.toContain("include_tracking");
expect(mockedRequest).toHaveBeenCalledTimes(3);
}),
);
it.effect("reconstructs auto-merge from the timeline when discovery is unavailable", () =>
Expand Down Expand Up @@ -1334,6 +1338,48 @@ layer("GiteaPullRequestApi", (it) => {
}),
);

it.effect.each([1, 4])(
"marks inline comments incomplete when page %i has no pagination evidence",
(pageCount) =>
Effect.gen(function* () {
mockedRequest.mockReturnValueOnce(
Effect.succeed(
response([
{ id: 21, body: "Review", state: "COMMENT", submitted_at: "2026-09-03T11:00:00Z" },
]),
),
);
for (let page = 1; page <= pageCount; page += 1) {
mockedRequest.mockReturnValueOnce(
Effect.succeed(
response(
Array.from({ length: 50 }, (_, index) => ({
id: page * 50 + index,
body: `Comment ${index + 1}`,
path: "src/a.ts",
position: index + 1,
created_at: "2026-09-03T11:01:00Z",
})),
page === pageCount ? {} : { "x-total-count": "201" },
),
),
);
}
const api = yield* GiteaPullRequestApi.make;
const result = yield* api.listReviews({
host: "forge.example.test",
repository: "acme/web",
number: 7,
});

assert.isTrue(result.truncated);
expect(result.comments.filter((comment) => comment.kind === "review-comment")).toHaveLength(
50 * pageCount,
);
expect(mockedRequest).toHaveBeenCalledTimes(1 + pageCount);
}),
);

it.effect("does not repeat an unpaginated native review-comment response at the page size", () =>
Effect.gen(function* () {
mockedRequest
Expand Down Expand Up @@ -1422,6 +1468,51 @@ layer("GiteaPullRequestApi", (it) => {
}),
);

it.effect("shares the raw inline-comment budget across reviews", () =>
Effect.gen(function* () {
mockedRequest
.mockReturnValueOnce(
Effect.succeed(
response([
{ id: 21, body: "First review", submitted_at: "2026-09-03T11:00:00Z" },
{ id: 22, body: "Second review", submitted_at: "2026-09-03T12:00:00Z" },
{ id: 23, body: "Third review", submitted_at: "2026-09-03T13:00:00Z" },
]),
),
)
.mockReturnValueOnce(
Effect.succeed(
response([
...Array.from({ length: 199 }, (_, index) => ({
id: index + 31,
body: `Comment ${index + 1}`,
path: "src/a.ts",
position: index + 1,
created_at: "2026-09-03T11:01:00Z",
})),
{ id: "malformed" },
]),
),
);
const api = yield* GiteaPullRequestApi.make;
const result = yield* api.listReviews({
host: "forge.example.test",
repository: "acme/web",
number: 7,
});

expect(
result.comments.filter((comment) => comment.kind === "review").map((comment) => comment.id),
).toEqual(["review:21", "review:22", "review:23"]);
expect(result.comments.filter((comment) => comment.kind === "review-comment")).toHaveLength(
199,
);
assert.isTrue(result.truncated);
expect(mockedRequest).toHaveBeenCalledTimes(2);
expect(callAt(1).path).toContain("/reviews/21/comments?");
}),
);

it.effect("follows pagination links when Gitea caps comment pages below the limit", () =>
Effect.gen(function* () {
mockedRequest
Expand Down Expand Up @@ -2249,7 +2340,7 @@ layer("GiteaPullRequestApi", (it) => {
number: 7,
}),
);
expect(callAt(2).path).toContain("page=2");
expect(callAt(2).path).toBe("/repos/acme/web/issues/7/timeline?page=2&limit=1");
}),
);

Expand Down
21 changes: 17 additions & 4 deletions apps/server/src/pullRequest/GiteaPullRequestApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,13 +1079,20 @@ export const make = Effect.gen(function* () {
nextLink(result.headers) !== null || totalCount(result.headers) !== null;
const paginationNext =
input.requirePaginationEvidence && !hasPaginationEvidence ? null : next;
// Native unpaginated endpoints can return more than the requested page size. Exactly
// one requested page is ambiguous when headers do not establish whether more rows exist.
const paginationUncertain =
input.requirePaginationEvidence === true &&
!hasPaginationEvidence &&
result.rows.length === PAGE_SIZE;
if (result.rows.length > remaining || rows.length >= input.limit) {
return {
rows,
truncated: result.rows.length > remaining || paginationNext !== null,
truncated:
result.rows.length > remaining || paginationNext !== null || paginationUncertain,
};
}
if (paginationNext === null) return { rows, truncated: false };
if (paginationNext === null) return { rows, truncated: paginationUncertain };
path = paginationNext;
}
return { rows, truncated: true };
Expand All @@ -1109,7 +1116,7 @@ export const make = Effect.gen(function* () {
sort: relationshipOnly ? "oldest" : "recentupdate",
page,
limit: PAGE_SIZE,
include_tracking: input.includeTracking === true ? "true" : undefined,
include_tracking: !relationshipOnly && input.includeTracking === true ? "true" : undefined,
...(input.involvement === "authored" ? { poster: input.viewer } : {}),
});
let rowsSeen = 0;
Expand Down Expand Up @@ -1368,6 +1375,7 @@ export const make = Effect.gen(function* () {
const comments: Array<PullRequestComment> = [];
const threads: Array<PullRequestReviewThread> = [];
let commentsTruncated = reviewsTruncated;
let remainingReviewCommentRows = PAGE_SIZE * CONVERSATION_PAGES;
for (const row of reviewRows) {
const review = decodeReview(row);
if (Option.isNone(review)) continue;
Expand All @@ -1384,16 +1392,21 @@ export const make = Effect.gen(function* () {
reviewState: review.value.state?.toLowerCase().replaceAll("_", " ") ?? null,
});
}
if (remainingReviewCommentRows === 0) {
commentsTruncated = true;
continue;
}
const codeRows = yield* readUnknownSlice({
operation: "listReviewComments",
...input,
path: query(
`${basePath(input.repository)}/pulls/${input.number}/reviews/${review.value.id}/comments`,
{ page: 1, limit: PAGE_SIZE },
),
limit: PAGE_SIZE * CONVERSATION_PAGES,
limit: remainingReviewCommentRows,
requirePaginationEvidence: true,
});
remainingReviewCommentRows -= codeRows.rows.length;
commentsTruncated ||= codeRows.truncated;
const grouped = new Map<
string,
Expand Down
6 changes: 3 additions & 3 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ For Azure DevOps, use the host website to view diffs or change comments. Bitbuck
reopening a declined pull request.

Gitea supports PR tracking, comments, reviews, diffs, reviewer and label updates, merge methods,
branch updates, close/reopen, draft/ready changes, and auto-merge controls. Reactions, comment
editing, workflow approval, and revert PRs are not currently available in T3. Use your Gitea
website for those tasks.
branch updates, close/reopen, draft/ready changes, auto-merge controls, comment editing, and
reactions. Workflow approval and revert PRs are available when your Gitea server advertises
support for them.

## Troubleshooting

Expand Down