Skip to content

Commit

Permalink
feat(azure): implement mergePr (#7908)
Browse files Browse the repository at this point in the history
  • Loading branch information
elwynelwyn committed Dec 18, 2020
1 parent 34cd73b commit 32459d3
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 12 deletions.
95 changes: 95 additions & 0 deletions lib/platform/azure/azure-helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,100 @@ describe('platform/azure/helpers', () => {
GitPullRequestMergeStrategy.Squash
);
});
it('should return most specific exact branch policy', async () => {
const refMock = 'refs/heads/ding';
azureApi.policyApi.mockImplementationOnce(
() =>
({
getPolicyConfigurations: jest.fn(() => [
{
settings: {
allowSquash: true,
scope: [
{
repositoryId: 'doo-dee-doo-repository-id',
},
],
},
type: {
id: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
},
},
{
settings: {
allowSquash: true,
scope: [
{
repositoryId: '',
},
],
},
type: {
id: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
},
},
{
settings: {
allowRebase: true,
scope: [
{
matchKind: 'Exact',
refName: refMock,
repositoryId: '',
},
],
},
type: {
id: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
},
},
]),
} as any)
);
expect(await azureHelper.getMergeMethod('', '', refMock)).toEqual(
GitPullRequestMergeStrategy.Rebase
);
});
it('should return most specific prefix branch policy', async () => {
const refMock = 'refs/heads/ding-wow';
azureApi.policyApi.mockImplementationOnce(
() =>
({
getPolicyConfigurations: jest.fn(() => [
{
settings: {
allowSquash: true,
scope: [
{
repositoryId: '',
},
],
},
type: {
id: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
},
},
{
settings: {
allowRebase: true,
scope: [
{
matchKind: 'Prefix',
refName: 'refs/heads/ding',
repositoryId: '',
},
],
},
type: {
id: 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab',
},
},
]),
} as any)
);
expect(await azureHelper.getMergeMethod('', '', refMock)).toEqual(
GitPullRequestMergeStrategy.Rebase
);
});
});
});
31 changes: 28 additions & 3 deletions lib/platform/azure/azure-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,43 @@ export function getProjectAndRepo(

export async function getMergeMethod(
repoId: string,
project: string
project: string,
branchRef?: string
): Promise<GitPullRequestMergeStrategy> {
type Scope = {
repositoryId: string;
refName?: string;
matchKind: 'Prefix' | 'Exact';
};
const isRelevantScope = (scope: Scope): boolean => {
if (scope.repositoryId !== repoId) {
return false;
}
if (!branchRef) {
return true;
}
return scope.matchKind === 'Exact'
? scope.refName === branchRef
: branchRef.startsWith(scope.refName);
};

const policyConfigurations = (
await (await azureApi.policyApi()).getPolicyConfigurations(project)
)
.filter(
(p) =>
p.settings.scope.some((s) => s.repositoryId === repoId) &&
p.type.id === mergePolicyGuid
p.settings.scope.some(isRelevantScope) && p.type.id === mergePolicyGuid
)
.map((p) => p.settings)[0];

logger.trace(
`getMergeMethod(${repoId}, ${project}, ${branchRef}) determining mergeMethod from matched policy:\n${JSON.stringify(
policyConfigurations,
null,
4
)}`
);

try {
return Object.keys(policyConfigurations)
.map((p) => GitPullRequestMergeStrategy[p.slice(5)])
Expand Down
87 changes: 84 additions & 3 deletions lib/platform/azure/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import {
GitPullRequestMergeStrategy,
GitStatusState,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
Expand Down Expand Up @@ -1004,11 +1005,91 @@ describe('platform/azure', () => {
);
});
});
describe('Not supported by Azure DevOps (yet!)', () => {
it('mergePr', async () => {
const res = await azure.mergePr(0, undefined);

describe('mergePr', () => {
it('should complete the PR', async () => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
const branchNameMock = 'test';
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
const updatePullRequestMock = jest.fn();
azureApi.gitApi.mockImplementationOnce(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: lastMergeSourceCommitMock,
targetRefName: 'refs/heads/ding',
})),
updatePullRequest: updatePullRequestMock,
} as any)
);

azureHelper.getMergeMethod = jest
.fn()
.mockReturnValue(GitPullRequestMergeStrategy.Squash);

const res = await azure.mergePr(pullRequestIdMock, branchNameMock);

expect(updatePullRequestMock).toHaveBeenCalledWith(
{
status: PullRequestStatus.Completed,
lastMergeSourceCommit: lastMergeSourceCommitMock,
completionOptions: {
mergeStrategy: GitPullRequestMergeStrategy.Squash,
deleteSourceBranch: true,
},
},
'1',
pullRequestIdMock
);
expect(res).toBe(true);
});
it('should return false if the PR does not update successfully', async () => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
const branchNameMock = 'test';
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
azureApi.gitApi.mockImplementationOnce(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: lastMergeSourceCommitMock,
})),
updatePullRequest: jest
.fn()
.mockRejectedValue(new Error(`oh no pr couldn't be updated`)),
} as any)
);

