feat: compare all branch authors when deciding if a branch is modified (#21558)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
This commit is contained in:
Alex Kessock 2023-10-17 02:45:53 -06:00 committed by GitHub
parent e13f5a09f5
commit c2cb93ce44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 41 deletions

View file

@ -45,7 +45,7 @@ describe('modules/platform/default-scm', () => {
it('delegate isBranchModified to util/git', async () => { it('delegate isBranchModified to util/git', async () => {
git.isBranchModified.mockResolvedValueOnce(true); git.isBranchModified.mockResolvedValueOnce(true);
await defaultGitScm.isBranchModified('branchName'); await defaultGitScm.isBranchModified('branchName', 'main');
expect(git.isBranchModified).toHaveBeenCalledTimes(1); expect(git.isBranchModified).toHaveBeenCalledTimes(1);
}); });

View file

@ -27,8 +27,8 @@ export class DefaultGitScm implements PlatformScm {
return git.isBranchConflicted(baseBranch, branch); return git.isBranchConflicted(baseBranch, branch);
} }
isBranchModified(branchName: string): Promise<boolean> { isBranchModified(branchName: string, baseBranch?: string): Promise<boolean> {
return git.isBranchModified(branchName); return git.isBranchModified(branchName, baseBranch);
} }
getFileList(): Promise<string[]> { getFileList(): Promise<string[]> {

View file

@ -233,7 +233,7 @@ export interface Platform {
export interface PlatformScm { export interface PlatformScm {
isBranchBehindBase(branchName: string, baseBranch: string): Promise<boolean>; isBranchBehindBase(branchName: string, baseBranch: string): Promise<boolean>;
isBranchModified(branchName: string): Promise<boolean>; isBranchModified(branchName: string, baseBranch?: string): Promise<boolean>;
isBranchConflicted(baseBranch: string, branch: string): Promise<boolean>; isBranchConflicted(baseBranch: string, branch: string): Promise<boolean>;
branchExists(branchName: string): Promise<boolean>; branchExists(branchName: string): Promise<boolean>;
getBranchCommit(branchName: string): Promise<CommitSha | null>; getBranchCommit(branchName: string): Promise<CommitSha | null>;

View file

@ -77,6 +77,20 @@ describe('util/git/index', () => {
await repo.addConfig('user.email', 'custom@example.com'); await repo.addConfig('user.email', 'custom@example.com');
await repo.commit('custom message'); await repo.commit('custom message');
await repo.checkoutBranch('renovate/multiple_commits', defaultBranch);
await fs.writeFile(base.path + '/commit1', 'commit1');
await repo.add(['commit1']);
await repo.addConfig('user.email', 'author1@example.com');
await repo.commit('commit1 message');
await fs.writeFile(base.path + '/commit2', 'commit2');
await repo.add(['commit2']);
await repo.addConfig('user.email', 'author2@example.com');
await repo.commit('commit2 message');
await fs.writeFile(base.path + '/commit3', 'commit3');
await repo.add(['commit3']);
await repo.addConfig('user.email', 'author1@example.com');
await repo.commit('commit3 message');
await repo.checkoutBranch('renovate/nested_files', defaultBranch); await repo.checkoutBranch('renovate/nested_files', defaultBranch);
await fs.mkdirp(base.path + '/bin/'); await fs.mkdirp(base.path + '/bin/');
await fs.writeFile(base.path + '/bin/nested', 'nested'); await fs.writeFile(base.path + '/bin/nested', 'nested');
@ -275,11 +289,16 @@ describe('util/git/index', () => {
it('should return false when branch is not found', async () => { it('should return false when branch is not found', async () => {
expect(await git.isBranchModified('renovate/not_found')).toBeFalse(); expect(await git.isBranchModified('renovate/not_found')).toBeFalse();
expect(
await git.isBranchModified('renovate/not_found', defaultBranch)
).toBeFalse();
}); });
it('should return false when author matches', async () => { it('should return false when author matches', async () => {
expect(await git.isBranchModified('renovate/future_branch')).toBeFalse(); expect(await git.isBranchModified('renovate/future_branch')).toBeFalse();
expect(await git.isBranchModified('renovate/future_branch')).toBeFalse(); expect(
await git.isBranchModified('renovate/future_branch', defaultBranch)
).toBeFalse();
}); });
it('should return false when author is ignored', async () => { it('should return false when author is ignored', async () => {
@ -287,15 +306,33 @@ describe('util/git/index', () => {
gitIgnoredAuthors: ['custom@example.com'], gitIgnoredAuthors: ['custom@example.com'],
}); });
expect(await git.isBranchModified('renovate/custom_author')).toBeFalse(); expect(await git.isBranchModified('renovate/custom_author')).toBeFalse();
expect(
await git.isBranchModified('renovate/custom_author', defaultBranch)
).toBeFalse();
}); });
it('should return true when custom author is unknown', async () => { it('should return true when custom author is unknown', async () => {
expect(await git.isBranchModified('renovate/custom_author')).toBeTrue(); expect(await git.isBranchModified('renovate/custom_author')).toBeTrue();
expect(
await git.isBranchModified('renovate/custom_author', defaultBranch)
).toBeTrue();
});
it('should return true if any commit is modified', async () => {
git.setUserRepoConfig({
gitIgnoredAuthors: ['author1@example.com'],
});
expect(
await git.isBranchModified('renovate/multiple_commits', defaultBranch)
).toBeTrue();
}); });
it('should return value stored in modifiedCacheResult', async () => { it('should return value stored in modifiedCacheResult', async () => {
modifiedCache.getCachedModifiedResult.mockReturnValue(true); modifiedCache.getCachedModifiedResult.mockReturnValue(true);
expect(await git.isBranchModified('renovate/future_branch')).toBeTrue(); expect(await git.isBranchModified('renovate/future_branch')).toBeTrue();
expect(
await git.isBranchModified('renovate/future_branch', defaultBranch)
).toBeTrue();
}); });
}); });

View file

@ -616,7 +616,10 @@ export async function isBranchBehindBase(
} }
} }
export async function isBranchModified(branchName: string): Promise<boolean> { export async function isBranchModified(
branchName: string,
baseBranch?: string
): Promise<boolean> {
if (!branchExists(branchName)) { if (!branchExists(branchName)) {
logger.debug('branch.isModified(): no cache'); logger.debug('branch.isModified(): no cache');
return false; return false;
@ -636,21 +639,49 @@ export async function isBranchModified(branchName: string): Promise<boolean> {
return isModified; return isModified;
} }
logger.debug('branch.isModified(): using git to calculate');
await syncGit(); await syncGit();
// Retrieve the author of the most recent commit // Retrieve the commit authors
let lastAuthor: string | undefined; let branchAuthors: string[] = [];
try { try {
lastAuthor = ( if (baseBranch) {
await git.raw([ logger.debug(
'log', `branch.isModified(): using git to calculate authors between ${branchName} and ${baseBranch}`
'-1', );
'--pretty=format:%ae', branchAuthors = [
`origin/${branchName}`, ...new Set(
'--', (
]) await git.raw([
).trim(); 'log',
'--pretty=format:%ae',
`origin/${branchName}..origin/${baseBranch}`,
'--',
])
)
.trim()
.split(newlineRegex)
),
];
logger.debug(`Git authors in branch: ${branchAuthors.join(', ')}`);
} else {
logger.debug(
`branch.isModified(): checking last author of ${branchName}`
);
branchAuthors = [
...new Set(
(
await git.raw([
'log',
'-1',
'--pretty=format:%ae',
`origin/${branchName}`,
'--',
])
)
.trim()
.split(newlineRegex)
),
];
}
} catch (err) /* istanbul ignore next */ { } catch (err) /* istanbul ignore next */ {
if (err.message?.includes('fatal: bad revision')) { if (err.message?.includes('fatal: bad revision')) {
logger.debug( logger.debug(
@ -659,26 +690,20 @@ export async function isBranchModified(branchName: string): Promise<boolean> {
); );
throw new Error(REPOSITORY_CHANGED); throw new Error(REPOSITORY_CHANGED);
} }
logger.warn({ err }, 'Error checking last author for isBranchModified'); logger.warn({ err }, 'Error retrieving git authors for isBranchModified');
} }
const { gitAuthorEmail } = config; const { gitAuthorEmail } = config;
if ( let branchIsModified = false;
lastAuthor === gitAuthorEmail || for (const author of branchAuthors) {
config.ignoredAuthors.some((ignoredAuthor) => lastAuthor === ignoredAuthor) if (author !== gitAuthorEmail && !config.ignoredAuthors.includes(author)) {
) { logger.debug(`branch.isModified=true due to this git author: ${author}`);
// author matches - branch has not been modified branchIsModified = true;
logger.debug('branch.isModified() = false'); }
config.branchIsModified[branchName] = false;
setCachedModifiedResult(branchName, false);
return false;
} }
logger.debug( logger.debug(`branch.isModified() = ${branchIsModified}`);
{ branchName, lastAuthor, gitAuthorEmail }, config.branchIsModified[branchName] = branchIsModified;
'branch.isModified() = true' setCachedModifiedResult(branchName, branchIsModified);
); return branchIsModified;
config.branchIsModified[branchName] = true;
setCachedModifiedResult(branchName, true);
return true;
} }
export async function isBranchConflicted( export async function isBranchConflicted(

View file

@ -16,7 +16,7 @@ export async function rebaseMigrationBranch(
): Promise<string | null> { ): Promise<string | null> {
logger.debug('Checking if migration branch needs rebasing'); logger.debug('Checking if migration branch needs rebasing');
const branchName = getMigrationBranchName(config); const branchName = getMigrationBranchName(config);
if (await scm.isBranchModified(branchName)) { if (await scm.isBranchModified(branchName, config.baseBranch)) {
logger.debug('Migration branch has been edited and cannot be rebased'); logger.debug('Migration branch has been edited and cannot be rebased');
return null; return null;
} }

View file

@ -242,7 +242,10 @@ export async function processBranch(
} }
logger.debug('Checking if PR has been edited'); logger.debug('Checking if PR has been edited');
const branchIsModified = await scm.isBranchModified(config.branchName); const branchIsModified = await scm.isBranchModified(
config.branchName,
config.baseBranch
);
if (branchPr) { if (branchPr) {
logger.debug('Found existing branch PR'); logger.debug('Found existing branch PR');
if (branchPr.state !== 'open') { if (branchPr.state !== 'open') {

View file

@ -29,7 +29,7 @@ export async function shouldReuseExistingBranch(
if (await scm.isBranchBehindBase(branchName, baseBranch)) { if (await scm.isBranchBehindBase(branchName, baseBranch)) {
logger.debug(`Branch is behind base branch and needs rebasing`); logger.debug(`Branch is behind base branch and needs rebasing`);
// We can rebase the branch only if no PR or PR can be rebased // We can rebase the branch only if no PR or PR can be rebased
if (await scm.isBranchModified(branchName)) { if (await scm.isBranchModified(branchName, baseBranch)) {
logger.debug('Cannot rebase branch as it has been modified'); logger.debug('Cannot rebase branch as it has been modified');
result.reuseExistingBranch = true; result.reuseExistingBranch = true;
result.isModified = true; result.isModified = true;
@ -50,7 +50,7 @@ export async function shouldReuseExistingBranch(
if (result.isConflicted) { if (result.isConflicted) {
logger.debug('Branch is conflicted'); logger.debug('Branch is conflicted');
if ((await scm.isBranchModified(branchName)) === false) { if ((await scm.isBranchModified(branchName, baseBranch)) === false) {
logger.debug(`Branch is not mergeable and needs rebasing`); logger.debug(`Branch is not mergeable and needs rebasing`);
if (config.rebaseWhen === 'never') { if (config.rebaseWhen === 'never') {
logger.debug('Rebasing disabled by config'); logger.debug('Rebasing disabled by config');

View file

@ -82,7 +82,7 @@ export async function checkAutoMerge(
}; };
} }
// Check if it's been touched // Check if it's been touched
if (await scm.isBranchModified(branchName)) { if (await scm.isBranchModified(branchName, config.baseBranch)) {
logger.debug('PR is ready for automerge but has been modified'); logger.debug('PR is ready for automerge but has been modified');
return { return {
automerged: false, automerged: false,