Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ff8cbc9
fix(web): keep pull request review annotations in the app font
Bil0000 Aug 10, 2026
8754813
feat(web): collapse pull request chrome on scroll in the right panel
Bil0000 Aug 10, 2026
58bc0ba
feat(web): hand pull request tasks to the open thread beside the panel
Bil0000 Aug 10, 2026
e9cbc08
feat(web): show labels and resolved conversations on the pull request…
Bil0000 Aug 10, 2026
4095917
fix(web): report host line counts for files with withheld hunks
Bil0000 Aug 10, 2026
b27a2dd
fix(web): hide the tab strip scrollbar in narrow pull request panels
Bil0000 Aug 10, 2026
d06636c
fix(web): compare withheld-hunk counts when reusing a diff slice
Bil0000 Aug 10, 2026
c4e4edc
feat(web): collapse resolved conversations on the pull request summary
Bil0000 Aug 10, 2026
2e832b7
Merge branch 'main' into fix/pr-right-panel-quick-fixes
Bil0000 Aug 10, 2026
9b66581
fix(web): keep the reader at the top when the pull request chrome reo…
Bil0000 Aug 10, 2026
4132aae
fix(web): match the resolved marker to the other review states
Bil0000 Aug 10, 2026
f47c443
fix(web): ease the pull request chrome back open at the top
Bil0000 Aug 10, 2026
8a4a972
Merge branch 'main' into fix/pr-right-panel-quick-fixes
Bil0000 Aug 10, 2026
a7c4e2f
fix(web): tidy pull request comment meta lines
Bil0000 Aug 10, 2026
3abab8f
feat(web): collapse dismissed approvals with their reason
Bil0000 Aug 10, 2026
8e231dd
fix(web): carry the dismissal reason onto bodiless dismissed reviews
Bil0000 Aug 10, 2026
a6ccf72
fix(web): read dismissed-review bodies past their HTML marker comments
Bil0000 Aug 10, 2026
e87d5b9
fix(web): follow dismissal events past the first page
Bil0000 Aug 10, 2026
aa1a05a
Merge branch 'main' into fix/pr-right-panel-quick-fixes
Bil0000 Aug 11, 2026
f59c9a2
feat(web): add review decision and list filters to the pull request c…
Bil0000 Aug 11, 2026
95a9ed9
feat(web): narrow GitHub listings by draft, review, checks and size
Bil0000 Aug 11, 2026
97dc528
feat(web): filter the pull request list and show each row's review st…
Bil0000 Aug 11, 2026
1c4c08f
feat(web): order pull request diffs by dependency instead of path
Bil0000 Aug 11, 2026
ab14ec1
feat(web): take search qualifiers and wider review filters in the lis…
Bil0000 Aug 11, 2026
681dfc1
feat(web): parse GitHub-style search qualifiers and rework the filter…
Bil0000 Aug 11, 2026
1d446a5
feat(web): merge pull request listings across connected environments
Bil0000 Aug 11, 2026
aae512b
feat(web): list every connected environment's pull requests on the page
Bil0000 Aug 11, 2026
91f6c9c
fix(web): take a filtered empty search as the answer it is
Bil0000 Aug 11, 2026
59799af
fix(web): filter provider rows service-side and hand Azure the reposi…
Bil0000 Aug 11, 2026
3a4f155
fix(web): keep viewers, truncation and project scope per environment
Bil0000 Aug 11, 2026
7a95a28
feat(web): add a server filter and per-environment paging to the pull…
Bil0000 Aug 11, 2026
9c531cc
fix(web): route pull request links, sidebar and panel tabs across ser…
Bil0000 Aug 11, 2026
36b3238
Merge branch 'main' into fix/pr-right-panel-quick-fixes
Bil0000 Aug 11, 2026
6135637
feat(web): report base-branch freshness and take an update-branch action
Bil0000 Aug 11, 2026
cbbb842
feat(web): offer Update branch with merge or rebase in the detail panel
Bil0000 Aug 11, 2026
0636e8c
test(web): pin authored rows from the partition reads to their group
Bil0000 Aug 11, 2026
3b755c0
fix(web): repair the base comparison read and its write-time permissions
Bil0000 Aug 11, 2026
2976bed
fix(web): match the thread's own pull request by project and trust on…
Bil0000 Aug 11, 2026
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
3 changes: 3 additions & 0 deletions apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ function actionArgs(
return ["--draft", "true"];
case "close":
return ["--status", "abandoned"];
// Never reached: this host does not declare the action, so nothing offers it.
case "update-branch":
return [];
case "reopen":
return ["--status", "active"];
}
Expand Down
168 changes: 167 additions & 1 deletion apps/server/src/pullRequest/GitHubPullRequestCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChildProcessSpawner } from "effect/unstable/process";

import * as GitHubCli from "../sourceControl/GitHubCli.ts";
import * as GitHubPullRequestCli from "./GitHubPullRequestCli.ts";
import { BASE_COMPARISON_GRAPHQL_QUERY } from "./gitHubPullRequestJson.ts";

const mockedExecute = vi.fn<GitHubCli.GitHubCli["Service"]["execute"]>();

Expand Down Expand Up @@ -551,6 +552,85 @@ layer("GitHubPullRequestCli.layer", (it) => {
}),
);

