Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f973d6c
test(pull-requests): host-reported edit and resolve permissions
juliusmarminge Oct 10, 2026
9d1b45c
feat(pull-requests): hosts can report edit and resolve permissions pe…
juliusmarminge Oct 10, 2026
1174d5e
refactor(pull-requests): pass canEdit through; note both copies of a …
juliusmarminge Oct 10, 2026
bd9c6f3
test(pull-requests): a reported edit permission can't widen the host'…
juliusmarminge Oct 10, 2026
e457e05
test(source-control): pin GitCafe decoding, hosts, credentials and re…
juliusmarminge Oct 10, 2026
81f7822
feat(source-control): GitCafe lives in @t3tools/source-control-gitcafe
juliusmarminge Oct 10, 2026
a0adfca
test(source-control): fix GitCafe test layer order and reaction literal
juliusmarminge Oct 10, 2026
548d3ac
fix(source-control): GitCafe merge base, line stats, blockers, filter…
juliusmarminge Oct 10, 2026
1f44b67
fix(source-control): GitCafe never follows a redirect with the token
juliusmarminge Oct 10, 2026
5ff9278
fix(source-control): GitCafe listings and line counts degrade instead…
juliusmarminge Oct 10, 2026
afafada
fix(source-control): GitCafe comment permission, foreign URLs, bare n…
juliusmarminge Oct 10, 2026
2a3c09b
fix(source-control): GitCafe diff headers C-escape names; detail list…
juliusmarminge Oct 10, 2026
e66c875
fix(source-control): GitCafe publishes, reviews and merges against th…
juliusmarminge Oct 10, 2026
37f8188
fix(source-control): GitCafe stack layers carry their heads
juliusmarminge Oct 10, 2026
221a55f
fix(source-control): GitCafe reaction ids, checkout transport, bounde…
juliusmarminge Oct 10, 2026
9efc1a7
fix(source-control): GitCafe detail reads every page of checks
juliusmarminge Oct 10, 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
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@t3tools/source-control-bitbucket": "workspace:*",
"@t3tools/source-control-core": "workspace:*",
"@t3tools/source-control-forgejo": "workspace:*",
"@t3tools/source-control-gitcafe": "workspace:*",
"@t3tools/source-control-github": "workspace:*",
"@t3tools/source-control-gitlab": "workspace:*",
"effect": "catalog:",
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/sourceControl/SourceControlDiscovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/Git
import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli";
import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli";
import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli";
import * as GitCafeApi from "@t3tools/source-control-gitcafe/server/GitCafeApi";
import * as ForgejoSourceControlProvider from "@t3tools/source-control-forgejo/server/ForgejoSourceControlProvider";
import * as ForgejoPullRequestProvider from "@t3tools/source-control-forgejo/server/ForgejoPullRequestProvider";
import * as SourceControlDiscovery from "./SourceControlDiscovery.ts";
Expand Down Expand Up @@ -53,6 +54,7 @@ const layerSourceControlProviderRegistryTest = (input: {
Layer.mock(GitLabCli.GitLabCli)({}),
Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({}),
Layer.mock(ForgejoCli.ForgejoCli)({ listLogins: () => Effect.succeed([]) }),
Layer.mock(GitCafeApi.GitCafeApi)({}),
Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({}),
Layer.mock(VcsProcess.VcsProcess)(input.process),
ServerSourceControlHost.layer.pipe(
Expand Down Expand Up @@ -203,6 +205,12 @@ it.effect("reports implemented tools separately from locally available executabl
auth: "unknown",
account: Option.none(),
},
{
kind: "gitcafe",
status: "missing",
auth: "unknown",
account: Option.none(),
},
],
);
const bitbucket = result.sourceControlProviders.find((item) => item.kind === "bitbucket");
Expand Down Expand Up @@ -266,6 +274,13 @@ Logged in to gitlab.com as gitlab-user
) {
return Effect.succeed(processOutput("azure-user@example.com\n"));
}
if (input.command === "cafe" && input.args.slice(-3).join(" ") === "auth status --json") {
return Effect.succeed(
processOutput(
encodeJson({ schemaVersion: 1, data: { host: "git.cafe", username: "cafe-user" } }),
),
);
}
return Effect.fail(
new VcsProcessSpawnError({
operation: input.operation,
Expand Down Expand Up @@ -341,6 +356,12 @@ Logged in to gitlab.com as gitlab-user
account: Option.some("forgejo-user"),
detail: Option.none(),
},
{
kind: "gitcafe",
auth: "authenticated",
account: Option.some("cafe-user"),
detail: Option.none(),
},
],
);
}).pipe(Effect.provide(layerTest));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHub
import * as GitLabCli from "@t3tools/source-control-gitlab/server/GitLabCli";
import * as GitLabPullRequestCli from "@t3tools/source-control-gitlab/server/GitLabPullRequestCli";
import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli";
import * as GitCafeApi from "@t3tools/source-control-gitcafe/server/GitCafeApi";
import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
import * as ServerSourceControlHost from "./ServerSourceControlHost.ts";

