-
Notifications
You must be signed in to change notification settings - Fork 251
Fix push_to_pull_request_branch generating bad patch on issue_comment follow-up runs #16927
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
Closed
Closed
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d2e0def
Initial plan
Copilot db0a145
Initial plan for fixing push_to_pull_request_branch bad patch bug
Copilot f888f2c
Fix push_to_pull_request_branch bad patch for issue_comment follow-up…
Copilot bdce136
Merge origin/main and recompile lock files
Copilot b1a81f2
Add extensive logging to generate_git_patch.cjs
Copilot 069541d
Merge remote-tracking branch 'origin/main' into copilot/fix-patch-gen…
Copilot 5d412ab
Simplify logging to core.info/core.warning, merge main, recompile
Copilot 174e6f7
Remove generate_git_patch.sh (unused in lock.yml files)
Copilot 9df3c33
Fix ReferenceError: core is not defined in generate_git_patch.cjs
Copilot 15a8193
Merge branch 'main' into copilot/fix-patch-generation-bug
pelikhan 774c8fb
Remove typeof core guards; set up global.core mock in tests
Copilot 6e90b4d
Merge origin/main, recompile (153 workflows)
Copilot 3d4fe02
Merge remote-tracking branch 'origin/main' into copilot/fix-patch-gen…
Copilot b001c9f
Refactor generate_git_patch.cjs into smaller functions; merge main, r…
Copilot 2f84fbd
Merge remote-tracking branch 'origin/main' into copilot/fix-patch-gen…
pelikhan 17e6c13
Merge branch 'main' into copilot/fix-patch-generation-bug
pelikhan 35d50c4
Merge remote-tracking branch 'origin/main' into copilot/fix-patch-gen…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,19 +8,140 @@ 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"; | ||
|
|
||
| /** | ||
| * 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}`; | ||
| core.info(`[generate_git_patch] using remote tracking ref as baseRef="${baseRef}"`); | ||
| return baseRef; | ||
| } catch { | ||
| // 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) { | ||
| // Branch doesn't exist on origin yet (new branch) – fall back to merge-base | ||
| core.warning(`[generate_git_patch] fetch of origin/${branchName} failed (${getErrorMessage(fetchErr)}); 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; | ||
| } | ||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
|
|
||
| /** | ||
| * Generates a git patch file for the current changes | ||
| * 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); | ||
|
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. Nice refactor! The |
||
| 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}"`); | ||
github-actions[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const patchDir = path.dirname(PATCH_PATH); | ||
| if (!fs.existsSync(patchDir)) { | ||
| fs.mkdirSync(patchDir, { recursive: true }); | ||
| } | ||
|
|
@@ -29,105 +150,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, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding
PATCH_PATHas a module-level constant is a good improvement over having it scattered as a string literal. Consider exporting it or making it configurable for testing purposes.