it.effect("carries the further narrowings into the search as qualifiers", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(Effect.succeed(output("[]")));
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

yield* cli.listPullRequests({
cwd: "/w",
repository: "acme/web",
host: "github.com",
state: "open",
involvement: "all",
viewer: "bilal",
limit: 10,
filters: {
draft: "hide",
review: "changes-requested",
checks: "failing",
labels: ["needs design", 'quo"te'],
excludedLabels: ["wip"],
author: "octocat",
},
});

// Quotes around anything a reader typed, and the one character that could end a quoted
// value early dropped rather than escaped.
expect(searchOfCall(0)).toBe(
'label:"needs design" label:"quote" -label:"wip" author:"octocat" draft:false ' +
"review:changes_requested status:failure sort:updated-desc",
);
}),
);

it.effect("takes an empty filtered answer as an answer rather than falling back", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValueOnce(Effect.succeed(output("[]")));
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

const batch = yield* cli.listPullRequests({
cwd: "/w",
repository: "acme/web",
host: "github.com",
state: "open",
involvement: "all",
viewer: "bilal",
limit: 10,
filters: { draft: "hide", checks: "passing", excludedLabels: ["wip"], author: "OctoCat" },
});

// The filters were qualifiers on that very search, so nothing matching them exists. The
// search-free fallback is for a repository the index does not cover, and it could not
// judge `checks` at all — no listed row says anything about them.
assert.strictEqual(mockedExecute.mock.calls.length, 1);
assert.deepStrictEqual(batch.items, []);
}),
);

it.effect("carries the further narrowings into a batched search", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(Effect.succeed(searchPage([])));
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

yield* cli.searchPullRequests({
cwd: "/w",
host: "github.com",
repositories: ["acme/web"],
state: "open",
involvement: "all",
viewer: "bilal",
limit: 10,
filters: { draft: "only", review: "none", labels: ["bug"] },
});

assert.strictEqual(
searchQueryOfCall(0),
'is:pr is:open label:"bug" draft:true review:none sort:updated-desc repo:acme/web',
);
}),
);

it.effect("quotes a search, so it cannot add a qualifier or a flag of its own", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(Effect.succeed(output("[]")));
Expand Down Expand Up @@ -819,6 +899,40 @@ layer("GitHubPullRequestCli.layer", (it) => {
}),
);

it.effect("updates a stale branch with a merge commit unless asked to rebase", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(Effect.succeed(output("")));
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

yield* cli.runPullRequestAction({
cwd: "/w",
repository: "acme/web",
host: "github.com",
number: 7,
action: "update-branch",
});
// GitHub's own default, and `gh`'s: a merge commit unless the rebase flag says otherwise.
expect(callAt(0).args).toEqual(["pr", "update-branch", "7", "--repo", "github.com/acme/web"]);

yield* cli.runPullRequestAction({
cwd: "/w",
repository: "acme/web",
host: "github.com",
number: 7,
action: "update-branch",
updateMethod: "rebase",
});
expect(callAt(1).args).toEqual([
"pr",
"update-branch",
"7",
"--repo",
"github.com/acme/web",
"--rebase",
]);
}),
);

it.effect("merges with the strategy it was asked for", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(Effect.succeed(output("")));
Expand Down Expand Up @@ -1472,7 +1586,7 @@ layer("GitHubPullRequestCli.layer", (it) => {
expect(detail.body).toBe("Core body");
expect(activity.author?.login).toBe("octocat");
expect(callAt(0).args.at(-1)).toBe(
"number,title,url,author,headRefName,baseRefName,state,isDraft,mergeable,additions,deletions,createdAt,updatedAt,mergedAt,reviewRequests,labels,body,changedFiles,closedAt,statusCheckRollup",
"number,title,url,author,headRefName,baseRefName,state,isDraft,mergeable,reviewDecision,additions,deletions,createdAt,updatedAt,mergedAt,reviewRequests,labels,body,changedFiles,closedAt,statusCheckRollup,headRepositoryOwner",
);
expect(callAt(1).args.at(-1)).toBe("author,comments,reviews,commits");
}),
Expand Down Expand Up @@ -1634,6 +1748,58 @@ layer("GitHubPullRequestCli.layer", (it) => {
}),
);

it.effect("sends the base comparison's variables as gh flags, not as bare words", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(
Effect.succeed(
output(
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify({
data: {
repository: {
pullRequest: {
viewerCanUpdateBranch: true,
baseRef: { compare: { behindBy: 4 } },
},
},
},
}),
),
),
);
const cli = yield* GitHubPullRequestCli.GitHubPullRequestCli;

const comparison = yield* cli.getPullRequestBaseComparison({
cwd: "/w",
repository: "acme/web",
host: "github.com",
number: 7,
headRef: "fork:feat/page",
});

// The tuples are flattened straight into argv, so a variable without its flag is a
// positional argument gh refuses outright.
const args = callAt(0).args;
expect(args).toEqual([
"api",
"graphql",
"--hostname",
"github.com",
"-f",
"owner=acme",
"-f",
"name=web",
"-F",
"number=7",
"-f",
"headRef=fork:feat/page",
"-f",
`query=${BASE_COMPARISON_GRAPHQL_QUERY}`,
]);
expect(comparison).toEqual({ behindBy: 4, viewerCanUpdate: true });
}),
);

it.effect("reads the viewer's role off the same call as the merge settings", () =>
Effect.gen(function* () {
mockedExecute.mockReturnValue(
Expand Down
Loading
Loading