diff --git a/action.yml b/action.yml index 9d28570cb5..d2a7abb13c 100644 --- a/action.yml +++ b/action.yml @@ -82,6 +82,11 @@ inputs: maintainer-can-modify: description: 'Indicates whether maintainers can modify the pull request.' default: true + skip-if-commits-from-other-authors: + description: > + Skip updating the pull request branch if it contains commits from authors other than the configured action author/committer. + This prevents overwriting changes made by other users to the branch. + default: false outputs: pull-request-number: description: 'The pull request number' diff --git a/dist/index.js b/dist/index.js index 75ae3c2f07..1b2fe03722 100644 --- a/dist/index.js +++ b/dist/index.js @@ -168,8 +168,8 @@ function splitLines(multilineString) { .map(s => s.trim()) .filter(x => x !== ''); } -function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName, signoff, addPaths) { - return __awaiter(this, void 0, void 0, function* () { +function createOrUpdateBranch(git_1, commitMessage_1, base_1, branch_1, branchRemoteName_1, signoff_1, addPaths_1) { + return __awaiter(this, arguments, void 0, function* (git, commitMessage, base, branch, branchRemoteName, signoff, addPaths, skipIfCommitsFromOtherAuthors = false, authorEmail = '', committerEmail = '') { // Get the working base. // When a ref, it may or may not be the actual base. // When a commit, we must rebase onto the actual base. @@ -268,6 +268,52 @@ function createOrUpdateBranch(git, commitMessage, base, branch, branchRemoteName core.info(`Pull request branch '${branch}' already exists as remote branch '${branchRemoteName}/${branch}'`); // Checkout the pull request branch yield git.checkout(branch); + // Check if the branch has commits from other authors + if (skipIfCommitsFromOtherAuthors && authorEmail && committerEmail) { + core.info('Checking if branch has commits from other authors...'); + const branchCommitsAheadCount = yield commitsAhead(git, base, branch); + if (branchCommitsAheadCount > 0) { + try { + const commitAuthors = yield git.getCommitAuthors(`${base}..${branch}`); + const hasOtherAuthors = commitAuthors.some(commit => commit.authorEmail !== authorEmail && + commit.committerEmail !== committerEmail); + if (hasOtherAuthors) { + core.info(`Branch '${branch}' has commits from other authors. Skipping update to prevent overwriting their changes.`); + core.info(`Configured author: ${authorEmail}, committer: ${committerEmail}`); + const otherAuthors = commitAuthors.filter(commit => commit.authorEmail !== authorEmail || + commit.committerEmail !== committerEmail); + core.info(`Found commits from: ${otherAuthors.map(c => `${c.authorEmail} (committer: ${c.committerEmail})`).join(', ')}`); + action = 'not-updated'; + hasDiffWithBase = yield isAhead(git, base, branch); + const baseSha = yield git.revParse(base); + const baseCommit = yield git.getCommit(baseSha); + const headSha = yield git.revParse(branch); + let branchCommits = []; + if (hasDiffWithBase) { + branchCommits = yield buildBranchCommits(git, base, branch); + } + yield git.exec(['branch', '--delete', '--force', tempBranch]); + yield git.checkout(workingBase); + if (stashed) { + yield git.stashPop(); + } + return { + action: action, + base: base, + hasDiffWithBase: hasDiffWithBase, + baseCommit: baseCommit, + headSha: headSha, + branchCommits: branchCommits + }; + } + core.info('No commits from other authors found. Proceeding with update.'); + } + catch (error) { + core.warning(`Failed to check commit authors: ${utils.getErrorMessage(error)}`); + core.info('Proceeding with update despite check failure.'); + } + } + } // Reset the branch if one of the following conditions is true. // - If the branch differs from the recreated temp branch. // - If the number of commits ahead of the base branch differs between the branch and @@ -505,7 +551,7 @@ function createPullRequest(inputs) { outputs.set('pull-request-operation', 'none'); // Create or update the pull request branch core.startGroup('Create or update the pull request branch'); - const result = yield (0, create_or_update_branch_1.createOrUpdateBranch)(git, inputs.commitMessage, inputs.base, inputs.branch, branchRemoteName, inputs.signoff, inputs.addPaths); + const result = yield (0, create_or_update_branch_1.createOrUpdateBranch)(git, inputs.commitMessage, inputs.base, inputs.branch, branchRemoteName, inputs.signoff, inputs.addPaths, inputs.skipIfCommitsFromOtherAuthors, parsedAuthor.email, parsedCommitter.email); outputs.set('pull-request-head-sha', result.headSha); // Set the base. It would have been '' if not specified as an input inputs.base = result.base; @@ -815,6 +861,22 @@ class GitCommandManager { }; }); } + getCommitAuthors(commitRange) { + return __awaiter(this, void 0, void 0, function* () { + const output = yield this.exec(['log', '--format=%ae%n%ce%n###COMMIT###', commitRange], { suppressGitCmdOutput: true }); + const commits = []; + const lines = output.stdout.split('\n').filter(x => x !== ''); + for (let i = 0; i < lines.length; i += 3) { + if (lines[i + 2] === '###COMMIT###') { + commits.push({ + authorEmail: lines[i], + committerEmail: lines[i + 1] + }); + } + } + return commits; + }); + } getConfigValue(configKey_1) { return __awaiter(this, arguments, void 0, function* (configKey, configValue = '.') { const output = yield this.exec([ @@ -1654,7 +1716,8 @@ function run() { teamReviewers: utils.getInputAsArray('team-reviewers'), milestone: Number(core.getInput('milestone')), draft: getDraftInput(), - maintainerCanModify: core.getBooleanInput('maintainer-can-modify') + maintainerCanModify: core.getBooleanInput('maintainer-can-modify'), + skipIfCommitsFromOtherAuthors: core.getBooleanInput('skip-if-commits-from-other-authors') }; core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`); if (!inputs.token) { diff --git a/src/create-or-update-branch.ts b/src/create-or-update-branch.ts index 746a3a0381..7efd3b7dbc 100644 --- a/src/create-or-update-branch.ts +++ b/src/create-or-update-branch.ts @@ -173,7 +173,10 @@ export async function createOrUpdateBranch( branch: string, branchRemoteName: string, signoff: boolean, - addPaths: string[] + addPaths: string[], + skipIfCommitsFromOtherAuthors = false, + authorEmail = '', + committerEmail = '' ): Promise { // Get the working base. // When a ref, it may or may not be the actual base. @@ -294,6 +297,68 @@ export async function createOrUpdateBranch( // Checkout the pull request branch await git.checkout(branch) + // Check if the branch has commits from other authors + if (skipIfCommitsFromOtherAuthors && authorEmail && committerEmail) { + core.info('Checking if branch has commits from other authors...') + const branchCommitsAheadCount = await commitsAhead(git, base, branch) + if (branchCommitsAheadCount > 0) { + try { + const commitAuthors = await git.getCommitAuthors(`${base}..${branch}`) + const hasOtherAuthors = commitAuthors.some( + commit => + commit.authorEmail !== authorEmail && + commit.committerEmail !== committerEmail + ) + if (hasOtherAuthors) { + core.info( + `Branch '${branch}' has commits from other authors. Skipping update to prevent overwriting their changes.` + ) + core.info( + `Configured author: ${authorEmail}, committer: ${committerEmail}` + ) + const otherAuthors = commitAuthors.filter( + commit => + commit.authorEmail !== authorEmail || + commit.committerEmail !== committerEmail + ) + core.info( + `Found commits from: ${otherAuthors.map(c => `${c.authorEmail} (committer: ${c.committerEmail})`).join(', ')}` + ) + action = 'not-updated' + hasDiffWithBase = await isAhead(git, base, branch) + const baseSha = await git.revParse(base) + const baseCommit = await git.getCommit(baseSha) + const headSha = await git.revParse(branch) + let branchCommits: Commit[] = [] + if (hasDiffWithBase) { + branchCommits = await buildBranchCommits(git, base, branch) + } + await git.exec(['branch', '--delete', '--force', tempBranch]) + await git.checkout(workingBase) + if (stashed) { + await git.stashPop() + } + return { + action: action, + base: base, + hasDiffWithBase: hasDiffWithBase, + baseCommit: baseCommit, + headSha: headSha, + branchCommits: branchCommits + } + } + core.info( + 'No commits from other authors found. Proceeding with update.' + ) + } catch (error) { + core.warning( + `Failed to check commit authors: ${utils.getErrorMessage(error)}` + ) + core.info('Proceeding with update despite check failure.') + } + } + } + // Reset the branch if one of the following conditions is true. // - If the branch differs from the recreated temp branch. // - If the number of commits ahead of the base branch differs between the branch and diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 0ab4f8cc17..b0f524088a 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -37,6 +37,7 @@ export interface Inputs { always: boolean } maintainerCanModify: boolean + skipIfCommitsFromOtherAuthors: boolean } export async function createPullRequest(inputs: Inputs): Promise { @@ -194,7 +195,10 @@ export async function createPullRequest(inputs: Inputs): Promise { inputs.branch, branchRemoteName, inputs.signoff, - inputs.addPaths + inputs.addPaths, + inputs.skipIfCommitsFromOtherAuthors, + parsedAuthor.email, + parsedCommitter.email ) outputs.set('pull-request-head-sha', result.headSha) // Set the base. It would have been '' if not specified as an input diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 6270f19392..ebd0839b10 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -208,6 +208,26 @@ export class GitCommandManager { } } + async getCommitAuthors( + commitRange: string + ): Promise<{authorEmail: string; committerEmail: string}[]> { + const output = await this.exec( + ['log', '--format=%ae%n%ce%n###COMMIT###', commitRange], + {suppressGitCmdOutput: true} + ) + const commits: {authorEmail: string; committerEmail: string}[] = [] + const lines = output.stdout.split('\n').filter(x => x !== '') + for (let i = 0; i < lines.length; i += 3) { + if (lines[i + 2] === '###COMMIT###') { + commits.push({ + authorEmail: lines[i], + committerEmail: lines[i + 1] + }) + } + } + return commits + } + async getConfigValue(configKey: string, configValue = '.'): Promise { const output = await this.exec([ 'config', diff --git a/src/main.ts b/src/main.ts index dad26794d0..7c0af8d0c6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,7 +37,10 @@ async function run(): Promise { teamReviewers: utils.getInputAsArray('team-reviewers'), milestone: Number(core.getInput('milestone')), draft: getDraftInput(), - maintainerCanModify: core.getBooleanInput('maintainer-can-modify') + maintainerCanModify: core.getBooleanInput('maintainer-can-modify'), + skipIfCommitsFromOtherAuthors: core.getBooleanInput( + 'skip-if-commits-from-other-authors' + ) } core.debug(`Inputs: ${inspect(inputs)}`)