feat(source-control): add Entire as a source control host #35920
Openpeyton-alt(GitHub profile, opens in a new tab)wants to merge 1 commit into
main fromrefs/pull/35920/headopened View on GitHubgithub-actions[bot](GitHub profile, opens in a new tab) added labelsvouch:unvouched
tanstack-source added labelsfeature
github-actions[bot](GitHub profile, opens in a new tab) added labelssize:XXL
- packages/source-control-entire/src/server/EntirePullRequestProvider.ts line 560@@ -0,0 +540,21 @@540+ location: {541+ granularity: "line",542+ file_path: comment.path,543+ start_line: line,544+ end_line: line,545+ selected_text: text,546+ },547+ };548+ }),549+ );550+ });551+552+ const publishReview = Effect.fn("EntirePullRequestProvider.publishReview")(function* (553+ input: Parameters<PullRequestProviderApi["submitReview"]>[0],554+ ) {555+ if (input.comments.length > MAX_REVIEW_COMMENTS)556+ return yield* failure(557+ "submitReview",558+ `Entire publishes at most ${MAX_REVIEW_COMMENTS} comments in one review.`,559+ );560+ const { base, head } = yield* trailRevisions(input);packages/source-control-entire/src/server/entireTrailBody.ts line 177@@ -0,0 +157,21 @@157+function renderChildren(node: ProseMirrorNode, lose: Lose): string {158+ return children(node)159+ .map((child) => renderBlock(child, lose))160+ .filter(Boolean)161+ .join("\n\n");162+}163+164+interface TrailBodyDocument {165+ readonly text_snapshot: string;166+ readonly content_json?: unknown;167+}168+169+function render(document: TrailBodyDocument): { markdown: string; lost: string | null } {170+ let lost: string | null = null;171+ const lose: Lose = (what) => {172+ lost ??= what;173+ };174+ const doc = document.content_json;175+ const rendered = isNode(doc) && doc.type === "doc" ? renderBlock(doc, lose) : "";176+ const body = rendered.trim() === "" ? document.text_snapshot : rendered;177+ return { markdown: collapseBlankLines(body.replace(TRAIL_LINK, "")).trim(), lost };packages/source-control-entire/src/server/entireTrailJson.ts lines 429 to 429@@ -0,0 +409,21 @@409+ case "cancelled":410+ return "cancelled";411+ case "action_required":412+ return "action-required";413+ default:414+ return "failure";415+ }416+}417+418+const runsRead = (detail: EntireTrailDetail) =>419+ detail.mergeability?.checks?.availability === "available";420+421+function gateChecks(422+ detail: EntireTrailDetail,423+ repository: string,424+): ReadonlyArray<PullRequestCheck> {425+ const url = trailUrl(detail, repository);426+ return blockingGates(detail)427+ .filter(428+ (gate) =>429+ gate.gate_type !== "approvals" && (gate.gate_type !== "checks" || !runsRead(detail)),packages/source-control-entire/src/server/EntirePullRequestProvider.ts line 195@@ -0,0 +175,21 @@175+ });176+177+ /** The change's description, which is its project trail's wherever that can be read. */178+ const description = Effect.fn("EntirePullRequestProvider.description")(function* (179+ input: TrailRef,180+ trail: Json.EntireTrailDetail,181+ ) {182+ const route = Json.projectTrailRoute(trail);183+ const shown =184+ route === null185+ ? Json.changeBody(trail)186+ : yield* readProjectTrail("getChangeRequest", input, route).pipe(187+ Effect.map(({ body }) => Json.changeBody(trail, body.bodyDocument ?? null)),188+ // The change keeps a copy from when it was linked, which beats no description.189+ Effect.catch((error) =>190+ Effect.logDebug("Entire project trail unreadable; showing the change's copy", {191+ detail: error.detail,192+ }).pipe(Effect.as(Json.changeBody(trail))),193+ ),194+ );195+ yield* Cache.set(shownDescriptions, trailKey(input), shown);packages/source-control-entire/src/server/entireTrailBody.ts lines 46 to 46@@ -0,0 +26,21 @@26+27+const safeHref = (href: unknown): string | null =>28+ typeof href === "string" && /^(https?:|mailto:)/iu.test(href) ? href : null;29+30+/** Records what the renderer leaves out; only the first loss is reported. */31+type Lose = (what: string) => void;32+33+const MARKS = new Set(["bold", "italic", "strike", "code", "link"]);34+35+const unsupported = (node: ProseMirrorNode) =>36+ typeof node.type === "string" ? `unsupported content: ${node.type}` : "unsupported content";37+38+const longestBacktickRun = (text: string) =>39+ Math.max(0, ...(text.match(/`+/gu) ?? []).map((run) => run.length));40+41+/**42+ * Typed characters markdown would read as formatting. A delimiter needs a partner, so a lone one43+ * stays as typed, as does `_` inside a word; a partner in a neighbouring node is not looked for.44+ */45+function escapeText(text: string): string {46+ const paired = (char: string) => text.split(char).length > 2;
- packages/source-control-core/src/server/SourceControlProvider.ts line 110@@ -107,4 +107,4 @@107107 }108108109109 const match =110- /^(?:[^@/\s]+@[^:/\s]+:|[^@:/\\\s]{2,}:(?!:)|(?:ssh|https?|git):\/\/[^/]+\/)((?:[^/\s]+\/)+[^/\s]+?)(?:\.git)?\/?$/iu.exec(110+ /^(?:[^@/\s]+@[^:/\s]+:|[^@:/\\\s]{2,}:(?!:)|(?:ssh|https?|git|entire):\/\/[^/]+\/)((?:[^/\s]+\/)+[^/\s]+?)(?:\.git)?\/?$/iu.exec(packages/source-control-entire/src/server/EntireApi.ts line 235@@ -0,0 +215,21 @@215+ status: null,216+ reason: "failed",217+ detail: `Entire's cell list has no ${cell.slug} in ${cell.jurisdiction}.`,218+ });219+ return match.apiUrl;220+ });221+222+ const requestJson = Effect.fn("EntireApi.requestJson")(function* <S extends JsonSchema>(223+ input: EntireApiRequest,224+ schema: S,225+ ) {226+ const fail = failure(input.operation);227+ const { repository, coordinates } = yield* locate(input);228+ const apiUrl =229+ input.cell === undefined ? coordinates.apiUrl : yield* cellApiUrl(input.cell, fail);230+ const origin = entireApiOrigin(apiUrl);231+ if (origin === null)232+ return yield* fail({233+ status: null,234+ reason: "failed",235+ detail: `The Entire CLI named ${apiUrl} as the API, which is not an Entire API host.`,packages/source-control-entire/src/server/EntireCli.ts lines 187 to 189@@ -0,0 +169,21 @@169+ })170+ .pipe(171+ Effect.mapError((error) =>172+ isMissingExecutable(error)173+ ? new EntireCliMissingError()174+ : new EntireCliFailedError({175+ operation: "run",176+ detail: "The Entire CLI did not finish.",177+ cause: error,178+ }),179+ ),180+ );181+182+ const lookupRepository = Effect.fn("EntireCli.repository")(function* (reference: string) {183+ const output = yield* run("EntireCli.repository", ["repo", "view", reference, "--json"]);184+ if (output.exitCode !== 0)185+ return yield* new EntireCliFailedError({186+ operation: "find the repository",187+ detail:188+ boundedDetail(Option.getOrUndefined(firstNonEmptyLine(output.stderr))) ??189+ `\`entire repo view\` exited ${output.exitCode}.`,packages/source-control-entire/src/server/entireGitFixture.ts line 29@@ -0,0 +9,21 @@9+import * as Effect from "effect/Effect";10+import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";11+12+// Variables a git hook exports would point the fixture's git at the developer's repository.13+const REPOSITORY_SELECTORS = new Set([14+ "GIT_DIR",15+ "GIT_WORK_TREE",16+ "GIT_INDEX_FILE",17+ "GIT_OBJECT_DIRECTORY",18+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",19+ "GIT_COMMON_DIR",20+ "GIT_PREFIX",21+ "GIT_NAMESPACE",22+ "GIT_CEILING_DIRECTORIES",23+ "GIT_DISCOVERY_ACROSS_FILESYSTEM",24+]);25+26+// The developer's own git config must not shape the fixture or the patch.27+const GIT_ENV = {28+ ...Object.fromEntries(29+ Object.entries(process.env).filter(([name]) => !REPOSITORY_SELECTORS.has(name)),packages/source-control-entire/src/server/EntirePullRequestProvider.ts line 195@@ -0,0 +175,21 @@175+ });176+177+ /** The change's description, which is its project trail's wherever that can be read. */178+ const description = Effect.fn("EntirePullRequestProvider.description")(function* (179+ input: TrailRef,180+ trail: Json.EntireTrailDetail,181+ ) {182+ const route = Json.projectTrailRoute(trail);183+ const shown =184+ route === null185+ ? Json.changeBody(trail)186+ : yield* readProjectTrail("getChangeRequest", input, route).pipe(187+ Effect.map(({ body }) => Json.changeBody(trail, body.bodyDocument ?? null)),188+ // The change keeps a copy from when it was linked, which beats no description.189+ Effect.catch((error) =>190+ Effect.logDebug("Entire project trail unreadable; showing the change's copy", {191+ detail: error.detail,192+ }).pipe(Effect.as(Json.changeBody(trail))),193+ ),194+ );195+ yield* Cache.set(shownDescriptions, trailKey(input), shown);packages/source-control-entire/src/server/EntirePullRequestProvider.ts lines 560 to 561@@ -0,0 +541,21 @@541+ granularity: "line",542+ file_path: comment.path,543+ start_line: line,544+ end_line: line,545+ selected_text: text,546+ },547+ };548+ }),549+ );550+ });551+552+ const publishReview = Effect.fn("EntirePullRequestProvider.publishReview")(function* (553+ input: Parameters<PullRequestProviderApi["submitReview"]>[0],554+ ) {555+ if (input.comments.length > MAX_REVIEW_COMMENTS)556+ return yield* failure(557+ "submitReview",558+ `Entire publishes at most ${MAX_REVIEW_COMMENTS} comments in one review.`,559+ );560+ const { base, head } = yield* trailRevisions(input);561+ const comments = yield* lineComments(input, head, input.comments);packages/source-control-entire/src/server/EntireSourceControlProvider.ts line 226@@ -0,0 +206,21 @@206+ Json.EntireTrailDetail,207+ );208+ return { target, trail };209+ });210+211+ const toChangeRequest = (212+ trail: Json.EntireTrail & { readonly head_sha?: string | null | undefined },213+ target: Target,214+ ): ChangeRequest => {215+ const head = trail.head_sha?.trim();216+ const branch = Json.trailHeadBranch(trail).trim();217+ return {218+ provider: KIND,219+ number: trail.number,220+ title: Json.trailTitle(trail),221+ url: Json.trailUrl(trail, target.repository),222+ baseRefName: trail.base,223+ headRefName: branch || `trail-${trail.number}`,224+ ...(head ? { headSha: head } : {}),225+ state: Json.trailState(trail.status),226+ ...(Json.isDraftTrail(trail) ? { isDraft: true } : {}),packages/source-control-entire/src/server/EntireSourceControlProvider.ts line 388@@ -0,0 +368,21 @@368+ error(369+ operation,370+ input.cwd,371+ `Entire created trail #${trail.number} but could not link ${branch}: ${cause.detail}. ${linkByHand}`,372+ cause,373+ ),374+ ),375+ );376+ const change = linked.body;377+ if (change.trailId !== trail.id || change.repositoryId !== coordinates.repoId)378+ return yield* error(operation, input.cwd, "Entire linked the branch somewhere else.");379+ },380+ ),381+ getRepositoryCloneUrls: Effect.fn("EntireSourceControlProvider.getRepositoryCloneUrls")(382+ function* (input) {383+ const target = yield* resolveTarget("getRepositoryCloneUrls", input);384+ const url =385+ target.remote?.url ??386+ (yield* cli.repository(`/${target.repository}`).pipe(387+ Effect.mapError((cause) =>388+ error("getRepositoryCloneUrls", input.cwd, cause.message, cause),packages/source-control-entire/src/server/EntireSourceControlProvider.ts line 459@@ -0,0 +439,21 @@439+ .execute({440+ operation: "EntireSourceControlProvider.checkoutChangeRequest",441+ cwd: input.cwd,442+ args: ["check-ref-format", `refs/heads/${branch}`],443+ })444+ .pipe(445+ Effect.mapError((cause) =>446+ error(447+ "checkoutChangeRequest",448+ input.cwd,449+ "Entire named a branch git cannot check out.",450+ cause,451+ ),452+ ),453+ );454+ yield* Effect.gen(function* () {455+ const branches = yield* git.listLocalBranchNames(input.cwd);456+ const remoteBranch = { cwd: input.cwd, remoteName, remoteBranch: branch };457+ if (input.force !== true && !branches.includes(branch))458+ yield* git.fetchRemoteBranch({ ...remoteBranch, localBranch: branch });459+ else yield* git.fetchRemoteTrackingBranch(remoteBranch);packages/source-control-entire/src/server/entireTrailBody.ts lines 46 to 49@@ -0,0 +29,21 @@29+30+/** Records what the renderer leaves out; only the first loss is reported. */31+type Lose = (what: string) => void;32+33+const MARKS = new Set(["bold", "italic", "strike", "code", "link"]);34+35+const unsupported = (node: ProseMirrorNode) =>36+ typeof node.type === "string" ? `unsupported content: ${node.type}` : "unsupported content";37+38+const longestBacktickRun = (text: string) =>39+ Math.max(0, ...(text.match(/`+/gu) ?? []).map((run) => run.length));40+41+/**42+ * Typed characters markdown would read as formatting. A delimiter needs a partner, so a lone one43+ * stays as typed, as does `_` inside a word; a partner in a neighbouring node is not looked for.44+ */45+function escapeText(text: string): string {46+ const paired = (char: string) => text.split(char).length > 2;47+ return text48+ .replace(/\\(?=[!-/:-@[-`{-~])/gu, "\\\\")49+ .replace(/[`*~]/gu, (char) => (paired(char) ? `\\${char}` : char))packages/source-control-entire/src/server/entireTrailBody.ts line 75@@ -0,0 +55,21 @@55+ const raw = typeof node.text === "string" ? node.text : "";56+ if (raw === "") return "";57+ const marks = Array.isArray(node.marks) ? node.marks : [];58+ const has = (type: string) => marks.some((mark) => mark?.type === type);59+ if (marks.some((mark) => typeof mark?.type !== "string" || !MARKS.has(mark.type)))60+ lose("unsupported text formatting");61+ if (has("code")) {62+ if (marks.some((mark) => mark?.type !== "code")) lose("formatted or linked code");63+ const delimiter = "`".repeat(longestBacktickRun(raw) + 1);64+ const pad = raw.startsWith("`") || raw.endsWith("`") ? " " : "";65+ return `${delimiter}${pad}${raw}${pad}${delimiter}`;66+ }67+ let text = escapeText(raw);68+ if (has("bold")) text = `**${text}**`;69+ // `*`, not `_`: Entire's markdown converter reads only `*` emphasis back.70+ if (has("italic")) text = `*${text}*`;71+ if (has("strike")) text = `~~${text}~~`;72+ const link = marks.find((mark) => mark?.type === "link");73+ const href = link ? safeHref(link.attrs?.href) : null;74+ if (link && href === null) lose("links that aren't web or mail links");75+ return href ? `[${text}](${href})` : text;
Sign in to join this conversation.







This PR changes the test suite by +126 test cases (browser +1, unit +125, server 0).
Keep tests that would fail for a real reason, and test logic below the UI where it can be. See Testing.