feat(pulls): add merge pull request@@ -144,7 +144,11 @@ RUN echo "" > /etc/motd
# Change to git user home dir
WORKDIR /home/git/
-# Add git-shell command no-interactive-login
+# Set committer id of this machine
+RUN git config --global user.name "GitFOSS Agent (system)"
+RUN git config --global user.email "git@gitfoss.dev"
+
+# Add git-shell command no-interactive-login
RUN mkdir git-shell-commands/
COPY ./data/git-shell-commands/no-interactive-login /home/git/git-shell-commands/no-interactive-login
RUN chown git:git git-shell-commands/no-interactive-login
@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1778495079942,
+ "_generatedAtUnix": 1778513881558,
"_hashAlgorithm": "sha1",
"_version": 2,
"assets": {
@@ -70,7 +70,7 @@
"pathSourceMap": "./public/.islands/RepositoryForkForm.bundle.js.map"
},
"RepositoryHero": {
- "hash": "b0ab4c086a536112a3d6bc3ff3c6c65d29d25288",
+ "hash": "e291deada81cf96f29cc858d9e686c4f4a0bbe10",
"pathSource": "./app/islands/RepositoryHero.tsx",
"pathBundle": "./public/.islands/RepositoryHero.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryHero.bundle.js.map"
@@ -122,7 +122,7 @@
"pathSource": "./app/views/organization/OrganizationDetailsView.tsx"
},
"RepositoryBrowserView": {
- "hash": "570126c6a07d3a866ad7554527d4b3de55879607",
+ "hash": "6c617e1ebda50ebcf3edf3b644185ac5c687a580",
"pathSource": "./app/views/repository/RepositoryBrowserView.tsx"
},
"RepositoryCommitsLogView": {
@@ -158,7 +158,7 @@
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestCreateView.tsx"
},
"RepositoryPullRequestDetailsView": {
- "hash": "c2971a7253f86686c3f0b74570274dcb5be75af1",
+ "hash": "01d5ce1ba9934f396ac51b24721d991846cbd667",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx"
},
"RepositoryPullRequestsView": {
@@ -10,6 +10,7 @@ import { NamedColors } from "../utils/style";
import { type CommonViewProps, type WithThemeSchemeProp } from "../types";
import { buildRouteLink } from "../utils/shared";
import { AppRoute } from "../routes.defs";
+import { TextEllipsis } from "./TextEllipsis.styled";
export const DrawerPrimary = ({
visible = false,
@@ -19,6 +20,13 @@ export const DrawerPrimary = ({
repoSlug,
currentRef = Const.DEFAULT_HEAD_REF,
path = "/",
+ counters = {
+ pulls: 0,
+ tests: 0,
+ builds: 0,
+ issues: 0,
+ apiRefSymbols: 0,
+ },
}: WithThemeSchemeProp & {
visible: boolean;
commonProps: CommonViewProps;
@@ -26,6 +34,14 @@ export const DrawerPrimary = ({
repoSlug: string;
currentRef?: string;
path?: string;
+ counters?: {
+ pulls?: number;
+ tests?: number;
+ builds?: number;
+ issues?: number;
+ apiRefSymbols?: number;
+ helpCenterNotifs?: number;
+ };
}) => {
const pathRepo = buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
orgSlug: orgSlug,
@@ -67,18 +83,30 @@ export const DrawerPrimary = ({
</StyledDrawerHeader>
<StyledDrawerContent>
<StyledDrawerListHeader>
- <span>acme-org</span>
- <span>/</span>
- <span>my-app</span>
+ <a href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, { orgSlug })}>
+ {orgSlug}
+ </a>
+ <span>{" / "}</span>
+ <a
+ href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug,
+ repoSlug,
+ })}
+ >
+ <TextEllipsis>{repoSlug}</TextEllipsis>
+ </a>
</StyledDrawerListHeader>
<StyledDrawerList>
<StyledDrawerListItem
themeScheme={themeScheme}
href={pathFiles}
className={
- [pathFiles, pathRepo, pathRepoTrailing].includes(
- commonProps.path || "/",
- )
+ (commonProps.path!.startsWith(pathFiles) ||
+ [pathFiles, pathRepo, pathRepoTrailing].some(
+ (p) =>
+ commonProps.path === p || commonProps.path!.startsWith(p),
+ )) &&
+ commonProps.path!.startsWith(pathPulls) === false
? "active"
: undefined
}
@@ -88,26 +116,31 @@ export const DrawerPrimary = ({
<StyledDrawerListItem
themeScheme={themeScheme}
href={pathPulls}
- className={commonProps.path === pathPulls ? "active" : undefined}
+ className={
+ commonProps.path! === pathPulls ||
+ commonProps.path!.startsWith(pathPulls)
+ ? "active"
+ : undefined
+ }
>
<span>Pull Requests</span>
- <Chip>0</Chip>
+ <Chip>{counters.pulls || 0}</Chip>
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>Tests & Coverage</span>
- <Chip>0</Chip>
+ <Chip>{counters.tests || 0}</Chip>
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>Builds</span>
- <Chip>0</Chip>
+ <Chip>{counters.builds || 0}</Chip>
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>Issues</span>
- <Chip>0</Chip>
+ <Chip>{counters.issues || 0}</Chip>
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>API Reference</span>
- <Chip>0</Chip>
+ <Chip>{counters.apiRefSymbols || 0}</Chip>
</StyledDrawerListItem>
</StyledDrawerList>
<StyledDrawerListHeader></StyledDrawerListHeader>
@@ -120,6 +153,9 @@ export const DrawerPrimary = ({
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>Help Center</span>
+ {counters.helpCenterNotifs! > 0 && (
+ <Chip>{counters.helpCenterNotifs}</Chip>
+ )}
</StyledDrawerListItem>
<StyledDrawerListItem themeScheme={themeScheme} disabled>
<span>Settings</span>
@@ -203,15 +239,20 @@ const StyledDrawerContent = styled.main`
const StyledDrawerListHeader = styled.section`
width: 100%;
- height: 40px;
+ min-height: 40px;
display: flex;
+ flex-flow: row wrap;
justify-content: center;
align-items: center;
font-weight: bold;
- margin-bottom: 4px;
+ margin-bottom: 12px;
+
+ & > span {
+ margin: 0 4px;
+ }
`;
const StyledDrawerList = styled.section`
@@ -1,5 +1,5 @@
// 3rd-party
-import React, { FC } from "react";
+import React, { FC, useState } from "react";
import styled, { css } from "styled-components";
// app
import type { CommonViewProps, WithThemeSchemeProp } from "../types";
@@ -45,6 +45,9 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
themeScheme,
};
+ const [drawerPrimaryOpen, setDrawerPrimaryOpen] =
+ useState<boolean>(showDrawerPrimary);
+
return (
<>
<style
@@ -98,7 +101,7 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
<DrawerPrimary
commonProps={commonProps as any}
themeScheme={themeScheme}
- visible={showDrawerPrimary}
+ visible={drawerPrimaryOpen}
orgSlug={orgSlug}
repoSlug={repoSlug}
currentRef={currentRef}
@@ -106,13 +109,14 @@ export const Layout: FC<LayoutProps & WithThemeSchemeProp> = (commonProps) => {
/>
<StyledChildrenWrapper
{...sharedProps}
- showDrawerPrimary={showDrawerPrimary}
+ showDrawerPrimary={drawerPrimaryOpen}
>
<StyledPageHeaderWrapper {...sharedProps}>
<PageHeader
commonProps={commonProps}
themeScheme={themeScheme}
forceShowLogo={showDrawerPrimary !== true}
+ setDrawerPrimaryOpen={setDrawerPrimaryOpen}
/>
</StyledPageHeaderWrapper>
{children}
@@ -11,15 +11,23 @@ import { PageWrapper } from "./PageWrapper";
interface PageHeaderProps extends CommonProps {
forceShowLogo?: boolean;
+ setDrawerPrimaryOpen?: (predicate: (prev: boolean) => boolean) => void;
}
export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
commonProps,
themeScheme,
forceShowLogo = true,
+ setDrawerPrimaryOpen = undefined,
}) => {
const invertThemeScheme = themeScheme === "light" ? "dark" : "light";
+ const toggleDrawerPrimary = () => {
+ if (setDrawerPrimaryOpen) {
+ setDrawerPrimaryOpen((prev) => !prev);
+ }
+ };
+
const pageHeaderActions = useMemo(() => {
if (commonProps.authenticated) {
return (
@@ -65,6 +73,10 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
return (
<StyledPageHeader themeScheme={themeScheme}>
<PageWrapper>
+ <StyledBurgerMenu
+ themeScheme={themeScheme}
+ onClick={toggleDrawerPrimary}
+ />
<StyledLogoArea themeScheme={themeScheme} forceShowLogo={forceShowLogo}>
<a href={"/"}>
<h1>{Const.APP_NAME}</h1>
@@ -130,6 +142,25 @@ export const PageHeader: VFC<PageHeaderProps & WithThemeSchemeProp> = ({
);
};
+const StyledBurgerMenu = styled.button<WithThemeSchemeProp>`
+ ${({ themeScheme }) => css`
+ /* above mobile size */
+ @media only screen and (min-width: 768px) {
+ & {
+ display: none;
+ }
+ }
+
+ width: 40px;
+ height: 40px;
+ border-image: none;
+ border: none;
+ border-radius: 20px;
+ background: ${NamedColors.CARD_OVERLAY[themeScheme]};
+ /*color: red;*/
+ `}
+`;
+
const StyledPageHeader = styled.header<WithThemeSchemeProp>`
display: flex;
flex-flow: row nowrap;
@@ -10,7 +10,7 @@ export const PageWrapper = styled.div`
width: 100%;
margin: 0 auto;
- padding: 24px 16px 64px 16px;
+ padding: 16px 16px 64px 16px;
@media only screen and (min-width: 1260px) {
max-width: 1260px;
@@ -10,9 +10,11 @@ import { makePullRequestService } from "../../services/pullRequest";
import { makeRepositoryService } from "../../services/repository";
import { makeUsersService } from "../../services/user";
// app views
-import RepositoryPullRequestsView, {
- RepositoryPullRequestsViewProps,
-} from "../../views/repositoryPullRequests/RepositoryPullRequestsView";
+// import RepositoryPullRequestsView, {
+// RepositoryPullRequestsViewProps,
+// } from "../../views/repositoryPullRequests/RepositoryPullRequestsView";
+// new merge service
+import { buildRouteLink } from "../../utils/shared";
const postRepositoryPullRequestMergeAction: ReqHandler<
AppRouteParams,
@@ -40,7 +42,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
const pullRequest = await prService.getPullRequestByUid(
orgSlug,
repoSlug,
- pullUid
+ pullUid,
);
if (pullRequest == null) {
@@ -48,7 +50,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
}
const sourceRepo = await repoService.getRepositoryById(
- pullRequest.sourceRepositoryId
+ pullRequest.sourceRepositoryId,
);
if (sourceRepo == null) {
@@ -56,7 +58,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
}
const targetRepo = await repoService.getRepositoryById(
- pullRequest.targetRepositoryId
+ pullRequest.targetRepositoryId,
);
if (targetRepo == null) {
@@ -64,7 +66,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
}
const sourceParentOrg = await orgService.getOrganizationById(
- sourceRepo.organizationId
+ sourceRepo.organizationId,
);
if (sourceParentOrg == null) {
@@ -83,7 +85,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
}
const targetParentOrg = await orgService.getOrganizationById(
- targetRepo.organizationId
+ targetRepo.organizationId,
);
if (targetParentOrg == null) {
@@ -108,14 +110,14 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
fileDiffs = await repoService.getRepositoryRefDiff(
sourceRepo,
pullRequest.targetBranch,
- pullRequest.sourceBranch
+ pullRequest.sourceBranch,
);
} else {
fileDiffs = await repoService.getRepositoryRemoteRefDiff(
sourceRepo,
pullRequest.sourceBranch,
targetRepo,
- pullRequest.targetBranch
+ pullRequest.targetBranch,
);
}
@@ -124,17 +126,34 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
throw new Error("Cannot merge two branches without difference.");
}
- // 3. Do the merge !
+ // 3. Do the merge via service method
+ try {
+ const result = await prService.mergePullRequest({
+ pullRequestId: pullRequest.id,
+ mergeMessage: merge_message ?? undefined,
+ });
- const reqHandler = reply.makeRequestHandler(request, reply);
- return reqHandler<RepositoryPullRequestsViewProps>(
- RepositoryPullRequestsView.name,
- {
- // parentOrg,
- pullRequest,
- // repo,
+ if (result.success !== true) {
+ return reply.status(500).send({ error: "Merge failed" });
}
- );
+
+ const updatedPull = result.updatedPullRequest || pullRequest;
+
+ // Redirect to PR details page after merge
+ return reply.redirect(
+ 303,
+ buildRouteLink(AppRoute.REPOSITORY_PULL_REQUEST_DETAILS, {
+ orgSlug,
+ repoSlug,
+ pullUid: updatedPull.uid,
+ }),
+ );
+ } catch (err) {
+ console.error("Merge action failed:", (err as Error).message);
+ return reply
+ .status(500)
+ .send({ error: "Merge action failed", detail: (err as Error).message });
+ }
};
export default postRepositoryPullRequestMergeAction;
@@ -27,15 +27,20 @@ const RepositoryHero: ReactIsland<RepositoryHeroProps> = ({
forkedFromRepo = null,
forksCount = 0,
path = undefined,
- separator = "∙",
+ // separator = "∙",
showForkButton = true,
}) => {
return (
<Grid.Col fluid gap={16}>
<Grid.Row fluid alignItems={"center"}>
- <Grid.Col nowrap flex={"1 0 468px"} style={{ minWidth: 468 }}>
- <h1 style={{ margin: 0 }}>
+ <Grid.Col
+ nowrap
+ flex={"1 0 468px"}
+ style={{ minWidth: 468, marginBottom: 12 }}
+ >
+ <h2 style={{ margin: 0 }}>
<a
+ style={{ whiteSpace: "nowrap" }}
href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, {
orgSlug: parentOrg.slug,
})}
@@ -44,6 +49,7 @@ const RepositoryHero: ReactIsland<RepositoryHeroProps> = ({
</a>
{" / "}
<a
+ style={{ whiteSpace: "nowrap" }}
href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
@@ -51,15 +57,12 @@ const RepositoryHero: ReactIsland<RepositoryHeroProps> = ({
>
{repo.displayName || repo.slug}
</a>
- {` ${separator} `}
- {path == null ? (
- <span style={{ textTransform: "capitalize", fontSize: 16 }}>
- ({repo.visibility.toLowerCase()})
- </span>
- ) : (
- <span>{path}</span>
- )}
- </h1>
+ </h2>
+ <div style={{ flex: 1 }}>
+ <h3 style={{ whiteSpace: "nowrap", margin: 0, marginTop: 4 }}>
+ {path || "Files"}
+ </h3>
+ </div>
<div style={{ flex: 1 }}>
{repo.isFork && forkedFromRepo != null && (
<h5 style={{ margin: 0, marginTop: 8 }}>
@@ -117,16 +120,6 @@ const RepositoryHero: ReactIsland<RepositoryHeroProps> = ({
</ButtonAnchor>
)}
</Grid.Row>
- <Grid.Col fluid nowrap gap={16}>
- <a
- href={buildRouteLink(AppRoute.REPOSITORY_PULL_REQUESTS, {
- orgSlug: parentOrg.slug,
- repoSlug: repo.slug,
- })}
- >
- Pull Requests
- </a>
- </Grid.Col>
</Grid.Col>
);
};
@@ -3,17 +3,19 @@ import { makeService } from "@ethicdevs/react-monolith";
// service types
import type { PullRequestServiceDeps, PullRequestServiceAPI } from "./types";
// service methods
-import { default as makeCreatePullRequest } from "./createPullRequest";
import { default as makeGetPullRequestById } from "./getPullRequestById";
import { default as makeGetPullRequestByUid } from "./getPullRequestByUid";
import { default as makeGetPullRequestsInRepository } from "./getPullRequestsInRepository";
+import { default as makeCreatePullRequest } from "./createPullRequest";
+import { default as makeMergePullRequest } from "./mergePullRequest";
export const makePullRequestService = makeService<
PullRequestServiceAPI,
PullRequestServiceDeps
>({
- createPullRequest: makeCreatePullRequest,
getPullRequestById: makeGetPullRequestById,
getPullRequestByUid: makeGetPullRequestByUid,
getPullRequestsInRepository: makeGetPullRequestsInRepository,
+ createPullRequest: makeCreatePullRequest,
+ mergePullRequest: makeMergePullRequest,
});
@@ -0,0 +1,186 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// 3rd-party
+import { join } from "node:path";
+import { mkdtemp, rm } from "fs/promises";
+import { spawn } from "node:child_process";
+import os from "os";
+// generated via script[generate:prisma]
+import type { Repository } from "@prisma/client";
+// app
+import { Env } from "../../env";
+import type {
+ MergePullRequestDTO,
+ MergePullRequestResult,
+ PullRequestServiceDeps,
+ // PullRequestServiceAPI,
+} from "./types";
+
+// Implementation of a merge operation for a pull request using a temporary working copy
+// This service is deliberately conservative and uses the standard merge workflow by default.
+// It does not attempt to squash or rebase unless you extend the DTO.
+
+export const makeMergePullRequest: ServiceMethodFactory<
+ PullRequestServiceDeps,
+ [MergePullRequestDTO],
+ Promise<MergePullRequestResult>
+> = ({ request }) => {
+ return async (dto) => {
+ // fetch pull request with related repos and their organizations to resolve paths
+ const pr = await request.prisma.pullRequest.findUnique({
+ where: { id: dto.pullRequestId },
+ include: {
+ sourceRepository: {
+ include: {
+ organization: true,
+ },
+ },
+ targetRepository: {
+ include: {
+ organization: true,
+ },
+ },
+ },
+ });
+
+ if (pr == null) {
+ throw new Error("Pull request not found");
+ }
+
+ const sourceRepo: Repository | null = pr.sourceRepository ?? null;
+ const targetRepo: Repository | null = pr.targetRepository ?? null;
+
+ if (sourceRepo == null || targetRepo == null) {
+ throw new Error("Invalid pull request repositories");
+ }
+
+ const sourceOrgSlug = pr.sourceRepository.organization.slug ?? null;
+ const targetOrgSlug = pr.targetRepository.organization.slug ?? null;
+
+ if (!sourceOrgSlug || !targetOrgSlug) {
+ throw new Error("Could not resolve repository organizations");
+ }
+
+ // bare repo paths
+ const sourceBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${sourceOrgSlug}/${sourceRepo.slug}.git`;
+ const targetBarePath = `${Env.GIT_REPOSITORIES_ROOT}/${targetOrgSlug}/${targetRepo.slug}.git`;
+
+ // ensure bare repos exist
+ // Best-effort check; actual fs check is optional here, rely on git failing gracefully otherwise
+
+ // create a temporary working directory
+ const tmpDir = await mkdtemp(join(os.tmpdir(), "gitfoss-merge-"));
+
+ try {
+ // clone target bare repo into working copy
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn("git", ["clone", targetBarePath, tmpDir], {
+ cwd: undefined,
+ env: { LANG: "C" },
+ });
+ let err = "";
+ c.stderr.on("data", (d) => (err += d.toString()));
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(err)),
+ );
+ });
+
+ // add source as remote
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn("git", ["remote", "add", "source", sourceBarePath], {
+ cwd: tmpDir,
+ env: { LANG: "C" },
+ });
+ let err = "";
+ c.stderr.on("data", (d) => (err += d.toString()));
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(err)),
+ );
+ });
+
+ // fetch source branch
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn("git", ["fetch", "source", pr.sourceBranch], {
+ cwd: tmpDir,
+ env: { LANG: "C" },
+ });
+ let err = "";
+ c.stderr.on("data", (d) => (err += d.toString()));
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(err)),
+ );
+ });
+
+ // checkout target branch
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn("git", ["checkout", pr.targetBranch], {
+ cwd: tmpDir,
+ env: { LANG: "C" },
+ });
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error("checkout failed")),
+ );
+ });
+
+ // merge
+ const mergeArgs = ["merge", "--no-ff", `source/${pr.sourceBranch}`];
+ if (dto.mergeMessage && dto.mergeMessage.trim() !== "") {
+ mergeArgs.push("-m", dto.mergeMessage);
+ }
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn("git", mergeArgs, {
+ cwd: tmpDir,
+ env: { LANG: "C" },
+ });
+ let err = "";
+ c.stderr.on("data", (d) => (err += d.toString()));
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(err || "merge failed")),
+ );
+ });
+
+ // push merged result back to target bare repo
+ await new Promise<void>((resolve, reject) => {
+ const c = spawn(
+ "git",
+ ["push", targetBarePath, `${pr.targetBranch}:${pr.targetBranch}`],
+ {
+ cwd: tmpDir,
+ env: { LANG: "C" },
+ },
+ );
+ let err = "";
+ c.stderr.on("data", (d) => (err += d.toString()));
+ c.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(err || "push failed")),
+ );
+ });
+
+ // update PR as merged
+ const updatedPR = await request.prisma.pullRequest.update({
+ where: { id: pr.id },
+ data: {
+ state: (require("@prisma/client").PullRequestState as any)
+ .CLOSE_MERGED,
+ closedAt: new Date(),
+ },
+ });
+
+ return {
+ success: true,
+ updatedPullRequest: updatedPR,
+ };
+ } catch (err) {
+ throw err;
+ } finally {
+ // cleanup
+ try {
+ await rm(tmpDir, { recursive: true, force: true });
+ } catch {
+ // ignore cleanup errors
+ }
+ }
+ };
+};
+
+export default makeMergePullRequest;
@@ -2,7 +2,7 @@
import type { ServiceApiContract } from "@ethicdevs/react-monolith";
// 3rd-party
import type { FastifyRequest } from "fastify";
-// generated via script[prisma:generate]
+// generated via script[generate via prisma]
import { Prisma, PullRequest, Repository, User } from "@prisma/client";
export interface CreatePullRequestDTO {
@@ -23,22 +23,34 @@ export type PullRequestSelectOrIncludes =
| { select?: Prisma.PullRequestSelect }
| { includes?: Prisma.PullRequestInclude };
+export interface MergePullRequestDTO {
+ pullRequestId: string;
+ mergeMessage?: string;
+ // could extend with mergeStrategy in future (e.g., merge, squash, rebase)
+}
+
+export interface MergePullRequestResult {
+ success: boolean;
+ updatedPullRequest?: PullRequest;
+}
+
export interface PullRequestServiceAPI extends ServiceApiContract {
getPullRequestById(
pullRequestId: string,
- selectOrIncludes?: PullRequestSelectOrIncludes
+ selectOrIncludes?: PullRequestSelectOrIncludes,
): Promise<PullRequest | null>;
getPullRequestByUid<R = PullRequest | null>(
orgSlug: string,
repoSlug: string,
pullRequestUid: number,
- selectOrIncludes?: PullRequestSelectOrIncludes
+ selectOrIncludes?: PullRequestSelectOrIncludes,
): Promise<R>;
getPullRequestsInRepository(
repository: Repository,
- selectOrIncludes?: PullRequestSelectOrIncludes
+ selectOrIncludes?: PullRequestSelectOrIncludes,
): Promise<PullRequest[]>;
createPullRequest(dto: CreatePullRequestDTO): Promise<PullRequest>;
+ mergePullRequest(dto: MergePullRequestDTO): Promise<MergePullRequestResult>;
}
export interface PullRequestServiceDeps {
@@ -63,6 +63,7 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
orgSlug={parentOrg.slug}
repoSlug={repo.slug}
currentRef={currentRef}
+ path={path}
>
<PageWrapper>
<IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
@@ -242,15 +242,7 @@ const RepositoryPullRequestDetailsView: ReactView<
alignItems={"center"}
style={{ marginTop: 8 }}
>
- <button type={"submit"} name={"merge_default"}>
- Merge
- </button>
- <button type={"submit"} name={"merge_squash"}>
- Merge w/ Squash
- </button>
- <button type={"submit"} name={"merge_rebase"}>
- Merge w/ Rebase
- </button>
+ <button type={"submit"}>Merge</button>
</Grid.Row>
</Grid.Col>
</form>