Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 61 additions & 19 deletions actions/setup/js/generate_git_patch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,54 @@

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { promisify } = require("util");
const { exec: execCallback } = require("child_process");

const { getBaseBranch } = require("./get_base_branch.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");

/**
* Execute a git command - works in both github-script and MCP server contexts
* @param {string} command - The git command to execute
* @param {Object} options - Options including cwd
* @returns {Promise<{stdout: string, stderr: string, exitCode: number}>} The result
*/
async function execGit(command, options = {}) {
const { cwd } = options;

// When running in github-script context, exec is available as a global
if (typeof exec !== "undefined" && exec.getExecOutput) {
const args = command.split(" ");
const cmd = args[0];
const cmdArgs = args.slice(1);
const result = await exec.getExecOutput(cmd, cmdArgs, { cwd, ignoreReturnCode: true });
if (result.exitCode !== 0) {
const error = new Error(`Command failed: ${command}`);
// @ts-ignore - Adding exitCode property to Error
error.exitCode = result.exitCode;
throw error;
}
return result;
}

// Fallback to promisified child_process.exec for MCP server context
const execAsync = promisify(execCallback);
try {
const result = await execAsync(command, { cwd });
return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
} catch (error) {
// @ts-ignore - Adding exitCode property to Error
error.exitCode = error.code || 1;
throw error;
}
}

