feat(pull_requests): add support for remote repository diffing@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1665548701070,
+ "_generatedAtUnix": 1665593390876,
"_hashAlgorithm": "sha1",
"_version": 2,
"islands": {
@@ -64,7 +64,7 @@
"pathSourceMap": "./public/.islands/RepositoryInitialSetup.bundle.js.map"
},
"RepositoryPullRequestCreateForm": {
- "hash": "c22f19c53243560de6029b412ddae5bc60ee5dc6",
+ "hash": "e35581cdc53a2ea39b27c25637a181db891a386c",
"pathSource": "./app/islands/RepositoryPullRequestCreateForm.tsx",
"pathBundle": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryPullRequestCreateForm.bundle.js.map"
@@ -16,6 +16,7 @@ import { makeUsersService } from "../../services/user";
import RepositoryPullRequestCreateView, {
RepositoryPullRequestCreateViewProps,
} from "../../views/repository/RepositoryPullRequestCreateView";
+import { RepositoryFileDiff } from "app/types";
const postRepositoryPullRequestCreateAction: ReqHandler = async (
request,
@@ -107,11 +108,22 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
return reply.status(404).callNotFound();
}
- const fileDiffs = await repoService.getRepositoryRefDiff(
- sourceRepo,
- sourceRepoFromBranch,
- targetRepoDestBranch
- );
+ let fileDiffs = [] as RepositoryFileDiff[];
+
+ if (sourceParentOrg.slug === targetParentOrg.slug) {
+ fileDiffs = await repoService.getRepositoryRefDiff(
+ sourceRepo,
+ sourceRepoFromBranch,
+ targetRepoDestBranch
+ );
+ } else {
+ fileDiffs = await repoService.getRepositoryRemoteRefDiff(
+ sourceRepo,
+ sourceRepoFromBranch,
+ targetRepo,
+ targetRepoDestBranch
+ );
+ }
variant = {
state: desiredState,
@@ -193,42 +193,53 @@ const RepositoryPullRequestCreateForm: ReactIsland<RepositoryPullRequestCreateFo
name={"state_dest"}
value={PullRequestFormState.COMPARE}
/>
- <Grid.Row fluid alignItems={"center"}>
- <div data-islandid={`${PullRequestSourceSelect.name}$$1`}>
- <PullRequestSourceSelect
- namePrefix={"target"}
- defaultSource={{
- org: Object.values(data.sources)[0].org,
- repo: Object.values(
- Object.values(data.sources)[0].repos
- )[0].repo,
- branch: Object.values(
- Object.values(data.sources)[0].repos
- )[0].branches[0],
- }}
- sources={data.sources}
- onSelectionChange={onTargetSelectionChange}
- />
- </div>
- <div style={{ margin: "0 8px" }}>
- <span>vs.</span>
- </div>
- <div data-islandid={`${PullRequestSourceSelect.name}$$0`}>
- <PullRequestSourceSelect
- namePrefix={"source"}
- defaultSource={{
- org: Object.values(data.sources)[0].org,
- repo: Object.values(
- Object.values(data.sources)[0].repos
- )[0].repo,
- branch: Object.values(
- Object.values(data.sources)[0].repos
- )[0].branches[0],
- }}
- sources={data.sources}
- onSelectionChange={onSourceSelectionChange}
- />
- </div>
+ <Grid.Row fluid nowrap alignItems={"center"}>
+ <Grid.Row nowrap fluid alignItems={"center"}>
+ <label>Target:</label>
+ <div data-islandid={`${PullRequestSourceSelect.name}$$1`}>
+ <PullRequestSourceSelect
+ namePrefix={"target"}
+ defaultSource={{
+ org: Object.values(data.sources)[0].org,
+ repo: Object.values(
+ Object.values(data.sources)[0].repos
+ )[0].repo,
+ branch: Object.values(
+ Object.values(data.sources)[0].repos
+ )[0].branches[0],
+ }}
+ sources={data.sources}
+ onSelectionChange={onTargetSelectionChange}
+ />
+ </div>
+ </Grid.Row>
+ <Grid.Row
+ flex={0.2}
+ justifyContent={"center"}
+ alignItems={"center"}
+ style={{ margin: "0 8px" }}
+ >
+ <span><--</span>
+ </Grid.Row>
+ <Grid.Row nowrap fluid alignItems={"center"}>
+ <label>Source:</label>
+ <div data-islandid={`${PullRequestSourceSelect.name}$$0`}>
+ <PullRequestSourceSelect
+ namePrefix={"source"}
+ defaultSource={{
+ org: Object.values(data.sources)[0].org,
+ repo: Object.values(
+ Object.values(data.sources)[0].repos
+ )[0].repo,
+ branch: Object.values(
+ Object.values(data.sources)[0].repos
+ )[0].branches[0],
+ }}
+ sources={data.sources}
+ onSelectionChange={onSourceSelectionChange}
+ />
+ </div>
+ </Grid.Row>
<button
// disabled={isCompareButtonEnabled === false}
type={"submit"}
@@ -0,0 +1,148 @@
+// std
+import { existsSync } from "node:fs";
+import { spawn } from "node:child_process";
+// 1st-party
+import { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// 3rd-party
+import parseDiff from "diffparser";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import type { RepositoryFileDiff } from "../../types";
+import { Env } from "../../env";
+// service
+import type { RepositoryServiceDeps } from "./types";
+import { default as makeGetRepositoryBranches } from "./getRepositoryBranches";
+
+const makeGetRepositoryRemoteRefDiff: ServiceMethodFactory<
+ RepositoryServiceDeps,
+ [Repository, string, Repository, string],
+ Promise<RepositoryFileDiff[]>
+> = ({ request }) => {
+ const getRepositoryBranches = makeGetRepositoryBranches({ request });
+
+ return async (sourceRepo, sourceFromBranch, targetRepo, targetDestBranch) => {
+ // - source
+ const sourceParentOrg = await request.prisma.organization.findUnique({
+ where: {
+ id: sourceRepo.organizationId,
+ },
+ });
+
+ if (sourceParentOrg == null) {
+ throw new Error(
+ `Could not find the parent organization for project "${sourceRepo.id}".`
+ );
+ }
+
+ const sourceBranches = await getRepositoryBranches(sourceRepo);
+
+ if (
+ sourceBranches.length <= 0 ||
+ sourceBranches.includes(sourceFromBranch) === false
+ ) {
+ throw new Error(
+ `No such branch ${sourceFromBranch} found in repository "${sourceRepo.id}"`
+ );
+ }
+
+ const sourceRepoPath = `${Env.GIT_REPOSITORIES_ROOT}/${sourceParentOrg.slug}/${sourceRepo.slug}.git`;
+
+ if (existsSync(sourceRepoPath) === false) {
+ throw new Error(
+ `Could not find a valid source git repository at: ${sourceRepoPath}`
+ );
+ }
+
+ // - target
+ const targetParentOrg = await request.prisma.organization.findUnique({
+ where: {
+ id: targetRepo.organizationId,
+ },
+ });
+
+ if (targetParentOrg == null) {
+ throw new Error(
+ `Could not find the parent organization for project "${targetRepo.slug}".`
+ );
+ }
+
+ const targetBranches = await getRepositoryBranches(targetRepo);
+
+ if (
+ targetBranches.length <= 0 ||
+ targetBranches.includes(targetDestBranch) === false
+ ) {
+ throw new Error(
+ `No such branch ${targetDestBranch} found in repository "${targetRepo.id}"`
+ );
+ }
+
+ const targetRepoPath = `${Env.GIT_REPOSITORIES_ROOT}/${targetParentOrg.slug}/${targetRepo.slug}.git`;
+
+ if (existsSync(targetRepoPath) === false) {
+ throw new Error(
+ `Could not find a valid target git repository at: ${targetRepoPath}`
+ );
+ }
+
+ const sourceRemoteName = `remotes/${sourceParentOrg.slug}/${sourceRepo.slug}`;
+ let isTargetRemoteSetup = false;
+
+ try {
+ // - gitAddSourceRemoteInTargetRepoProcess
+ spawn("git", ["remote", "add", sourceRemoteName, sourceRepoPath], {
+ cwd: targetRepoPath,
+ });
+ isTargetRemoteSetup = true;
+ } catch (err) {
+ const error = err as Error;
+ if (error.message.includes("already exists")) {
+ isTargetRemoteSetup = true;
+ } else {
+ isTargetRemoteSetup = false;
+ }
+ console.warn(
+ `Could not add remote "${sourceRemoteName}" pointing to "${sourceRepoPath}" in repository at: "${targetRepoPath}".\n\nError: ${error.message}`
+ );
+ }
+
+ if (isTargetRemoteSetup === false) {
+ return [];
+ }
+
+ try {
+ const gitDiffRefsProcess = spawn(
+ "git",
+ [
+ "diff",
+ `${sourceRemoteName}/${sourceFromBranch}..${targetDestBranch}`,
+ ],
+ {
+ cwd: targetRepoPath,
+ }
+ );
+
+ const gitDiffRefsResult = await new Promise<string>((resolve, reject) => {
+ let buffer = [] as string[];
+ gitDiffRefsProcess.stdout.on("data", (data) => buffer.push(data));
+ gitDiffRefsProcess.stderr.on("data", (data) => {
+ reject(new Error(Buffer.from(data).toString("utf-8")));
+ });
+ gitDiffRefsProcess.stdout.on("close", () => {
+ resolve(buffer.join(""));
+ });
+ });
+
+ return parseDiff(gitDiffRefsResult, { findRenames: true });
+ } catch (err) {
+ const error = err as Error;
+ console.warn(
+ `Could not get diff between local branch "${targetDestBranch}" and remote branch "${sourceRemoteName}/${sourceFromBranch}" in repository at: "${targetRepoPath}".\n\nError: ${error.message}`
+ );
+ return [];
+ }
+ };
+};
+
+export default makeGetRepositoryRemoteRefDiff;
@@ -19,6 +19,7 @@ import { default as makeGetRepositoryHTTPCloneUrl } from "./getRepositoryHTTPClo
import { default as makeGetRepositorySSHCloneUrl } from "./getRepositorySSHCloneUrl";
import { default as makeGetRepositoryObject } from "./getRepositoryObject";
import { default as makeGetRepositoryRefDiff } from "./getRepositoryRefDiff";
+import { default as makeGetRepositoryRemoteRefDiff } from "./getRepositoryRemoteRefDiff";
import { default as makeGetRepositoryTags } from "./getRepositoryTags";
import { default as makeIsFileInRepositoryPath } from "./isFileInRepositoryPath";
@@ -42,6 +43,7 @@ export const makeRepositoryService = makeService<
getRepositorySSHCloneUrl: makeGetRepositorySSHCloneUrl,
getRepositoryObject: makeGetRepositoryObject,
getRepositoryRefDiff: makeGetRepositoryRefDiff,
+ getRepositoryRemoteRefDiff: makeGetRepositoryRemoteRefDiff,
getRepositoryTags: makeGetRepositoryTags,
isFileInRepositoryPath: makeIsFileInRepositoryPath,
});
@@ -98,6 +98,12 @@ export interface RepositoryServiceAPI extends ServiceApiContract {
refA: string,
refB?: string
): Promise<RepositoryFileDiff[]>;
+ getRepositoryRemoteRefDiff(
+ sourceRepo: Repository,
+ sourceFromBranch: string,
+ targetRepo: Repository,
+ targetDestBranch: string
+ ): Promise<RepositoryFileDiff[]>;
getRepositoryTags(repository: Repository): Promise<string[]>;
isFileInRepositoryPath(
repository: Repository,