Expand Down Expand Up @@ -126,6 +127,7 @@ function makeRegistry(input: {
Layer.mock(GitLabCli.GitLabCli)(input.gitlab ?? {}),
Layer.mock(GitLabPullRequestCli.GitLabPullRequestCli)({}),
Layer.mock(ForgejoCli.ForgejoCli)({ listLogins: () => Effect.succeed([]) }),
Layer.mock(GitCafeApi.GitCafeApi)({}),
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-source-control-registry-test-",
}).pipe(Layer.provide(NodeServices.layer)),
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/sourceControl/builtInDrivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import * as BitbucketPullRequestApi from "@t3tools/source-control-bitbucket/serv
import * as BitbucketDriver from "@t3tools/source-control-bitbucket/server/driver";
import * as ForgejoCli from "@t3tools/source-control-forgejo/server/ForgejoCli";
import * as ForgejoDriver from "@t3tools/source-control-forgejo/server/driver";
import * as GitCafeApi from "@t3tools/source-control-gitcafe/server/GitCafeApi";
import * as GitCafeCredentials from "@t3tools/source-control-gitcafe/server/GitCafeCredentials";
import * as GitCafeDriver from "@t3tools/source-control-gitcafe/server/driver";
import * as GitHubApi from "@t3tools/source-control-github/server/GitHubApi";
import * as GitHubPullRequestApi from "@t3tools/source-control-github/server/GitHubPullRequestApi";
import * as GitHubDriver from "@t3tools/source-control-github/server/driver";
Expand All @@ -32,6 +35,7 @@ const drivers = [
AzureDevOpsDriver.driver,
BitbucketDriver.driver,
ForgejoDriver.driver,
GitCafeDriver.driver,
];

