Skip to content
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

feat: compare all branch authors when deciding if a branch is modified #20739

Merged
merged 13 commits into from
Apr 13, 2023
Merged
2 changes: 1 addition & 1 deletion lib/modules/platform/default-scm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('modules/platform/default-scm', () => {

it('delegate isBranchModified to util/git', async () => {
git.isBranchModified.mockResolvedValueOnce(true);
await defaultGitScm.isBranchModified('branchName');
await defaultGitScm.isBranchModified('main', 'branchName');
expect(git.isBranchModified).toHaveBeenCalledTimes(1);
});
});
4 changes: 2 additions & 2 deletions lib/modules/platform/default-scm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class DefaultGitScm implements PlatformScm {
return git.isBranchConflicted(baseBranch, branch);
}

isBranchModified(branchName: string): Promise<boolean> {
return git.isBranchModified(branchName);
isBranchModified(baseBranch: string, branchName: string): Promise<boolean> {
return git.isBranchModified(baseBranch, branchName);
}
}
2 changes: 1 addition & 1 deletion lib/modules/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export interface Platform {

export interface PlatformScm {
isBranchBehindBase(branchName: string, baseBranch: string): Promise<boolean>;
isBranchModified(branchName: string): Promise<boolean>;
isBranchModified(baseBranch: string, branchName: string): Promise<boolean>;
isBranchConflicted(baseBranch: string, branch: string): Promise<boolean>;
branchExists(branchName: string): Promise<boolean>;
getBranchCommit(branchName: string): Promise<CommitSha | null>;
Expand Down
56 changes: 50 additions & 6 deletions lib/util/git/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ describe('util/git/index', () => {
await repo.addConfig('user.email', '[email protected]');
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', '[email protected]');
await repo.commit('commit1 message');
await fs.writeFile(base.path + '/commit2', 'commit2');
await repo.add(['commit2']);
await repo.addConfig('user.email', '[email protected]');
await repo.commit('commit2 message');
await fs.writeFile(base.path + '/commit3', 'commit3');
await repo.add(['commit3']);
await repo.addConfig('user.email', '[email protected]');
await repo.commit('commit3 message');

await repo.checkoutBranch('renovate/nested_files', defaultBranch);
await fs.mkdirp(base.path + '/bin/');
await fs.writeFile(base.path + '/bin/nested', 'nested');
Expand Down Expand Up @@ -274,28 +288,58 @@ describe('util/git/index', () => {
});

it('should return false when branch is not found', async () => {
expect(await git.isBranchModified('renovate/not_found')).toBeFalse();
expect(
await git.isBranchModified(defaultBranch, 'renovate/not_found')
).toBeFalse();
});

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(defaultBranch, 'renovate/future_branch')
).toBeFalse();
expect(
await git.isBranchModified(defaultBranch, 'renovate/future_branch')
).toBeFalse();
});

it('should return false when author is ignored', async () => {
git.setUserRepoConfig({
gitIgnoredAuthors: ['[email protected]'],
});
expect(await git.isBranchModified('renovate/custom_author')).toBeFalse();
expect(
await git.isBranchModified(defaultBranch, 'renovate/custom_author')
).toBeFalse();
});

it('should return false if every author is ignored with multiple commits', async () => {
git.setUserRepoConfig({
gitIgnoredAuthors: ['[email protected]', '[email protected]'],
});
expect(
await git.isBranchModified(defaultBranch, 'renovate/multiple_commits')
).toBeFalse();
});

it('should return true when custom author is unknown', async () => {
expect(await git.isBranchModified('renovate/custom_author')).toBeTrue();
expect(
await git.isBranchModified(defaultBranch, 'renovate/custom_author')
).toBeTrue();
});

it('should return true if any commit is modified', async () => {
git.setUserRepoConfig({
gitIgnoredAuthors: ['[email protected]'],
});
expect(
await git.isBranchModified(defaultBranch, 'renovate/multiple_commits')
).toBeTrue();
});

it('should return value stored in modifiedCacheResult', async () => {
modifiedCache.getCachedModifiedResult.mockReturnValue(true);
expect(await git.isBranchModified('renovate/future_branch')).toBeTrue();
expect(
await git.isBranchModified(defaultBranch, 'renovate/future_branch')
).toBeTrue();
});
});

Expand Down
59 changes: 30 additions & 29 deletions lib/util/git/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,10 @@ export async function isBranchBehindBase(
}
}