azureHelper.getMergeMethod = jest
.fn()
.mockReturnValue(GitPullRequestMergeStrategy.Squash);

const res = await azure.mergePr(pullRequestIdMock, branchNameMock);
expect(res).toBe(false);
});

it('should cache the mergeMethod for subsequent merges', async () => {
await initRepo({ repository: 'some/repo' });
azureApi.gitApi.mockImplementation(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: { commitId: 'abcd1234' },
targetRefName: 'refs/heads/ding',
})),
updatePullRequest: jest.fn(),
} as any)
);
azureHelper.getMergeMethod = jest
.fn()
.mockReturnValue(GitPullRequestMergeStrategy.Squash);

await azure.mergePr(1234, 'test-branch-1');
await azure.mergePr(5678, 'test-branch-2');

expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);
});
});

describe('getVulnerabilityAlerts()', () => {
Expand Down
64 changes: 58 additions & 6 deletions lib/platform/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import {

interface Config {
repoForceRebase: boolean;
mergeMethod: GitPullRequestMergeStrategy;
defaultMergeMethod: GitPullRequestMergeStrategy;
mergeMethods: Record<string, GitPullRequestMergeStrategy>;
owner: string;
repoId: string;
project: string;
Expand Down Expand Up @@ -143,7 +144,11 @@ export async function initRepo({
const defaultBranch = repo.defaultBranch.replace('refs/heads/', '');
config.defaultBranch = defaultBranch;
logger.debug(`${repository} default branch = ${defaultBranch}`);
config.mergeMethod = await azureHelper.getMergeMethod(repo.id, names.project);
config.defaultMergeMethod = await azureHelper.getMergeMethod(
repo.id,
names.project
);
config.mergeMethods = {};
config.repoForceRebase = false;

const [projectName, repoName] = repository.split('/');
Expand Down Expand Up @@ -387,7 +392,7 @@ export async function createPr({
id: pr.createdBy.id,
},
completionOptions: {
mergeStrategy: config.mergeMethod,
mergeStrategy: config.defaultMergeMethod,
deleteSourceBranch: true,
},
},
Expand Down Expand Up @@ -575,9 +580,56 @@ export async function setBranchStatus({
logger.trace(`Created commit status of ${state} on branch ${branchName}`);
}

export function mergePr(pr: number, branchName: string): Promise<boolean> {
logger.debug(`mergePr(pr)(${pr}) - Not supported by Azure DevOps (yet!)`);
return Promise.resolve(false);
export async function mergePr(
pullRequestId: number,
branchName: string
): Promise<boolean> {
logger.debug(`mergePr(${pullRequestId}, ${branchName})`);
const azureApiGit = await azureApi.gitApi();

const pr = await azureApiGit.getPullRequestById(
pullRequestId,
config.project
);

const mergeMethod =
config.mergeMethods[pr.targetRefName] ??
(config.mergeMethods[pr.targetRefName] = await azureHelper.getMergeMethod(
config.repoId,
config.project,
pr.targetRefName
));

const objToUpdate: GitPullRequest = {
status: PullRequestStatus.Completed,
lastMergeSourceCommit: pr.lastMergeSourceCommit,
completionOptions: {
mergeStrategy: mergeMethod,
deleteSourceBranch: true,
},
};

logger.trace(
`Updating PR ${pullRequestId} to status ${PullRequestStatus.Completed} (${
PullRequestStatus[PullRequestStatus.Completed]
}) with lastMergeSourceCommit ${
pr.lastMergeSourceCommit.commitId
} using mergeStrategy ${mergeMethod} (${
GitPullRequestMergeStrategy[mergeMethod]
})`
);

try {
await azureApiGit.updatePullRequest(
objToUpdate,
config.repoId,
pullRequestId
);
return true;
} catch (err) {
logger.debug({ err }, 'Failed to set the PR as completed.');
return false;
}
}

export function getPrBody(input: string): string {
Expand Down

0 comments on commit 32459d3

Please sign in to comment.