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
2 changes: 1 addition & 1 deletion docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ kept in the server's secret store, applies right away, and is used before `CAFE_
sent to `git.cafe`; `CAFE_TOKEN` follows `CAFE_HOST` if you set it. Repositories on
`staging.git.cafe` need their own login with `--host https://staging.git.cafe/api`.

Line comments, reviewer requests, and labels are not available for GitCafe pull requests yet.
Line comments are not available for GitCafe pull requests yet.
Merging a stack lands it through GitCafe, and updating a branch restacks the layers above it.

## Start, clone, or publish a project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ function fakeGitCafe(
const response =
route === undefined
? new Response(encodeJson({ type: "https://cafe.sh/errors/not-found" }), { status: 404 })
: new Response(encodeJson(typeof route === "function" ? route(entry) : route), {
status: options.status ?? 200,
});
: route instanceof Response
? route
: new Response(encodeJson(typeof route === "function" ? route(entry) : route), {
status: options.status ?? 200,
});
return Effect.succeed(HttpClientResponse.fromWeb(request, response));
},
);
Expand Down Expand Up @@ -104,6 +106,10 @@ const pull = {
sourceRepo: null,
capabilities: { comment: true, review: true, merge: true, edit: true, moderate: true },
};
const labels = [
{ id: "label_bug", name: "bug", color: "#d73a4a", description: null },
{ id: "label_docs", name: "docs", color: "#0075ca", description: "Documentation" },
];
const root = {
id: "root",
threadId: "root",
Expand Down Expand Up @@ -439,16 +445,18 @@ describe("GitCafePullRequestProvider", () => {
},
);

it.effect("reads line counts, open merge strategies and requested reviewers", () => {
it.effect("reads line counts, open merge strategies, reviewers and labels", () => {
const snapshot = { version: 4, headOid, comparisonBaseOid: baseOid };
const files = (from: number, count: number) =>
Array.from({ length: count }, (_, index) => ({ path: `f${from + index}.ts` }));
let changePage = 0;
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": { ...pull, reviewers: [{ actor }] },
"GET /repos/owner/repo/pulls/7": pull,
"GET /repos/owner/repo/pulls/7/reviewers": { items: [{ id: "act_one", actor }], next: null },
"GET /repos/owner/repo/pulls/7/labels": { items: [labels[0]], next: null },
"GET /repos/owner/repo/pulls/7/status": {
merge: {
conflicts: "unknown",
conflicts: "none",
fastForward: true,
strategies: ["merge", "squash", "rebase"],
blockers: [{ blockedStrategies: ["squash"] }, {}],
Expand Down Expand Up @@ -483,7 +491,12 @@ describe("GitCafePullRequestProvider", () => {
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const detail = yield* provider.getChangeRequest(target);
assert.deepInclude(detail, { additions: 6, deletions: 3, changedFiles: 3 });
assert.deepInclude(detail, {
additions: 6,
deletions: 3,
changedFiles: 3,
mergeability: "mergeable",
});
assert.strictEqual(
server.sent.filter((request) => request.path.endsWith("/changes"))[1]?.query.get("after"),
"cursor-1",
Expand All @@ -501,6 +514,143 @@ describe("GitCafePullRequestProvider", () => {
detail.reviewers.map((reviewer) => reviewer.login),
["alice"],
);
assert.deepStrictEqual(detail.reviewRequestLogins, ["alice"]);
assert.deepStrictEqual(detail.labels, [{ name: "bug", color: "#d73a4a" }]);
}).pipe(Effect.provide(server.layer));
});

it.effect("still reads a pull request whose reviewers the token may not see", () => {
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": { ...pull, headOid: null },
"GET /repos/owner/repo/pulls/7/reviewers": new Response(
encodeJson({ type: "https://cafe.sh/errors/forbidden" }),
{ status: 403 },
),
"GET /repos/owner/repo/pulls/7/labels": { items: [labels[0]], next: null },
"GET /repos/owner/repo/pulls/7/status": {
merge: { conflicts: "unknown", fastForward: null, strategies: [] },
checks: { pending: 0, failing: 0, total: 0 },
},
});
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const detail = yield* provider.getChangeRequest(target);
assert.deepStrictEqual(detail.reviewers, []);
assert.deepStrictEqual(detail.labels, [{ name: "bug", color: "#d73a4a" }]);
}).pipe(Effect.provide(server.layer));
});

it.effect("fails the detail rather than hide reviewers GitCafe could not serve", () => {
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": { ...pull, headOid: null },
"GET /repos/owner/repo/pulls/7/reviewers": new Response("", { status: 503 }),
"GET /repos/owner/repo/pulls/7/labels": { items: [], next: null },
"GET /repos/owner/repo/pulls/7/status": {
merge: { conflicts: "unknown", fastForward: null, strategies: [] },
checks: { pending: 0, failing: 0, total: 0 },
},
});
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const result = yield* provider.getChangeRequest(target).pipe(Effect.result);
assert.strictEqual(result._tag, "Failure");
}).pipe(Effect.provide(server.layer));
});