/** Every service a built-in driver's `make` needs; the server's layers must provide them all. */
Expand All @@ -51,5 +55,6 @@ export const layer = Layer.mergeAll(
AzureDevOpsPullRequestCli.layer.pipe(Layer.provideMerge(AzureDevOpsCli.layer)),
BitbucketPullRequestApi.layer.pipe(Layer.provideMerge(BitbucketApi.layer)),
ForgejoCli.layer,
GitCafeApi.layer.pipe(Layer.provideMerge(GitCafeCredentials.layer)),
GitLabPullRequestCli.layer.pipe(Layer.provideMerge(GitLabCli.layer)),
).pipe(Layer.provideMerge(ServerSourceControlHost.layer));
20 changes: 18 additions & 2 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,14 @@ import {
CommandPaletteVirtualizedResults,
scrollCommandPaletteRowIntoView,
} from "./CommandPaletteResults";
import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon, ForgejoIcon } from "./Icons";
import {
AzureDevOpsIcon,
BitbucketIcon,
GitCafeIcon,
GitHubIcon,
GitLabIcon,
ForgejoIcon,
} from "./Icons";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
import { Checkbox } from "./ui/checkbox";
import { ProjectFavicon } from "./ProjectFavicon";
Expand Down Expand Up @@ -252,7 +259,7 @@ interface AddProjectEnvironmentOption {

type AddProjectRemoteProviderKind = Extract<
SourceControlProviderKind,
"github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops"
"github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe"
>;
type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url";

Expand All @@ -278,13 +285,15 @@ const REMOTE_PROJECT_SOURCES: ReadonlyArray<AddProjectRemoteSource> = [
"forgejo",
"bitbucket",
"azure-devops",
"gitcafe",
];
const REMOTE_PROJECT_PROVIDER_SOURCES: ReadonlyArray<AddProjectRemoteProviderKind> = [
"github",
"gitlab",
"forgejo",
"bitbucket",
"azure-devops",
"gitcafe",
];

function remoteProjectSourceLabel(source: AddProjectRemoteSource): string {
Expand All @@ -299,6 +308,8 @@ function remoteProjectSourceLabel(source: AddProjectRemoteSource): string {
return "Bitbucket";
case "azure-devops":
return "Azure DevOps";
case "gitcafe":
return "GitCafe";
case "url":
return "Git URL";
}
Expand All @@ -308,6 +319,7 @@ function remoteProjectSourcePathHint(source: AddProjectRemoteSource): string {
switch (source) {
case "forgejo":
case "github":
case "gitcafe":
return "owner/repo";
case "gitlab":
return "group/project";
Expand Down Expand Up @@ -338,6 +350,8 @@ function remoteProjectSourceIcon(source: AddProjectRemoteSource, className: stri
return <BitbucketIcon className={className} />;
case "azure-devops":
return <AzureDevOpsIcon className={className} />;
case "gitcafe":
return <GitCafeIcon className={className} />;
case "url":
return <LinkIcon className={className} />;
}
Expand Down Expand Up @@ -392,6 +406,7 @@ function buildAddProjectRemoteSourceReadiness(
forgejo: unavailable,
bitbucket: unavailable,
"azure-devops": unavailable,
gitcafe: unavailable,
};

if (!discovery) {
Expand Down Expand Up @@ -2093,6 +2108,7 @@ function OpenCommandPaletteDialog(props: {
"bitbucket",
"azure",
"devops",
"gitcafe",
"url",
"environment",
],
Expand Down
12 changes: 11 additions & 1 deletion apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
GitHubIcon,
GitLabIcon,
ForgejoIcon,
GitCafeIcon,
} from "~/components/Icons";
import { RadioGroup } from "~/components/ui/radio-group";
import { Spinner } from "~/components/ui/spinner";
Expand Down Expand Up @@ -158,7 +159,7 @@ interface PendingDefaultBranchAction {

type PublishProviderKind = Extract<
SourceControlProviderKind,
"github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops"
"github" | "gitlab" | "forgejo" | "bitbucket" | "azure-devops" | "gitcafe"
>;

type GitActionToastId = ReturnType<typeof toastManager.add>;
Expand Down Expand Up @@ -239,6 +240,14 @@ const PUBLISH_PROVIDER_OPTIONS = [
pathPlaceholder: "project/repository",
Icon: AzureDevOpsIcon,
},
{
value: "gitcafe",
label: "GitCafe",
description: "git.cafe",
host: "git.cafe",
pathPlaceholder: "owner/repo",
Icon: GitCafeIcon,
},
] as const satisfies ReadonlyArray<{
readonly value: PublishProviderKind;
readonly label: string;
Expand Down Expand Up @@ -565,6 +574,7 @@ function PublishRepositoryDialog(props: PublishRepositoryDialogProps) {
forgejo: null,
bitbucket: null,
"azure-devops": null,
gitcafe: null,
};
for (const provider of sourceControlDiscovery.data?.sourceControlProviders ?? []) {
if (isPublishProviderKind(provider.kind)) {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,14 @@ export const ForgejoIcon: Icon = (props) => (
</svg>
);

// GitCafe's own mark, from https://git.cafe/favicon.svg, inset so the full-bleed square does not
// read heavier than the round and cut-out marks beside it.
export const GitCafeIcon: Icon = (props) => (
<svg viewBox="0 0 64 64" aria-hidden="true" {...props}>
<rect x="4.8" y="4.8" width="54.4" height="54.4" fill="#a78bfa" />
</svg>
);

// macOS System Settings > Privacy & Security pane marks, so the SnapShot setup
// step points at the same icon the user is about to look for.
export const MacScreenRecordingIcon: Icon = (props) => {
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/ThreadStatusIndicators.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,26 @@ describe("linked pull request snapshots", () => {
sourceControlProvider: { kind: "gitlab", name: "gitlab", baseUrl: "" },
});
});
it.each([
["https://git.cafe/owner/repo/pulls/42", "gitcafe"],
["https://staging.git.cafe/owner/repo/pulls/42", "gitcafe"],
["https://codeberg.org/owner/repo/pulls/42", "forgejo"],
["https://gitlab.example.com/acme/web/-/merge_requests/42", "gitlab"],
["https://github.com/owner/repo/pull/42", "github"],
])("classifies %s as %s", (url, kind) => {
const result = linkedPullRequestSnapshotStatus({
...link,
url,
snapshot: {
state: "open",
title: "Change",
headBranch: "feature",
baseBranch: "main",
isDraft: false,
updatedAt: null,
syncedAt: "2026-01-03T00:00:00Z",
},
});
expect(result?.sourceControlProvider.kind).toBe(kind);
});
});
23 changes: 14 additions & 9 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";

import { pullRequestDetailToVcsStatus } from "@t3tools/client-runtime/state/pull-requests";
import { detectSourceControlProviderFromRemoteUrl } from "@t3tools/shared/sourceControl";
import {
resolveEnvironmentMachineKind,
type EnvironmentId,
Expand Down Expand Up @@ -129,15 +130,19 @@ export function linkedPullRequestSnapshotStatus(
): LinkedThreadPullRequestStatus | null {
const snapshot = link.snapshot;
if (snapshot === null) return null;
const kind = link.url.includes("/-/merge_requests/")
? "gitlab"
: link.url.includes("/pullrequest/")
? "azure-devops"
: link.url.includes("/pull-requests/")
? "bitbucket"
: link.url.includes("/pulls/")
? "forgejo"
: "github";
// GitCafe's `/pulls/` routes look like Forgejo's, so its two hosts are told apart first.
const kind =
detectSourceControlProviderFromRemoteUrl(link.url)?.kind === "gitcafe"
? "gitcafe"
: link.url.includes("/-/merge_requests/")
? "gitlab"
: link.url.includes("/pullrequest/")
? "azure-devops"
: link.url.includes("/pull-requests/")
? "bitbucket"
: link.url.includes("/pulls/")
? "forgejo"
: "github";
return {
pr: {
number: link.number,
Expand Down
10 changes: 7 additions & 3 deletions apps/web/src/components/pullRequest/PullRequestCodeTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"
import { useTheme } from "~/hooks/useTheme";
import { areAllDiffFilesCollapsed } from "~/lib/diffCollapse";
import { pullRequestFindingKey, type PullRequestFinding } from "./pullRequestDetail.logic";
import { canEditPullRequestComment } from "./pullRequestEditing.logic";
import { canEditPullRequestComment, canResolvePullRequestThread } from "./pullRequestEditing.logic";
import { orderDiffFiles } from "./pullRequestFileOrder.logic";
import {
buildFileDiffRenderKey,
Expand Down Expand Up @@ -934,7 +934,7 @@ function PullRequestCodeTab({
thread={thread}
workspaceRoot={detail.workspaceRoot}
canReply={review.reply}
canResolve={review.resolve}
canResolve={review.resolve && canResolvePullRequestThread(detail, thread)}
canReact={detail.capabilities.reactions === true}
environmentId={environmentId}
reference={reference}
Expand Down Expand Up @@ -966,7 +966,11 @@ function PullRequestCodeTab({
}
// A conversation on a line is made of review comments, whatever the host filed them as.
canEditComment={(comment) =>
canEditPullRequestComment(detail, { author: comment.author, kind: "review-comment" })
canEditPullRequestComment(detail, {
author: comment.author,
kind: "review-comment",
canEdit: comment.canEdit,
})
}
onEditComment={(commentId, body) =>
runThreadCommand("The comment could not be saved", () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export function pullRequestCheckoutCommand(
: null;
case "azure-devops":
return `az repos pr checkout --id ${number}`;
case "gitcafe":
return `cafe pr checkout ${number}`;
case "bitbucket": {
if (
!headRepositoryNameWithOwner ||
Expand Down
Loading
Loading