import { randomUUID } from "node:crypto";import { realpathSync } from "node:fs"; import { Effect, FileSystem, Layer, Path } from "effect";import { resolveAutoFeatureBranchName, sanitizeBranchFragment, sanitizeFeatureBranchName,} from "@t3tools/shared/git"; import { GitManagerError } from "../Errors.ts";import { GitManager, type GitManagerShape } from "../Services/GitManager.ts";import { GitCore } from "../Services/GitCore.ts";import { GitHubCli } from "../Services/GitHubCli.ts";import { TextGeneration } from "../Services/TextGeneration.ts"; interface OpenPrInfo { number: number; title: string; url: string; baseRefName: string; headRefName: string;} interface PullRequestInfo extends OpenPrInfo { state: "open" | "closed" | "merged"; updatedAt: string | null;} interface ResolvedPullRequest { number: number; title: string; url: string; baseBranch: string; headBranch: string; state: "open" | "closed" | "merged";} interface PullRequestHeadRemoteInfo { isCrossRepository?: boolean; headRepositoryNameWithOwner?: string | null; headRepositoryOwnerLogin?: string | null;} interface BranchHeadContext { localBranch: string; headBranch: string; headSelectors: ReadonlyArray<string>; preferredHeadSelector: string; remoteName: string | null; headRepositoryNameWithOwner: string | null; headRepositoryOwnerLogin: string | null; isCrossRepository: boolean;} function parseRepositoryNameFromPullRequestUrl(url: string): string | null { const trimmed = url.trim(); const match = /^https:\/\/github\.com\/[^/]+\/([^/]+)\/pull\/\d+(?:\/.*)?$/i.exec(trimmed); const repositoryName = match?.[1]?.trim() ?? ""; return repositoryName.length > 0 ? repositoryName : null;} function resolveHeadRepositoryNameWithOwner( pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,): string | null { const explicitRepository = pullRequest.headRepositoryNameWithOwner?.trim() ?? ""; if (explicitRepository.length > 0) { return explicitRepository; } if (!pullRequest.isCrossRepository) { return null; } const ownerLogin = pullRequest.headRepositoryOwnerLogin?.trim() ?? ""; const repositoryName = parseRepositoryNameFromPullRequestUrl(pullRequest.url); if (ownerLogin.length === 0 || !repositoryName) { return null; } return `${ownerLogin}/${repositoryName}`;} function resolvePullRequestWorktreeLocalBranchName( pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo,): string { if (!pullRequest.isCrossRepository) { return pullRequest.headBranch; } const sanitizedHeadBranch = sanitizeBranchFragment(pullRequest.headBranch).trim(); const suffix = sanitizedHeadBranch.length > 0 ? sanitizedHeadBranch : "head"; return `t3code/pr-${pullRequest.number}/${suffix}`;} function parseGitHubRepositoryNameWithOwnerFromRemoteUrl(url: string | null): string | null { const trimmed = url?.trim() ?? ""; if (trimmed.length === 0) { return null; } const match = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/|git:\/\/github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec( trimmed, ); const repositoryNameWithOwner = match?.[1]?.trim() ?? ""; return repositoryNameWithOwner.length > 0 ? repositoryNameWithOwner : null;} function parseRepositoryOwnerLogin(nameWithOwner: string | null): string | null { const trimmed = nameWithOwner?.trim() ?? ""; if (trimmed.length === 0) { return null; } const [ownerLogin] = trimmed.split("/"); const normalizedOwnerLogin = ownerLogin?.trim() ?? ""; return normalizedOwnerLogin.length > 0 ? normalizedOwnerLogin : null;} function parsePullRequestList(raw: unknown): PullRequestInfo[] { if (!Array.isArray(raw)) return []; const parsed: PullRequestInfo[] = []; for (const entry of raw) { if (!entry || typeof entry !== "object") continue; const record = entry as Record<string, unknown>; const number = record.number; const title = record.title; const url = record.url; const baseRefName = record.baseRefName; const headRefName = record.headRefName; const state = record.state; const mergedAt = record.mergedAt; const updatedAt = record.updatedAt; if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) { continue; } if ( typeof title !== "string" || typeof url !== "string" || typeof baseRefName !== "string" || typeof headRefName !== "string" ) { continue; } let normalizedState: "open" | "closed" | "merged"; if ((typeof mergedAt === "string" && mergedAt.trim().length > 0) || state === "MERGED") { normalizedState = "merged"; } else if (state === "OPEN" || state === undefined || state === null) { normalizedState = "open"; } else if (state === "CLOSED") { normalizedState = "closed"; } else { continue; } parsed.push({ number, title, url, baseRefName, headRefName, state: normalizedState, updatedAt: typeof updatedAt === "string" && updatedAt.trim().length > 0 ? updatedAt : null, }); } return parsed;} function gitManagerError(operation: string, detail: string, cause?: unknown): GitManagerError { return new GitManagerError({ operation, detail, ...(cause !== undefined ? { cause } : {}), });} function limitContext(value: string, maxChars: number): string { if (value.length <= maxChars) return value; return `${value.slice(0, maxChars)}\n\n[truncated]`;} function sanitizeCommitMessage(generated: { subject: string; body: string; branch?: string | undefined;}): { subject: string; body: string; branch?: string | undefined;} { const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? ""; const subject = rawSubject.replace(/[.]+$/g, "").trim(); const safeSubject = subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files"; return { subject: safeSubject, body: generated.body.trim(), ...(generated.branch !== undefined ? { branch: generated.branch } : {}), };} interface CommitAndBranchSuggestion { subject: string; body: string; branch?: string | undefined; commitMessage: string;} function formatCommitMessage(subject: string, body: string): string { const trimmedBody = body.trim(); if (trimmedBody.length === 0) { return subject; } return `${subject}\n\n${trimmedBody}`;} function parseCustomCommitMessage(raw: string): { subject: string; body: string } | null { const normalized = raw.replace(/\r\n/g, "\n").trim(); if (normalized.length === 0) { return null; } const [firstLine, ...rest] = normalized.split("\n"); const subject = firstLine?.trim() ?? ""; if (subject.length === 0) { return null; } return { subject, body: rest.join("\n").trim(), };} function extractBranchFromRef(ref: string): string { const normalized = ref.trim(); if (normalized.startsWith("refs/remotes/")) { const withoutPrefix = normalized.slice("refs/remotes/".length); const firstSlash = withoutPrefix.indexOf("/"); if (firstSlash === -1) { return withoutPrefix.trim(); } return withoutPrefix.slice(firstSlash + 1).trim(); } const firstSlash = normalized.indexOf("/"); if (firstSlash === -1) { return normalized; } return normalized.slice(firstSlash + 1).trim();} function appendUnique(values: string[], next: string | null | undefined): void { const trimmed = next?.trim() ?? ""; if (trimmed.length === 0 || values.includes(trimmed)) { return; } values.push(trimmed);} function toStatusPr(pr: PullRequestInfo): { number: number; title: string; url: string; baseBranch: string; headBranch: string; state: "open" | "closed" | "merged";} { return { number: pr.number, title: pr.title, url: pr.url, baseBranch: pr.baseRefName, headBranch: pr.headRefName, state: pr.state, };} function normalizePullRequestReference(reference: string): string { const trimmed = reference.trim(); const hashNumber = /^#(\d+)$/.exec(trimmed); return hashNumber?.[1] ?? trimmed;} function canonicalizeExistingPath(value: string): string { try { return realpathSync.native(value); } catch { return value; }} function toResolvedPullRequest(pr: { number: number; title: string; url: string; baseRefName: string; headRefName: string; state?: "open" | "closed" | "merged";}): ResolvedPullRequest { return { number: pr.number, title: pr.title, url: pr.url, baseBranch: pr.baseRefName, headBranch: pr.headRefName, state: pr.state ?? "open", };} function shouldPreferSshRemote(url: string | null): boolean { if (!url) return false; const trimmed = url.trim(); return trimmed.startsWith("git@") || trimmed.startsWith("ssh://");} function toPullRequestHeadRemoteInfo(pr: { isCrossRepository?: boolean; headRepositoryNameWithOwner?: string | null; headRepositoryOwnerLogin?: string | null;}): PullRequestHeadRemoteInfo { return { ...(pr.isCrossRepository !== undefined ? { isCrossRepository: pr.isCrossRepository } : {}), ...(pr.headRepositoryNameWithOwner !== undefined ? { headRepositoryNameWithOwner: pr.headRepositoryNameWithOwner } : {}), ...(pr.headRepositoryOwnerLogin !== undefined ? { headRepositoryOwnerLogin: pr.headRepositoryOwnerLogin } : {}), };} export const makeGitManager = Effect.gen(function* () { const gitCore = yield* GitCore; const gitHubCli = yield* GitHubCli; const textGeneration = yield* TextGeneration; const configurePullRequestHeadUpstream = ( cwd: string, pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, localBranch = pullRequest.headBranch, ) => Effect.gen(function* () { const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? ""; if (repositoryNameWithOwner.length === 0) { return; } const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; const preferredRemoteName = pullRequest.headRepositoryOwnerLogin?.trim() || repositoryNameWithOwner.split("/")[0]?.trim() || "fork"; const remoteName = yield* gitCore.ensureRemote({ cwd, preferredName: preferredRemoteName, url: remoteUrl, }); yield* gitCore.setBranchUpstream({ cwd, branch: localBranch, remoteName, remoteBranch: pullRequest.headBranch, }); }).pipe( Effect.catch((error) => Effect.logWarning( `GitManager.configurePullRequestHeadUpstream: failed to configure upstream for ${localBranch} -> ${pullRequest.headBranch} in ${cwd}: ${error.message}`, ).pipe(Effect.asVoid), ), ); const materializePullRequestHeadBranch = ( cwd: string, pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, localBranch = pullRequest.headBranch, ) => Effect.gen(function* () { const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? ""; if (repositoryNameWithOwner.length === 0) { yield* gitCore.fetchPullRequestBranch({ cwd, prNumber: pullRequest.number, branch: localBranch, }); return; } const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; const preferredRemoteName = pullRequest.headRepositoryOwnerLogin?.trim() || repositoryNameWithOwner.split("/")[0]?.trim() || "fork"; const remoteName = yield* gitCore.ensureRemote({ cwd, preferredName: preferredRemoteName, url: remoteUrl, }); yield* gitCore.fetchRemoteBranch({ cwd, remoteName, remoteBranch: pullRequest.headBranch, localBranch, }); yield* gitCore.setBranchUpstream({ cwd, branch: localBranch, remoteName, remoteBranch: pullRequest.headBranch, }); }).pipe( Effect.catch(() => gitCore.fetchPullRequestBranch({ cwd, prNumber: pullRequest.number, branch: localBranch, }), ), ); const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp"; const readConfigValueNullable = (cwd: string, key: string) => gitCore.readConfigValue(cwd, key).pipe(Effect.catch(() => Effect.succeed(null))); const resolveRemoteRepositoryContext = (cwd: string, remoteName: string | null) => Effect.gen(function* () { if (!remoteName) { return { repositoryNameWithOwner: null, ownerLogin: null, }; } const remoteUrl = yield* readConfigValueNullable(cwd, `remote.${remoteName}.url`); const repositoryNameWithOwner = parseGitHubRepositoryNameWithOwnerFromRemoteUrl(remoteUrl); return { repositoryNameWithOwner, ownerLogin: parseRepositoryOwnerLogin(repositoryNameWithOwner), }; }); const resolveBranchHeadContext = ( cwd: string, details: { branch: string; upstreamRef: string | null }, ) => Effect.gen(function* () { const remoteName = yield* readConfigValueNullable(cwd, `branch.${details.branch}.remote`); const headBranchFromUpstream = details.upstreamRef ? extractBranchFromRef(details.upstreamRef) : ""; const headBranch = headBranchFromUpstream.length > 0 ? headBranchFromUpstream : details.branch; const [remoteRepository, originRepository] = yield* Effect.all( [ resolveRemoteRepositoryContext(cwd, remoteName), resolveRemoteRepositoryContext(cwd, "origin"), ], { concurrency: "unbounded" }, ); const isCrossRepository = remoteRepository.repositoryNameWithOwner !== null && originRepository.repositoryNameWithOwner !== null ? remoteRepository.repositoryNameWithOwner.toLowerCase() !== originRepository.repositoryNameWithOwner.toLowerCase() : remoteName !== null && remoteName !== "origin" && remoteRepository.repositoryNameWithOwner !== null; const ownerHeadSelector = remoteRepository.ownerLogin && headBranch.length > 0 ? `${remoteRepository.ownerLogin}:${headBranch}` : null; const remoteAliasHeadSelector = remoteName && headBranch.length > 0 ? `${remoteName}:${headBranch}` : null; const shouldProbeRemoteOwnedSelectors = isCrossRepository || (remoteName !== null && remoteName !== "origin"); const headSelectors: string[] = []; if (isCrossRepository && shouldProbeRemoteOwnedSelectors) { appendUnique(headSelectors, ownerHeadSelector);Lines 1–500 of 1084. Load more
import { randomUUID } from "node:crypto"; import { realpathSync } from "node:fs"; import { Effect, FileSystem, Layer, Path } from "effect"; import { resolveAutoFeatureBranchName, sanitizeBranchFragment, sanitizeFeatureBranchName, } from "@t3tools/shared/git"; import { GitManagerError } from "../Errors.ts"; import { GitManager, type GitManagerShape } from "../Services/GitManager.ts"; import { GitCore } from "../Services/GitCore.ts"; import { GitHubCli } from "../Services/GitHubCli.ts"; import { TextGeneration } from "../Services/TextGeneration.ts"; interface OpenPrInfo { number: number; title: string; url: string; baseRefName: string; headRefName: string; } interface PullRequestInfo extends OpenPrInfo { state: "open" | "closed" | "merged"; updatedAt: string | null; } interface ResolvedPullRequest { number: number; title: string; url: string; baseBranch: string; headBranch: string; state: "open" | "closed" | "merged"; } interface PullRequestHeadRemoteInfo { isCrossRepository?: boolean; headRepositoryNameWithOwner?: string | null; headRepositoryOwnerLogin?: string | null; } interface BranchHeadContext { localBranch: string; headBranch: string; headSelectors: ReadonlyArray<string>; preferredHeadSelector: string; remoteName: string | null; headRepositoryNameWithOwner: string | null; headRepositoryOwnerLogin: string | null; isCrossRepository: boolean; } function parseRepositoryNameFromPullRequestUrl(url: string): string | null { const trimmed = url.trim(); const match = /^https:\/\/github\.com\/[^/]+\/([^/]+)\/pull\/\d+(?:\/.*)?$/i.exec(trimmed); const repositoryName = match?.[1]?.trim() ?? ""; return repositoryName.length > 0 ? repositoryName : null; } function resolveHeadRepositoryNameWithOwner( pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, ): string | null { const explicitRepository = pullRequest.headRepositoryNameWithOwner?.trim() ?? ""; if (explicitRepository.length > 0) { return explicitRepository; } if (!pullRequest.isCrossRepository) { return null; } const ownerLogin = pullRequest.headRepositoryOwnerLogin?.trim() ?? ""; const repositoryName = parseRepositoryNameFromPullRequestUrl(pullRequest.url); if (ownerLogin.length === 0 || !repositoryName) { return null; } return `${ownerLogin}/${repositoryName}`; } function resolvePullRequestWorktreeLocalBranchName( pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, ): string { if (!pullRequest.isCrossRepository) { return pullRequest.headBranch; } const sanitizedHeadBranch = sanitizeBranchFragment(pullRequest.headBranch).trim(); const suffix = sanitizedHeadBranch.length > 0 ? sanitizedHeadBranch : "head"; return `t3code/pr-${pullRequest.number}/${suffix}`; } function parseGitHubRepositoryNameWithOwnerFromRemoteUrl(url: string | null): string | null { const trimmed = url?.trim() ?? ""; if (trimmed.length === 0) { return null; } const match = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/|git:\/\/github\.com\/)([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i.exec( trimmed, ); const repositoryNameWithOwner = match?.[1]?.trim() ?? ""; return repositoryNameWithOwner.length > 0 ? repositoryNameWithOwner : null; } function parseRepositoryOwnerLogin(nameWithOwner: string | null): string | null { const trimmed = nameWithOwner?.trim() ?? ""; if (trimmed.length === 0) { return null; } const [ownerLogin] = trimmed.split("/"); const normalizedOwnerLogin = ownerLogin?.trim() ?? ""; return normalizedOwnerLogin.length > 0 ? normalizedOwnerLogin : null; } function parsePullRequestList(raw: unknown): PullRequestInfo[] { if (!Array.isArray(raw)) return []; const parsed: PullRequestInfo[] = []; for (const entry of raw) { if (!entry || typeof entry !== "object") continue; const record = entry as Record<string, unknown>; const number = record.number; const title = record.title; const url = record.url; const baseRefName = record.baseRefName; const headRefName = record.headRefName; const state = record.state; const mergedAt = record.mergedAt; const updatedAt = record.updatedAt; if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) { continue; } if ( typeof title !== "string" || typeof url !== "string" || typeof baseRefName !== "string" || typeof headRefName !== "string" ) { continue; } let normalizedState: "open" | "closed" | "merged"; if ((typeof mergedAt === "string" && mergedAt.trim().length > 0) || state === "MERGED") { normalizedState = "merged"; } else if (state === "OPEN" || state === undefined || state === null) { normalizedState = "open"; } else if (state === "CLOSED") { normalizedState = "closed"; } else { continue; } parsed.push({ number, title, url, baseRefName, headRefName, state: normalizedState, updatedAt: typeof updatedAt === "string" && updatedAt.trim().length > 0 ? updatedAt : null, }); } return parsed; } function gitManagerError(operation: string, detail: string, cause?: unknown): GitManagerError { return new GitManagerError({ operation, detail, ...(cause !== undefined ? { cause } : {}), }); } function limitContext(value: string, maxChars: number): string { if (value.length <= maxChars) return value; return `${value.slice(0, maxChars)}\n\n[truncated]`; } function sanitizeCommitMessage(generated: { subject: string; body: string; branch?: string | undefined; }): { subject: string; body: string; branch?: string | undefined; } { const rawSubject = generated.subject.trim().split(/\r?\n/g)[0]?.trim() ?? ""; const subject = rawSubject.replace(/[.]+$/g, "").trim(); const safeSubject = subject.length > 0 ? subject.slice(0, 72).trimEnd() : "Update project files"; return { subject: safeSubject, body: generated.body.trim(), ...(generated.branch !== undefined ? { branch: generated.branch } : {}), }; } interface CommitAndBranchSuggestion { subject: string; body: string; branch?: string | undefined; commitMessage: string; } function formatCommitMessage(subject: string, body: string): string { const trimmedBody = body.trim(); if (trimmedBody.length === 0) { return subject; } return `${subject}\n\n${trimmedBody}`; } function parseCustomCommitMessage(raw: string): { subject: string; body: string } | null { const normalized = raw.replace(/\r\n/g, "\n").trim(); if (normalized.length === 0) { return null; } const [firstLine, ...rest] = normalized.split("\n"); const subject = firstLine?.trim() ?? ""; if (subject.length === 0) { return null; } return { subject, body: rest.join("\n").trim(), }; } function extractBranchFromRef(ref: string): string { const normalized = ref.trim(); if (normalized.startsWith("refs/remotes/")) { const withoutPrefix = normalized.slice("refs/remotes/".length); const firstSlash = withoutPrefix.indexOf("/"); if (firstSlash === -1) { return withoutPrefix.trim(); } return withoutPrefix.slice(firstSlash + 1).trim(); } const firstSlash = normalized.indexOf("/"); if (firstSlash === -1) { return normalized; } return normalized.slice(firstSlash + 1).trim(); } function appendUnique(values: string[], next: string | null | undefined): void { const trimmed = next?.trim() ?? ""; if (trimmed.length === 0 || values.includes(trimmed)) { return; } values.push(trimmed); } function toStatusPr(pr: PullRequestInfo): { number: number; title: string; url: string; baseBranch: string; headBranch: string; state: "open" | "closed" | "merged"; } { return { number: pr.number, title: pr.title, url: pr.url, baseBranch: pr.baseRefName, headBranch: pr.headRefName, state: pr.state, }; } function normalizePullRequestReference(reference: string): string { const trimmed = reference.trim(); const hashNumber = /^#(\d+)$/.exec(trimmed); return hashNumber?.[1] ?? trimmed; } function canonicalizeExistingPath(value: string): string { try { return realpathSync.native(value); } catch { return value; } } function toResolvedPullRequest(pr: { number: number; title: string; url: string; baseRefName: string; headRefName: string; state?: "open" | "closed" | "merged"; }): ResolvedPullRequest { return { number: pr.number, title: pr.title, url: pr.url, baseBranch: pr.baseRefName, headBranch: pr.headRefName, state: pr.state ?? "open", }; } function shouldPreferSshRemote(url: string | null): boolean { if (!url) return false; const trimmed = url.trim(); return trimmed.startsWith("git@") || trimmed.startsWith("ssh://"); } function toPullRequestHeadRemoteInfo(pr: { isCrossRepository?: boolean; headRepositoryNameWithOwner?: string | null; headRepositoryOwnerLogin?: string | null; }): PullRequestHeadRemoteInfo { return { ...(pr.isCrossRepository !== undefined ? { isCrossRepository: pr.isCrossRepository } : {}), ...(pr.headRepositoryNameWithOwner !== undefined ? { headRepositoryNameWithOwner: pr.headRepositoryNameWithOwner } : {}), ...(pr.headRepositoryOwnerLogin !== undefined ? { headRepositoryOwnerLogin: pr.headRepositoryOwnerLogin } : {}), }; } export const makeGitManager = Effect.gen(function* () { const gitCore = yield* GitCore; const gitHubCli = yield* GitHubCli; const textGeneration = yield* TextGeneration; const configurePullRequestHeadUpstream = ( cwd: string, pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, localBranch = pullRequest.headBranch, ) => Effect.gen(function* () { const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? ""; if (repositoryNameWithOwner.length === 0) { return; } const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; const preferredRemoteName = pullRequest.headRepositoryOwnerLogin?.trim() || repositoryNameWithOwner.split("/")[0]?.trim() || "fork"; const remoteName = yield* gitCore.ensureRemote({ cwd, preferredName: preferredRemoteName, url: remoteUrl, }); yield* gitCore.setBranchUpstream({ cwd, branch: localBranch, remoteName, remoteBranch: pullRequest.headBranch, }); }).pipe( Effect.catch((error) => Effect.logWarning( `GitManager.configurePullRequestHeadUpstream: failed to configure upstream for ${localBranch} -> ${pullRequest.headBranch} in ${cwd}: ${error.message}`, ).pipe(Effect.asVoid), ), ); const materializePullRequestHeadBranch = ( cwd: string, pullRequest: ResolvedPullRequest & PullRequestHeadRemoteInfo, localBranch = pullRequest.headBranch, ) => Effect.gen(function* () { const repositoryNameWithOwner = resolveHeadRepositoryNameWithOwner(pullRequest) ?? ""; if (repositoryNameWithOwner.length === 0) { yield* gitCore.fetchPullRequestBranch({ cwd, prNumber: pullRequest.number, branch: localBranch, }); return; } const cloneUrls = yield* gitHubCli.getRepositoryCloneUrls({ cwd, repository: repositoryNameWithOwner, }); const originRemoteUrl = yield* gitCore.readConfigValue(cwd, "remote.origin.url"); const remoteUrl = shouldPreferSshRemote(originRemoteUrl) ? cloneUrls.sshUrl : cloneUrls.url; const preferredRemoteName = pullRequest.headRepositoryOwnerLogin?.trim() || repositoryNameWithOwner.split("/")[0]?.trim() || "fork"; const remoteName = yield* gitCore.ensureRemote({ cwd, preferredName: preferredRemoteName, url: remoteUrl, }); yield* gitCore.fetchRemoteBranch({ cwd, remoteName, remoteBranch: pullRequest.headBranch, localBranch, }); yield* gitCore.setBranchUpstream({ cwd, branch: localBranch, remoteName, remoteBranch: pullRequest.headBranch, }); }).pipe( Effect.catch(() => gitCore.fetchPullRequestBranch({ cwd, prNumber: pullRequest.number, branch: localBranch, }), ), ); const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const tempDir = process.env.TMPDIR ?? process.env.TEMP ?? process.env.TMP ?? "/tmp"; const readConfigValueNullable = (cwd: string, key: string) => gitCore.readConfigValue(cwd, key).pipe(Effect.catch(() => Effect.succeed(null))); const resolveRemoteRepositoryContext = (cwd: string, remoteName: string | null) => Effect.gen(function* () { if (!remoteName) { return { repositoryNameWithOwner: null, ownerLogin: null, }; } const remoteUrl = yield* readConfigValueNullable(cwd, `remote.${remoteName}.url`); const repositoryNameWithOwner = parseGitHubRepositoryNameWithOwnerFromRemoteUrl(remoteUrl); return { repositoryNameWithOwner, ownerLogin: parseRepositoryOwnerLogin(repositoryNameWithOwner), }; }); const resolveBranchHeadContext = ( cwd: string, details: { branch: string; upstreamRef: string | null }, ) => Effect.gen(function* () { const remoteName = yield* readConfigValueNullable(cwd, `branch.${details.branch}.remote`); const headBranchFromUpstream = details.upstreamRef ? extractBranchFromRef(details.upstreamRef) : ""; const headBranch = headBranchFromUpstream.length > 0 ? headBranchFromUpstream : details.branch; const [remoteRepository, originRepository] = yield* Effect.all( [ resolveRemoteRepositoryContext(cwd, remoteName), resolveRemoteRepositoryContext(cwd, "origin"), ], { concurrency: "unbounded" }, ); const isCrossRepository = remoteRepository.repositoryNameWithOwner !== null && originRepository.repositoryNameWithOwner !== null ? remoteRepository.repositoryNameWithOwner.toLowerCase() !== originRepository.repositoryNameWithOwner.toLowerCase() : remoteName !== null && remoteName !== "origin" && remoteRepository.repositoryNameWithOwner !== null; const ownerHeadSelector = remoteRepository.ownerLogin && headBranch.length > 0 ? `${remoteRepository.ownerLogin}:${headBranch}` : null; const remoteAliasHeadSelector = remoteName && headBranch.length > 0 ? `${remoteName}:${headBranch}` : null; const shouldProbeRemoteOwnedSelectors = isCrossRepository || (remoteName !== null && remoteName !== "origin"); const headSelectors: string[] = []; if (isCrossRepository && shouldProbeRemoteOwnedSelectors) { appendUnique(headSelectors, ownerHeadSelector);