/**
* Generates a git patch file for the current changes
* @param {string} branchName - The branch name to generate patch for
* @returns {Object} Object with patch info or error
* @returns {Promise<Object>} Object with patch info or error
*/
function generateGitPatch(branchName) {
async function generateGitPatch(branchName) {
const patchPath = "/tmp/gh-aw/aw.patch";
const cwd = process.env.GITHUB_WORKSPACE || process.cwd();
const defaultBranch = process.env.DEFAULT_BRANCH || getBaseBranch();
Expand All @@ -33,29 +70,34 @@ function generateGitPatch(branchName) {
if (branchName) {
// Check if the branch exists locally
try {
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd, encoding: "utf8" });
try {
await execGit(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd });
} catch (showRefError) {
// Branch doesn't exist, skip to strategy 2
throw showRefError;
}

// Determine base ref for patch generation
let baseRef;
try {
// Check if origin/branchName exists
execSync(`git show-ref --verify --quiet refs/remotes/origin/${branchName}`, { cwd, encoding: "utf8" });
await execGit(`git show-ref --verify --quiet refs/remotes/origin/${branchName}`, { cwd });
baseRef = `origin/${branchName}`;
} catch {
// Use merge-base with default branch
execSync(`git fetch origin ${defaultBranch}`, { cwd, encoding: "utf8" });
baseRef = execSync(`git merge-base origin/${defaultBranch} ${branchName}`, { cwd, encoding: "utf8" }).trim();
await execGit(`git fetch origin ${defaultBranch}`, { cwd });
const mergeBaseResult = await execGit(`git merge-base origin/${defaultBranch} ${branchName}`, { cwd });
baseRef = mergeBaseResult.stdout.trim();
}

// Count commits to be included
const commitCount = parseInt(execSync(`git rev-list --count ${baseRef}..${branchName}`, { cwd, encoding: "utf8" }).trim(), 10);
const commitCountResult = await execGit(`git rev-list --count ${baseRef}..${branchName}`, { cwd });
const commitCount = parseInt(commitCountResult.stdout.trim(), 10);

if (commitCount > 0) {
// Generate patch from the determined base to the branch
const patchContent = execSync(`git format-patch ${baseRef}..${branchName} --stdout`, {
cwd,
encoding: "utf8",
});
const patchContentResult = await execGit(`git format-patch ${baseRef}..${branchName} --stdout`, { cwd });
const patchContent = patchContentResult.stdout;

if (patchContent && patchContent.trim()) {
fs.writeFileSync(patchPath, patchContent, "utf8");
Expand All @@ -69,7 +111,8 @@ function generateGitPatch(branchName) {

// Strategy 2: Check if commits were made to current HEAD since checkout
if (!patchGenerated) {
const currentHead = execSync("git rev-parse HEAD", { cwd, encoding: "utf8" }).trim();
const currentHeadResult = await execGit("git rev-parse HEAD", { cwd });
const currentHead = currentHeadResult.stdout.trim();

if (!githubSha) {
errorMessage = "GITHUB_SHA environment variable is not set";
Expand All @@ -78,17 +121,16 @@ function generateGitPatch(branchName) {
} else {
// Check if GITHUB_SHA is an ancestor of current HEAD
try {
execSync(`git merge-base --is-ancestor ${githubSha} HEAD`, { cwd, encoding: "utf8" });
await execGit(`git merge-base --is-ancestor ${githubSha} HEAD`, { cwd });

// Count commits between GITHUB_SHA and HEAD
const commitCount = parseInt(execSync(`git rev-list --count ${githubSha}..HEAD`, { cwd, encoding: "utf8" }).trim(), 10);
const commitCountResult = await execGit(`git rev-list --count ${githubSha}..HEAD`, { cwd });
const commitCount = parseInt(commitCountResult.stdout.trim(), 10);

if (commitCount > 0) {
// Generate patch from GITHUB_SHA to HEAD
const patchContent = execSync(`git format-patch ${githubSha}..HEAD --stdout`, {
cwd,
encoding: "utf8",
});
const patchContentResult = await execGit(`git format-patch ${githubSha}..HEAD --stdout`, { cwd });
const patchContent = patchContentResult.stdout;

if (patchContent && patchContent.trim()) {
fs.writeFileSync(patchPath, patchContent, "utf8");
Expand Down
16 changes: 8 additions & 8 deletions actions/setup/js/generate_git_patch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("generateGitPatch", () => {

const { generateGitPatch } = await import("./generate_git_patch.cjs");

const result = generateGitPatch(null);
const result = await generateGitPatch(null);

expect(result.success).toBe(false);
expect(result).toHaveProperty("error");
Expand All @@ -43,7 +43,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_WORKSPACE = "/tmp/nonexistent-repo";
process.env.GITHUB_SHA = "abc123";

const result = generateGitPatch("nonexistent-branch");
const result = await generateGitPatch("nonexistent-branch");

expect(result.success).toBe(false);
expect(result).toHaveProperty("error");
Expand All @@ -57,7 +57,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_SHA = "abc123";

// Even if it fails, it should try to create the directory
const result = generateGitPatch("test-branch");
const result = await generateGitPatch("test-branch");

expect(result).toHaveProperty("patchPath");
expect(result.patchPath).toBe("/tmp/gh-aw/aw.patch");
Expand All @@ -69,7 +69,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_WORKSPACE = "/tmp/nonexistent-repo";
process.env.GITHUB_SHA = "abc123";

const result = generateGitPatch("test-branch");
const result = await generateGitPatch("test-branch");

expect(result).toHaveProperty("success");
expect(result).toHaveProperty("patchPath");
Expand All @@ -82,7 +82,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_WORKSPACE = "/tmp/nonexistent-repo";
process.env.GITHUB_SHA = "abc123";

const result = generateGitPatch(null);
const result = await generateGitPatch(null);

expect(result).toHaveProperty("success");
expect(result).toHaveProperty("patchPath");
Expand All @@ -94,7 +94,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_WORKSPACE = "/tmp/nonexistent-repo";
process.env.GITHUB_SHA = "abc123";

const result = generateGitPatch("");
const result = await generateGitPatch("");

expect(result).toHaveProperty("success");
expect(result).toHaveProperty("patchPath");
Expand All @@ -107,7 +107,7 @@ describe("generateGitPatch", () => {
process.env.GITHUB_SHA = "abc123";
process.env.DEFAULT_BRANCH = "develop";

const result = generateGitPatch("feature-branch");
const result = await generateGitPatch("feature-branch");

expect(result).toHaveProperty("success");
// Should attempt to use develop as default branch
Expand All @@ -121,7 +121,7 @@ describe("generateGitPatch", () => {
delete process.env.DEFAULT_BRANCH;
process.env.GH_AW_BASE_BRANCH = "master";

const result = generateGitPatch("feature-branch");
const result = await generateGitPatch("feature-branch");

expect(result).toHaveProperty("success");
// Should attempt to use master as base branch
Expand Down
8 changes: 4 additions & 4 deletions actions/setup/js/safe_outputs_handlers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ function createHandlers(server, appendSafeOutput, config = {}) {
* Resolves the current branch if branch is not provided or is the base branch
* Generates git patch for the changes (unless allow-empty is true)
*/
const createPullRequestHandler = args => {
const createPullRequestHandler = async args => {
const entry = { ...args, type: "create_pull_request" };
const baseBranch = getBaseBranch();

Expand Down Expand Up @@ -226,7 +226,7 @@ function createHandlers(server, appendSafeOutput, config = {}) {

// Generate git patch
server.debug(`Generating patch for create_pull_request with branch: ${entry.branch}`);
const patchResult = generateGitPatch(entry.branch);
const patchResult = await generateGitPatch(entry.branch);

if (!patchResult.success) {
// Patch generation failed or patch is empty
Expand Down Expand Up @@ -276,7 +276,7 @@ function createHandlers(server, appendSafeOutput, config = {}) {
* Resolves the current branch if branch is not provided or is the base branch
* Generates git patch for the changes
*/
const pushToPullRequestBranchHandler = args => {
const pushToPullRequestBranchHandler = async args => {
const entry = { ...args, type: "push_to_pull_request_branch" };
const baseBranch = getBaseBranch();

Expand All @@ -296,7 +296,7 @@ function createHandlers(server, appendSafeOutput, config = {}) {

// Generate git patch
server.debug(`Generating patch for push_to_pull_request_branch with branch: ${entry.branch}`);
const patchResult = generateGitPatch(entry.branch);
const patchResult = await generateGitPatch(entry.branch);

if (!patchResult.success) {
// Patch generation failed or patch is empty
Expand Down