it.effect("requests and withdraws reviewers by rewriting the whole list", () => {
const bob = { ...actor, actorId: "act_two", handle: "bob" };
let requested = [{ id: "act_two", actor: bob }];
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": pull,
"GET /repos/owner/repo/pulls/7/reviewer-candidates": {
items: [
{ id: "act_one", actor },
{ id: "act_two", actor: bob },
{ id: "act_bot", actor: { ...actor, actorId: "act_bot", handle: "lint", bot: true } },
],
next: null,
},
"GET /repos/owner/repo/pulls/7/reviewers": () => ({ items: requested, next: null }),
"PUT /repos/owner/repo/pulls/7/reviewers": { version: 5, items: [] },
});
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const list = yield* provider.listReviewerCandidates(target);
// The author, alice, cannot review her own pull request.
assert.deepStrictEqual(
list.candidates.map((c) => [c.id, c.login, c.isRequested, c.isBot ?? false]),
[
["act_two", "bob", true, false],
["act_bot", "lint", false, true],
],
);

const set = (id: string, on: boolean) =>
provider.setReviewerRequest({
...target,
reviewers: [{ id, kind: "user" }],
requested: on,
});
yield* set("act_bot", true);
assert.deepStrictEqual(server.sent.at(-1)?.body, {
expectedVersion: 4,
actorIds: ["act_two", "act_bot"],
});
yield* set("act_two", false);
assert.deepStrictEqual(server.sent.at(-1)?.body, { expectedVersion: 4, actorIds: [] });

requested = [];
yield* set("act_two", false);
assert.strictEqual(server.writes().length, 2);
}).pipe(Effect.provide(server.layer));
});

it.effect("names the missing scopes when the token may not request reviewers", () => {
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": pull,
"GET /repos/owner/repo/pulls/7/reviewer-candidates": new Response("", { status: 403 }),
"GET /repos/owner/repo/pulls/7/reviewers": { items: [], next: null },
});
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const error = yield* Effect.flip(provider.listReviewerCandidates(target));
assert.match(error.detail, /pr:reviewers:write/u);
}).pipe(Effect.provide(server.layer));
});

it.effect("applies and removes labels by name through their ids", () => {
const server = fakeGitCafe({
"GET /repos/owner/repo/pulls/7": pull,
"GET /repos/owner/repo/labels": { items: labels, next: null },
"GET /repos/owner/repo/pulls/7/labels": { items: [labels[0]], next: null },
"PUT /repos/owner/repo/pulls/7/labels": { version: 5, items: [] },
});
return Effect.gen(function* () {
const provider = yield* GitCafePullRequestProvider.make;
const list = yield* provider.listLabelCandidates!(target);
assert.deepStrictEqual(
list.candidates.map((label) => [label.name, label.isApplied]),
[
["bug", true],
["docs", false],
],
);

yield* provider.setLabels!({ ...target, labels: ["docs"], applied: true });
assert.deepStrictEqual(server.sent.at(-1)?.body, {
expectedVersion: 4,
labelIds: ["label_bug", "label_docs"],
});
yield* provider.setLabels!({ ...target, labels: ["bug"], applied: false });
assert.deepStrictEqual(server.sent.at(-1)?.body, { expectedVersion: 4, labelIds: [] });

const missing = yield* provider.setLabels!({
...target,
labels: ["nope"],
applied: true,
}).pipe(Effect.result);
assert.strictEqual(missing._tag, "Failure");
assert.strictEqual(server.writes().length, 2);
}).pipe(Effect.provide(server.layer));
});

Expand Down
Loading
Loading