feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
Mohamed Boudra
parent
04d1ebdce0
commit
a8ebd390fa
@@ -3,11 +3,34 @@ import { CheckoutPrStatusSchema } from "@getpaseo/protocol/messages";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
import { buildGitActions, type BuildGitActionsInput } from "./policy";
|
||||
import { deriveMergeCapability, type ForgeSpecificStatusFacts } from "./merge-capability";
|
||||
|
||||
function githubStatus(
|
||||
overrides: Partial<NonNullable<BuildGitActionsInput["pullRequestGithub"]>> = {},
|
||||
): NonNullable<BuildGitActionsInput["pullRequestGithub"]> {
|
||||
type GithubMergeFactsFixture = ForgeSpecificStatusFacts & {
|
||||
forge: "github";
|
||||
mergeStateStatus: string | null;
|
||||
autoMergeRequest: {
|
||||
enabledAt: string | null;
|
||||
mergeMethod: string | null;
|
||||
enabledBy: string | null;
|
||||
} | null;
|
||||
viewerCanEnableAutoMerge: boolean;
|
||||
viewerCanDisableAutoMerge: boolean;
|
||||
viewerCanMergeAsAdmin: boolean;
|
||||
viewerCanUpdateBranch: boolean;
|
||||
repository: {
|
||||
autoMergeAllowed: boolean;
|
||||
mergeCommitAllowed: boolean;
|
||||
squashMergeAllowed: boolean;
|
||||
rebaseMergeAllowed: boolean;
|
||||
viewerDefaultMergeMethod: string | null;
|
||||
};
|
||||
isMergeQueueEnabled: boolean;
|
||||
isInMergeQueue: boolean;
|
||||
};
|
||||
|
||||
function githubStatus(overrides: Partial<GithubMergeFactsFixture> = {}): GithubMergeFactsFixture {
|
||||
return {
|
||||
forge: "github",
|
||||
mergeStateStatus: "CLEAN",
|
||||
autoMergeRequest: null,
|
||||
viewerCanEnableAutoMerge: false,
|
||||
@@ -27,10 +50,17 @@ function githubStatus(
|
||||
};
|
||||
}
|
||||
|
||||
function createInput(overrides: Partial<BuildGitActionsInput> = {}): BuildGitActionsInput {
|
||||
function createInput(
|
||||
overrides: Partial<Omit<BuildGitActionsInput, "mergeCapability">> & {
|
||||
pullRequestGithub?: unknown;
|
||||
} = {},
|
||||
): BuildGitActionsInput {
|
||||
const { pullRequestGithub = null, ...rest } = overrides;
|
||||
return {
|
||||
isGit: true,
|
||||
githubFeaturesEnabled: true,
|
||||
forgeBrandLabel: "GitHub",
|
||||
forgeChangeRequestNoun: "PR",
|
||||
githubAutoMergeActionsEnabled: true,
|
||||
hasPullRequest: false,
|
||||
pullRequestUrl: null,
|
||||
@@ -38,7 +68,7 @@ function createInput(overrides: Partial<BuildGitActionsInput> = {}): BuildGitAct
|
||||
pullRequestIsDraft: false,
|
||||
pullRequestIsMerged: false,
|
||||
pullRequestMergeable: "UNKNOWN",
|
||||
pullRequestGithub: null,
|
||||
mergeCapability: deriveMergeCapability(pullRequestGithub),
|
||||
hasRemote: false,
|
||||
isPaseoOwnedWorktree: false,
|
||||
isOnBaseBranch: true,
|
||||
@@ -128,7 +158,7 @@ function createInput(overrides: Partial<BuildGitActionsInput> = {}): BuildGitAct
|
||||
handler: () => undefined,
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -344,6 +374,28 @@ describe("git-actions-policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("explains why pull-and-push is unavailable when there is nothing to pull first", () => {
|
||||
const actions = buildGitActions(
|
||||
createInput({ hasRemote: true, aheadOfOrigin: 1, behindOfOrigin: 0 }),
|
||||
);
|
||||
const action = actions.secondary.find((entry) => entry.id === "pull-and-push");
|
||||
|
||||
expect(action?.unavailableMessage).toBe(
|
||||
"Pull and push isn't available because there are no incoming changes to pull first",
|
||||
);
|
||||
});
|
||||
|
||||
it("explains why pull-and-push is unavailable when there is nothing to push after pulling", () => {
|
||||
const actions = buildGitActions(
|
||||
createInput({ hasRemote: true, aheadOfOrigin: 0, behindOfOrigin: 1 }),
|
||||
);
|
||||
const action = actions.secondary.find((entry) => entry.id === "pull-and-push");
|
||||
|
||||
expect(action?.unavailableMessage).toBe(
|
||||
"Pull and push isn't available because there is nothing new to send after pulling",
|
||||
);
|
||||
});
|
||||
|
||||
it("explains why pull-and-push is unavailable when there are uncommitted changes", () => {
|
||||
const actions = buildGitActions(
|
||||
createInput({
|
||||
@@ -507,6 +559,44 @@ describe("git-actions-policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the forge change-request noun in unavailable no-forge copy", () => {
|
||||
const createActions = buildGitActions(
|
||||
createInput({
|
||||
githubFeaturesEnabled: false,
|
||||
forgeBrandLabel: "GitLab",
|
||||
forgeChangeRequestNoun: "MR",
|
||||
hasRemote: true,
|
||||
isOnBaseBranch: false,
|
||||
aheadCount: 1,
|
||||
}),
|
||||
);
|
||||
const viewActions = buildGitActions(
|
||||
createInput({
|
||||
githubFeaturesEnabled: false,
|
||||
forgeBrandLabel: "GitLab",
|
||||
forgeChangeRequestNoun: "MR",
|
||||
hasRemote: true,
|
||||
isOnBaseBranch: false,
|
||||
hasPullRequest: true,
|
||||
pullRequestUrl: "https://gitlab.com/example/repo/-/merge_requests/1",
|
||||
}),
|
||||
);
|
||||
|
||||
const createPrAction = [...createActions.secondary, ...createActions.menu].find(
|
||||
(action) => action.id === "pr",
|
||||
);
|
||||
const viewPrAction = [viewActions.primary, ...viewActions.secondary, ...viewActions.menu].find(
|
||||
(action) => action?.id === "pr",
|
||||
);
|
||||
|
||||
expect(createPrAction?.unavailableMessage).toBe(
|
||||
"Create MR isn't available right now because GitLab isn't connected",
|
||||
);
|
||||
expect(viewPrAction?.unavailableMessage).toBe(
|
||||
"View MR isn't available right now because GitLab isn't connected",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses local merge when merge is the stored ship default", () => {
|
||||
const actions = buildGitActions(
|
||||
createInput({
|
||||
@@ -726,12 +816,12 @@ describe("git-actions-policy", () => {
|
||||
pullRequestIsDraft: oldDaemonStatus.isDraft,
|
||||
pullRequestIsMerged: oldDaemonStatus.isMerged,
|
||||
pullRequestMergeable: oldDaemonStatus.mergeable,
|
||||
pullRequestGithub: oldDaemonStatus.github,
|
||||
pullRequestGithub: oldDaemonStatus.forgeSpecific,
|
||||
shipDefault: "pr",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(oldDaemonStatus.github).toBeUndefined();
|
||||
expect(oldDaemonStatus.forgeSpecific).toBeUndefined();
|
||||
expect(actions.primary).toMatchObject({
|
||||
id: "merge-pr-squash",
|
||||
label: "Merge PR (squash)",
|
||||
|
||||
Reference in New Issue
Block a user