-
Notifications
You must be signed in to change notification settings - Fork 237
Fix push_to_pull_request_branch generating bad patch on follow-up issue_comment runs #17206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bd8a382
79e3b4b
4c21ea6
678937e
7227c4c
f714464
20ea234
5ff8086
98a6d6a
3bf047d
08de326
b546fad
abf44f4
9e83b36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,19 +8,157 @@ const { getBaseBranch } = require("./get_base_branch.cjs"); | |
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||
| const { execGitSync } = require("./git_helpers.cjs"); | ||
|
|
||
| const PATCH_PATH = "/tmp/gh-aw/aw.patch"; | ||
github-actions[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to extract |
||
|
|
||
| /** | ||
| * Resolves the base ref to use for patch generation against a named branch. | ||
| * Preference order: | ||
| * 1. Remote tracking ref refs/remotes/origin/<branch> (already fetched) | ||
| * 2. Fresh fetch of origin/<branch> (gh pr checkout path) | ||
| * 3. merge-base with origin/<defaultBranch> (brand-new branch) | ||
| * @param {string} branchName | ||
| * @param {string} defaultBranch | ||
| * @param {string} cwd | ||
| * @returns {string} baseRef | ||
| */ | ||
| function resolveBaseRef(branchName, defaultBranch, cwd) { | ||
pelikhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| execGitSync(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], { cwd }); | ||
| const baseRef = `origin/${branchName}`; | ||
pelikhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| core.info(`[generate_git_patch] using remote tracking ref as baseRef="${baseRef}"`); | ||
| return baseRef; | ||
| } catch { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The three-level fallback strategy in |
||
| // Remote tracking ref not found (e.g. after gh pr checkout which doesn't set tracking refs). | ||
| // Try fetching the branch from origin so we use only the NEW commits as the patch base. | ||
| core.info(`[generate_git_patch] refs/remotes/origin/${branchName} not found; fetching from origin`); | ||
| } | ||
|
|
||
| try { | ||
| execGitSync(["fetch", "origin", branchName], { cwd }); | ||
| const baseRef = `origin/${branchName}`; | ||
| core.info(`[generate_git_patch] fetch succeeded, baseRef="${baseRef}"`); | ||
| return baseRef; | ||
| } catch (fetchErr) { | ||
| // Distinguish between "branch doesn't exist on origin" and other fetch failures. | ||
| let branchExistsOnOrigin = false; | ||
| try { | ||
| // git ls-remote --exit-code origin refs/heads/<branch> succeeds only if the ref exists. | ||
| execGitSync(["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd }); | ||
| branchExistsOnOrigin = true; | ||
| } catch (lsRemoteErr) { | ||
| core.info(`[generate_git_patch] ls-remote did not find refs/heads/${branchName} on origin (${getErrorMessage(lsRemoteErr)}); treating as new branch`); | ||
| } | ||
|
|
||
| if (branchExistsOnOrigin) { | ||
| // The branch exists on origin, so this is a real fetch failure (auth/network/etc). | ||
| const message = `[generate_git_patch] fetch of origin/${branchName} failed, but branch exists on origin; cannot safely determine baseRef: ${getErrorMessage(fetchErr)}`; | ||
| core.error(message); | ||
| throw fetchErr; | ||
| } | ||
|
|
||
| // Branch doesn't exist on origin yet (new branch) – fall back to merge-base. | ||
| core.warning(`[generate_git_patch] origin does not have branch ${branchName}; falling back to merge-base with "${defaultBranch}"`); | ||
| } | ||
|
|
||
| execGitSync(["fetch", "origin", defaultBranch], { cwd }); | ||
| const baseRef = execGitSync(["merge-base", `origin/${defaultBranch}`, branchName], { cwd }).trim(); | ||
| core.info(`[generate_git_patch] merge-base baseRef="${baseRef}"`); | ||
| return baseRef; | ||
| } | ||
|
|
||
| /** | ||
| * Generates a git patch file for the current changes | ||
| * Writes a patch file for the range base..tip and returns whether it succeeded. | ||
| * @param {string} base - commit-ish for the base (exclusive) | ||
| * @param {string} tip - commit-ish for the tip (inclusive) | ||
| * @param {string} cwd | ||
| * @returns {boolean} true if the patch was written with content | ||
| */ | ||
| function writePatch(base, tip, cwd) { | ||
pelikhan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const commitCount = parseInt(execGitSync(["rev-list", "--count", `${base}..${tip}`], { cwd }).trim(), 10); | ||
| core.info(`[generate_git_patch] ${commitCount} commit(s) between ${base} and ${tip}`); | ||
|
|
||
| if (commitCount === 0) { | ||
| return false; | ||
| } | ||
|
|
||
| const patchContent = execGitSync(["format-patch", `${base}..${tip}`, "--stdout"], { cwd }); | ||
| if (!patchContent || !patchContent.trim()) { | ||
| core.warning(`[generate_git_patch] format-patch produced empty output for ${base}..${tip}`); | ||
| return false; | ||
| } | ||
|
|
||
| fs.writeFileSync(PATCH_PATH, patchContent, "utf8"); | ||
| core.info(`[generate_git_patch] patch written: ${patchContent.split("\n").length} lines, ${Math.ceil(Buffer.byteLength(patchContent, "utf8") / 1024)} KB`); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Strategy 1: generate a patch from the known remote state of branchName to | ||
| * its local tip, capturing only commits not yet on origin. | ||
| * @param {string} branchName | ||
| * @param {string} defaultBranch | ||
| * @param {string} cwd | ||
| * @returns {boolean} true if a patch was written | ||
| */ | ||
| function tryBranchStrategy(branchName, defaultBranch, cwd) { | ||
| core.info(`[generate_git_patch] Strategy 1: branch-based patch for "${branchName}"`); | ||
| try { | ||
| execGitSync(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd }); | ||
| } catch (err) { | ||
| core.info(`[generate_git_patch] local branch "${branchName}" not found: ${getErrorMessage(err)}`); | ||
| return false; | ||
| } | ||
|
|
||
| const baseRef = resolveBaseRef(branchName, defaultBranch, cwd); | ||
| return writePatch(baseRef, branchName, cwd); | ||
| } | ||
|
|
||
| /** | ||
| * Strategy 2: generate a patch from GITHUB_SHA to the current HEAD, capturing | ||
| * commits made by the agent after checkout. | ||
| * @param {string|undefined} githubSha | ||
| * @param {string} cwd | ||
| * @returns {{ generated: boolean, errorMessage: string|null }} | ||
| */ | ||
| function tryHeadStrategy(githubSha, cwd) { | ||
| const currentHead = execGitSync(["rev-parse", "HEAD"], { cwd }).trim(); | ||
| core.info(`[generate_git_patch] Strategy 2: HEAD="${currentHead}" GITHUB_SHA="${githubSha || ""}"`); | ||
|
|
||
| if (!githubSha) { | ||
| const msg = "GITHUB_SHA environment variable is not set"; | ||
| core.warning(`[generate_git_patch] ${msg}`); | ||
| return { generated: false, errorMessage: msg }; | ||
| } | ||
|
|
||
| if (currentHead === githubSha) { | ||
| core.info(`[generate_git_patch] HEAD matches GITHUB_SHA – no new commits`); | ||
| return { generated: false, errorMessage: null }; | ||
| } | ||
|
|
||
| try { | ||
| execGitSync(["merge-base", "--is-ancestor", githubSha, "HEAD"], { cwd }); | ||
| } catch { | ||
| core.warning(`[generate_git_patch] GITHUB_SHA is not an ancestor of HEAD – repository state has diverged`); | ||
| return { generated: false, errorMessage: null }; | ||
| } | ||
|
|
||
| const generated = writePatch(githubSha, "HEAD", cwd); | ||
| return { generated, errorMessage: null }; | ||
| } | ||
|
|
||
| /** | ||
| * 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 | ||
| */ | ||
| 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(); | ||
| const githubSha = process.env.GITHUB_SHA; | ||
|
|
||
| // Ensure /tmp/gh-aw directory exists | ||
| const patchDir = path.dirname(patchPath); | ||
| core.info(`[generate_git_patch] branchName="${branchName || ""}" GITHUB_SHA="${githubSha || ""}" defaultBranch="${defaultBranch}"`); | ||
|
|
||
| const patchDir = path.dirname(PATCH_PATH); | ||
| if (!fs.existsSync(patchDir)) { | ||
| fs.mkdirSync(patchDir, { recursive: true }); | ||
| } | ||
|
|
@@ -29,105 +167,49 @@ function generateGitPatch(branchName) { | |
| let errorMessage = null; | ||
|
|
||
| try { | ||
| // Strategy 1: If we have a branch name, check if that branch exists and get its diff | ||
| if (branchName) { | ||
| // Check if the branch exists locally | ||
| try { | ||
| execGitSync(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd }); | ||
|
|
||
| // Determine base ref for patch generation | ||
| let baseRef; | ||
| try { | ||
| // Check if origin/branchName exists | ||
| execGitSync(["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], { cwd }); | ||
| baseRef = `origin/${branchName}`; | ||
| } catch { | ||
| // Use merge-base with default branch | ||
| execGitSync(["fetch", "origin", defaultBranch], { cwd }); | ||
| baseRef = execGitSync(["merge-base", `origin/${defaultBranch}`, branchName], { cwd }).trim(); | ||
| } | ||
|
|
||
| // Count commits to be included | ||
| const commitCount = parseInt(execGitSync(["rev-list", "--count", `${baseRef}..${branchName}`], { cwd }).trim(), 10); | ||
|
|
||
| if (commitCount > 0) { | ||
| // Generate patch from the determined base to the branch | ||
| const patchContent = execGitSync(["format-patch", `${baseRef}..${branchName}`, "--stdout"], { cwd }); | ||
|
|
||
| if (patchContent && patchContent.trim()) { | ||
| fs.writeFileSync(patchPath, patchContent, "utf8"); | ||
| patchGenerated = true; | ||
| } | ||
| } | ||
| } catch (branchError) { | ||
| // Branch does not exist locally | ||
| } | ||
| patchGenerated = tryBranchStrategy(branchName, defaultBranch, cwd); | ||
| } else { | ||
| core.info(`[generate_git_patch] Strategy 1: skipped (no branchName)`); | ||
| } | ||
|
|
||
| // Strategy 2: Check if commits were made to current HEAD since checkout | ||
| if (!patchGenerated) { | ||
| const currentHead = execGitSync(["rev-parse", "HEAD"], { cwd }).trim(); | ||
|
|
||
| if (!githubSha) { | ||
| errorMessage = "GITHUB_SHA environment variable is not set"; | ||
| } else if (currentHead === githubSha) { | ||
| // No commits have been made since checkout | ||
| } else { | ||
| // Check if GITHUB_SHA is an ancestor of current HEAD | ||
| try { | ||
| execGitSync(["merge-base", "--is-ancestor", githubSha, "HEAD"], { cwd }); | ||
|
|
||
| // Count commits between GITHUB_SHA and HEAD | ||
| const commitCount = parseInt(execGitSync(["rev-list", "--count", `${githubSha}..HEAD`], { cwd }).trim(), 10); | ||
|
|
||
| if (commitCount > 0) { | ||
| // Generate patch from GITHUB_SHA to HEAD | ||
| const patchContent = execGitSync(["format-patch", `${githubSha}..HEAD`, "--stdout"], { cwd }); | ||
|
|
||
| if (patchContent && patchContent.trim()) { | ||
| fs.writeFileSync(patchPath, patchContent, "utf8"); | ||
| patchGenerated = true; | ||
| } | ||
| } | ||
| } catch { | ||
| // GITHUB_SHA is not an ancestor of HEAD - repository state has diverged | ||
| } | ||
| } | ||
| const result = tryHeadStrategy(githubSha, cwd); | ||
| patchGenerated = result.generated; | ||
| errorMessage = result.errorMessage; | ||
| } | ||
| } catch (error) { | ||
| errorMessage = `Failed to generate patch: ${getErrorMessage(error)}`; | ||
| core.warning(`[generate_git_patch] ${errorMessage}`); | ||
| } | ||
|
|
||
| // Check if patch was generated and has content | ||
| if (patchGenerated && fs.existsSync(patchPath)) { | ||
| const patchContent = fs.readFileSync(patchPath, "utf8"); | ||
| if (patchGenerated && fs.existsSync(PATCH_PATH)) { | ||
| const patchContent = fs.readFileSync(PATCH_PATH, "utf8"); | ||
| const patchSize = Buffer.byteLength(patchContent, "utf8"); | ||
| const patchLines = patchContent.split("\n").length; | ||
|
|
||
| if (!patchContent.trim()) { | ||
| // Empty patch | ||
| return { | ||
| success: false, | ||
| error: "No changes to commit - patch is empty", | ||
| patchPath: patchPath, | ||
| patchPath: PATCH_PATH, | ||
| patchSize: 0, | ||
| patchLines: 0, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| success: true, | ||
| patchPath: patchPath, | ||
| patchPath: PATCH_PATH, | ||
| patchSize: patchSize, | ||
| patchLines: patchLines, | ||
| }; | ||
| } | ||
|
|
||
| // No patch generated | ||
| return { | ||
| success: false, | ||
| error: errorMessage || "No changes to commit - no commits found", | ||
| patchPath: patchPath, | ||
| patchPath: PATCH_PATH, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.