Skip to content
Open
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
68 changes: 68 additions & 0 deletions apps/server/src/git/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface FakeGhScenario {
headRepositoryNameWithOwner?: string | null;
headRepositoryOwnerLogin?: string | null;
};
issue?: GitHubCli.GitHubIssueSummary;
repositoryCloneUrls?: Record<string, { url: string; sshUrl: string }>;
failWith?: GitHubCli.GitHubCliError;
}
Expand Down Expand Up @@ -476,6 +477,23 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
});
}

if (args[0] === "issue" && args[1] === "view") {
return Effect.succeed(
fakeGhOutput(
JSON.stringify(
scenario.issue ?? {
number: 84,
title: "Issue",
url: "https://github.com/pingdotgg/codething-mvp/issues/84",
state: "open",
labels: [],
assignees: [],
},
) + "\n",
),
);
}

if (args[0] === "repo" && args[1] === "view") {
const repository = args[2];
if (typeof repository === "string" && args.includes("nameWithOwner,url,sshUrl")) {
Expand Down Expand Up @@ -576,6 +594,17 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
}).pipe(
Effect.map((result) => JSON.parse(result.stdout) as GitHubCli.GitHubPullRequestSummary),
),
getIssue: (input) =>
execute({
cwd: input.cwd,
args: [
"issue",
"view",
input.reference,
"--json",
"number,title,url,state,labels,assignees",
],
}).pipe(Effect.map((result) => JSON.parse(result.stdout) as GitHubCli.GitHubIssueSummary)),
getRepositoryCloneUrls: (input) =>
execute({
cwd: input.cwd,
Expand Down Expand Up @@ -627,6 +656,13 @@ function resolvePullRequest(
return manager.resolvePullRequest(input);
}

function resolveIssue(
manager: GitManager.GitManager["Service"],
input: { cwd: string; reference: string },
) {
return manager.resolveIssue(input);
}

function preparePullRequestThread(
manager: GitManager.GitManager["Service"],
input: GitPreparePullRequestThreadInput,
Expand Down Expand Up @@ -2547,6 +2583,38 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("resolves GitHub issues from #number references", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);

const { manager, ghCalls } = yield* makeManager({
ghScenario: {
issue: {
number: 84,
title: "Draft page suggestions",
url: "https://github.com/pingdotgg/codething-mvp/issues/84",
state: "open",
labels: ["enhancement"],
assignees: ["octocat"],
},
},
});

const result = yield* resolveIssue(manager, { cwd: repoDir, reference: "#84" });

expect(result.issue).toEqual({
number: 84,
title: "Draft page suggestions",
url: "https://github.com/pingdotgg/codething-mvp/issues/84",
state: "open",
labels: ["enhancement"],
assignees: ["octocat"],
});
expect(ghCalls.some((call) => call.startsWith("issue view 84 "))).toBe(true);
}),
);

