feat(pr): allow to delete branch after merge
+ 73
- 15
app/controllers/repositoryPullRequests/postRepositoryPullRequestMergeAction.ts
@@ -21,11 +21,14 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
   AppRoute.REPOSITORY_PULL_REQUEST_MERGE_ACTION
 > = async (request, reply) => {
   const { orgSlug, repoSlug, pullUid } = request.params;
-  const { merge_message, merge_summary } = request.body;
+  const { merge_message, merge_summary, delete_source_branch } = request.body;
+
+  const shouldDeleteSourceBranch = delete_source_branch != null; // treat any presence as request to delete
 
   console.log("params:", {
     merge_message,
     merge_summary,
+    delete_source_branch,
   });
 
   const orgService = makeOrganizationService({ request });

...
@@ -131,6 +134,7 @@ const postRepositoryPullRequestMergeAction: ReqHandler<
     const result = await prService.mergePullRequest({
       pullRequestId: pullRequest.id,
       mergeMessage: merge_message ?? undefined,
+      deleteSourceBranch: shouldDeleteSourceBranch,
     });
 
     if (result.success !== true) {

@@ -312,6 +312,7 @@ export interface AppRouteParams {
     body: {
       merge_summary: string;
       merge_message: string;
+      delete_source_branch?: boolean;
     };
   };
   [AppRoute.REPOSITORY_PULL_REQUEST_UPDATE_ACTION]: {

app/services/gitServer/repositoryResolver.ts
@@ -40,13 +40,13 @@ const makeRepositoryResolver: ServiceMethodFactory<
 
     if (repo == null) {
       throw new Error(
-        `Unknown repository "${repo}" in (known) organization "${org}".`
+        `Unknown repository "${repoSlug}" in (known) organization "${org.slug}".`,
       );
     }
 
     // /!\ Notice how it use db's data instead of user's input to build path.
     const gitRepositoryDir = resolve(
-      join(Env.GIT_REPOSITORIES_ROOT, org.slug, repo.slug)
+      join(Env.GIT_REPOSITORIES_ROOT, org.slug, repo.slug),
     );
 
     if (

app/services/pullRequest/mergePullRequest.ts
@@ -85,6 +85,35 @@ export const makeMergePullRequest: ServiceMethodFactory<
         );
       });
 
+      // setup committer identity
+      await new Promise<void>((resolve, reject) => {
+        const c = spawn(
+          "git",
+          ["config", "user.name", "GitFOSS Agent (system)"],
+          {
+            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)),
+        );
+      });
+
+      await new Promise<void>((resolve, reject) => {
+        const c = spawn("git", ["config", "user.email", "git@gitfoss.dev"], {
+          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)),
+        );
+      });
+
       // add source as remote
       await new Promise<void>((resolve, reject) => {
         const c = spawn("git", ["remote", "add", "source", sourceBarePath], {

...
@@ -156,6 +185,25 @@ export const makeMergePullRequest: ServiceMethodFactory<
         );
       });
 
+      // Optional: delete source branch on the source repo if requested
+      if (dto.deleteSourceBranch === true) {
+        await new Promise<void>((resolve, reject) => {
+          const c = spawn(
+            "git",
+            ["push", sourceBarePath, "--delete", 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 || "delete source branch failed")),
+          );
+        });
+      }
+
       // update PR as merged
       const updatedPR = await request.prisma.pullRequest.update({
         where: { id: pr.id },

...
@@ -170,17 +218,6 @@ export const makeMergePullRequest: ServiceMethodFactory<
         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;

app/services/pullRequest/types.ts
@@ -26,6 +26,8 @@ export type PullRequestSelectOrIncludes =
 export interface MergePullRequestDTO {
   pullRequestId: string;
   mergeMessage?: string;
+  // delete the source branch in the source repository after merge if requested
+  deleteSourceBranch?: boolean;
   // could extend with mergeStrategy in future (e.g., merge, squash, rebase)
 }
 

app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx
@@ -244,6 +244,20 @@ const RepositoryPullRequestDetailsView: ReactView<
                     >
                       <button type={"submit"}>Merge</button>
                     </Grid.Row>
+                    {/* Delete source branch after merge checkbox */}
+                    <Grid.Col fluid style={{ marginTop: 8 }}>
+                      <label
+                        htmlFor={"delete_source_branch"}
+                        style={{ display: "block" }}
+                      >
+                        <input
+                          type={"checkbox"}
+                          id={"delete_source_branch"}
+                          name={"delete_source_branch"}
+                        />
+                        {" Delete source branch after merge"}
+                      </label>
+                    </Grid.Col>
                   </Grid.Col>
                 </form>
               </Grid.Col>