export async function isBranchModified(branchName: string): Promise<boolean> {
export async function isBranchModified(
baseBranch: string,
branchName: string
): Promise<boolean> {
if (!branchExists(branchName)) {
logger.debug('branch.isModified(): no cache');
return false;
Expand All @@ -626,18 +629,23 @@ export async function isBranchModified(branchName: string): Promise<boolean> {
logger.debug('branch.isModified(): using git to calculate');

await syncGit();
// Retrieve the author of the most recent commit
let lastAuthor: string | undefined;
// Retrieve the commit authors
let branchAuthors: string[] = [];
try {
lastAuthor = (
await git.raw([
'log',
'-1',
'--pretty=format:%ae',
`origin/${branchName}`,
'--',
])
).trim();
branchAuthors = [
...new Set(
(
await git.raw([
'log',
'--pretty=format:%ae',
`origin/${branchName}...origin/${baseBranch}`,
rarkins marked this conversation as resolved.
Show resolved Hide resolved
'--',
])
)
.trim()
.split('\n')
),
];
} catch (err) /* istanbul ignore next */ {
if (err.message?.includes('fatal: bad revision')) {
logger.debug(
Expand All @@ -646,26 +654,19 @@ export async function isBranchModified(branchName: string): Promise<boolean> {
);
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;
if (
lastAuthor === gitAuthorEmail ||
config.ignoredAuthors.some((ignoredAuthor) => lastAuthor === ignoredAuthor)
) {
// author matches - branch has not been modified
logger.debug('branch.isModified() = false');
config.branchIsModified[branchName] = false;
setCachedModifiedResult(branchName, false);
return false;
let branchIsModified = false;
for (const author of branchAuthors) {
if (author !== gitAuthorEmail && !config.ignoredAuthors.includes(author)) {
branchIsModified = true;
}
}
logger.debug(
{ branchName, lastAuthor, gitAuthorEmail },
'branch.isModified() = true'
);
config.branchIsModified[branchName] = true;
setCachedModifiedResult(branchName, true);
return true;
logger.debug(`branch.isModified() = ${branchIsModified}`);
config.branchIsModified[branchName] = branchIsModified;
setCachedModifiedResult(branchName, branchIsModified);
return branchIsModified;
}

export async function isBranchConflicted(
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/repository/config-migration/branch/rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function rebaseMigrationBranch(
): Promise<string | null> {
logger.debug('Checking if migration branch needs rebasing');
const branchName = getMigrationBranchName(config);
if (await scm.isBranchModified(branchName)) {
if (await scm.isBranchModified(config.baseBranch!, branchName)) {
logger.debug('Migration branch has been edited and cannot be rebased');
return null;
}
Expand Down
7 changes: 5 additions & 2 deletions lib/workers/repository/finalise/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { scm } from '../../../modules/platform/scm';
import { getBranchList } from '../../../util/git';

async function cleanUpBranches(
{ pruneStaleBranches: enabled }: RenovateConfig,
{ baseBranch, pruneStaleBranches: enabled }: RenovateConfig,
rarkins marked this conversation as resolved.
Show resolved Hide resolved
remainingBranches: string[]
): Promise<void> {
if (enabled === false) {
Expand All @@ -21,7 +21,10 @@ async function cleanUpBranches(
branchName,
state: 'open',
});
const branchIsModified = await scm.isBranchModified(branchName);
const branchIsModified = await scm.isBranchModified(
baseBranch!,
branchName
);
if (pr) {
if (branchIsModified) {
logger.debug(
Expand Down
4 changes: 3 additions & 1 deletion lib/workers/repository/onboarding/branch/rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export async function rebaseOnboardingBranch(
}

// TODO #7154
if (await scm.isBranchModified(config.onboardingBranch!)) {
if (
await scm.isBranchModified(config.baseBranch!, config.onboardingBranch!)
) {
logger.debug('Onboarding branch has been edited and cannot be rebased');
return null;
}
Expand Down
4 changes: 3 additions & 1 deletion lib/workers/repository/onboarding/pr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ If you need any further assistance then you can also [request help here](${
if (GlobalConfig.get('dryRun')) {
// TODO: types (#7154)
logger.info(`DRY-RUN: Would check branch ${config.onboardingBranch!}`);
} else if (await scm.isBranchModified(config.onboardingBranch!)) {
} else if (
await scm.isBranchModified(config.baseBranch!, config.onboardingBranch!)
) {
configDesc = emojify(
`### Configuration\n\n:abcd: Renovate has detected a custom config for this PR. Feel free to ask for [help](${
config.productLinks!.help
Expand Down
5 changes: 4 additions & 1 deletion lib/workers/repository/update/branch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export async function processBranch(
}

logger.debug('Checking if PR has been edited');
const branchIsModified = await scm.isBranchModified(config.branchName);
const branchIsModified = await scm.isBranchModified(
config.baseBranch,
config.branchName
);
if (branchPr) {
logger.debug('Found existing branch PR');
if (branchPr.state !== 'open') {
Expand Down
4 changes: 2 additions & 2 deletions lib/workers/repository/update/branch/reuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function shouldReuseExistingBranch(
if (await scm.isBranchBehindBase(branchName, baseBranch)) {
logger.debug(`Branch is behind base branch and needs rebasing`);
// We can rebase the branch only if no PR or PR can be rebased
if (await scm.isBranchModified(branchName)) {
if (await scm.isBranchModified(baseBranch, branchName)) {
logger.debug('Cannot rebase branch as it has been modified');
result.reuseExistingBranch = true;
result.isModified = true;
Expand All @@ -80,7 +80,7 @@ export async function shouldReuseExistingBranch(
if (result.isConflicted) {
logger.debug('Branch is conflicted');

if ((await scm.isBranchModified(branchName)) === false) {
if ((await scm.isBranchModified(baseBranch, branchName)) === false) {
logger.debug(`Branch is not mergeable and needs rebasing`);
if (config.rebaseWhen === 'never') {
logger.debug('Rebasing disabled by config');
Expand Down
2 changes: 1 addition & 1 deletion lib/workers/repository/update/pr/automerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function checkAutoMerge(
};
}
// Check if it's been touched
if (await scm.isBranchModified(branchName)) {
if (await scm.isBranchModified(config.baseBranch, branchName)) {
logger.debug('PR is ready for automerge but has been modified');
return {
automerged: false,
Expand Down