it.effect("prepares pull request threads in local mode by checking out the PR branch", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
45 changes: 44 additions & 1 deletion apps/server/src/git/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
GitCommandError,
GitPreparePullRequestThreadInput,
GitPreparePullRequestThreadResult,
GitIssueRefInput,
GitResolveIssueResult,
GitPullRequestRefInput,
GitResolvePullRequestResult,
GitRunStackedActionInput,
Expand All @@ -41,7 +43,11 @@ import {
type ChangeRequestTerminology,
} from "@t3tools/shared/sourceControl";

import { GitManagerError, GitPullRequestMaterializationError } from "@t3tools/contracts";
import {
GitManagerError,
GitPullRequestMaterializationError,
SourceControlProviderError,
} from "@t3tools/contracts";
import * as TextGeneration from "../textGeneration/TextGeneration.ts";
import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";
import { extractBranchNameFromRemoteRef } from "./remoteRefs.ts";
Expand Down Expand Up @@ -79,6 +85,9 @@ export class GitManager extends Context.Service<
readonly resolvePullRequest: (
input: GitPullRequestRefInput,
) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>;
readonly resolveIssue: (
input: GitIssueRefInput,
) => Effect.Effect<GitResolveIssueResult, GitManagerServiceError>;
readonly preparePullRequestThread: (
input: GitPreparePullRequestThreadInput,
) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>;
Expand Down Expand Up @@ -478,6 +487,10 @@ function normalizePullRequestReference(reference: string): string {
return hashNumber?.[1] ?? trimmed;
}

function normalizeIssueReference(reference: string): string {
return normalizePullRequestReference(reference);
}

function toResolvedPullRequest(pr: {
number: number;
title: string;
Expand Down Expand Up @@ -1458,6 +1471,35 @@ export const make = Effect.gen(function* () {
return { pullRequest };
});

const resolveIssue: GitManager["Service"]["resolveIssue"] = Effect.fn("resolveIssue")(
function* (input) {
const provider = yield* sourceControlProvider(input.cwd);
if (!provider.getIssue) {
return yield* new SourceControlProviderError({
provider: provider.kind,
operation: "getIssue",
cwd: input.cwd,
reference: normalizeIssueReference(input.reference),
detail: "Issue lookup is currently available for GitHub repositories only.",
});
}
const issue = yield* provider.getIssue({
cwd: input.cwd,
reference: normalizeIssueReference(input.reference),
});
return {
issue: {
number: issue.number,
title: issue.title,
url: issue.url,
state: issue.state,
labels: [...issue.labels],
assignees: [...issue.assignees],
},
};
},
);

const preparePullRequestThread: GitManager["Service"]["preparePullRequestThread"] = Effect.fn(
"preparePullRequestThread",
)(function* (input) {
Expand Down Expand Up @@ -1866,6 +1908,7 @@ export const make = Effect.gen(function* () {
invalidateRemoteStatus,
invalidateStatus,
resolvePullRequest,
resolveIssue,
preparePullRequestThread,
runStackedAction,
});
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/git/GitWorkflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
type GitManagerServiceError,
type GitPreparePullRequestThreadInput,
type GitPreparePullRequestThreadResult,
type GitIssueRefInput,
type GitResolveIssueResult,
type GitPullRequestRefInput,
type VcsPullResult,
type VcsRemoveWorktreeInput,
Expand Down Expand Up @@ -56,6 +58,9 @@ export class GitWorkflowService extends Context.Service<
readonly resolvePullRequest: (
input: GitPullRequestRefInput,
) => Effect.Effect<GitResolvePullRequestResult, GitManagerServiceError>;
readonly resolveIssue: (
input: GitIssueRefInput,
) => Effect.Effect<GitResolveIssueResult, GitManagerServiceError>;
readonly preparePullRequestThread: (
input: GitPreparePullRequestThreadInput,
) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>;
Expand Down Expand Up @@ -285,6 +290,7 @@ export const make = Effect.gen(function* () {
"GitWorkflowService.resolvePullRequest",
gitManager.resolvePullRequest,
),
resolveIssue: routeGitManager("GitWorkflowService.resolveIssue", gitManager.resolveIssue),
preparePullRequestThread: routeGitManager(
"GitWorkflowService.preparePullRequestThread",
gitManager.preparePullRequestThread,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4900,6 +4900,17 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
state: "open",
},
}),
resolveIssue: () =>
Effect.succeed({
issue: {
number: 2,
title: "Demo issue",
url: "https://example.com/issues/2",
state: "open",
labels: [],
assignees: [],
},
}),
preparePullRequestThread: () =>
Effect.succeed({
pullRequest: {
Expand Down
39 changes: 39 additions & 0 deletions apps/server/src/sourceControl/GitHubCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,45 @@ describe("GitHubCli.layer", () => {
}).pipe(Effect.provide(layer)),
);

it.effect("parses issue view output", () =>
Effect.gen(function* () {
mockRun.mockReturnValueOnce(
Effect.succeed(
processOutput(
// @effect-diagnostics-next-line preferSchemaOverJson:off
JSON.stringify({
number: 84,
title: "Draft page suggestions",
url: "https://github.com/pingdotgg/codething-mvp/issues/84",
state: "OPEN",
labels: [{ name: "enhancement" }, { name: "web" }],
assignees: [{ login: "octocat" }],
}),
),
),
);

const gh = yield* GitHubCli.GitHubCli;
const result = yield* gh.getIssue({ cwd: "/repo", reference: "84" });

assert.deepStrictEqual(result, {
number: 84,
title: "Draft page suggestions",
url: "https://github.com/pingdotgg/codething-mvp/issues/84",
state: "open",
labels: ["enhancement", "web"],
assignees: ["octocat"],
});
expect(mockRun).toHaveBeenCalledWith({
operation: "GitHubCli.execute",
command: "gh",
args: ["issue", "view", "84", "--json", "number,title,url,state,labels,assignees"],
cwd: "/repo",
timeoutMs: 30_000,
});
}).pipe(Effect.provide(layer)),
);

it.effect("trims pull request fields decoded from gh json", () =>
Effect.gen(function* () {
mockRun.mockReturnValueOnce(
Expand Down
Loading
Loading