Skip to content

Commit

Permalink
Add support for merge commits
Browse files Browse the repository at this point in the history
Fix bug where backport would fail if no .backportrc.json file existed
  • Loading branch information
sorenlouv committed Feb 16, 2022
1 parent da4b365 commit a82aeac
Show file tree
Hide file tree
Showing 22 changed files with 413 additions and 143 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/backport
11 changes: 1 addition & 10 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,7 @@
{
"type": "node",
"request": "launch",
"name": "Run file",
"program": "${file}",
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
"console": "integratedTerminal"
},

{
"type": "node",
"request": "launch",
"name": "Start backport (CLI)",
"name": "Run backport (ts-node)",
"program": "${workspaceRoot}/src/entrypoint.cli.ts",
"runtimeArgs": ["-r", "ts-node/register/transpile-only"],
"args": [
Expand Down
22 changes: 6 additions & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,19 @@
### Run

```
yarn start --branch 6.1 --repo-owner backport-org --repo-name backport-demo --all
yarn start --branch 6.1 --repo backport-org/backport-demo --all
```

or
**Run `backport` CLI globally**
This will build backport continously and link it, so it can accessed with `backport` command globally

```
# Compile
yarn tsc
# Run
node dist/entrypoint.cli.js --branch 6.1 --repo-owner backport-org --repo-name backport-demo --all
yarn tsc && sudo chmod +x bin/backport && yarn link && yarn tsc --watch
```

**Run `backport` CLI globally**

**Remove linked backport**
```
yarn global remove backport
npm -g uninstall backport
yarn unlink backport
yarn unlink
yarn link
sudo chmod +x dist/entrypoint.cli.js
yarn tsc --watch
yarn global remove backport; npm -g uninstall backport; yarn unlink backport; yarn unlink;
```

You can now use `backport` command anywhere, and it'll point to the development version.
Expand Down
2 changes: 2 additions & 0 deletions bin/backport
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../dist/entrypoint.cli.js')
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"main": "./dist/entrypoint.module.js",
"types": "dist/entrypoint.module.d.ts",
"bin": {
"backport": "./dist/entrypoint.cli.js"
"backport": "./bin/backport"
},
"license": "MIT",
"scripts": {
Expand Down
96 changes: 89 additions & 7 deletions src/entrypoint.cli.e2e.private.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,77 @@ describe('inquirer cli', () => {
`);
});

describe('repo: repo-with-backportrc-removed (missing .backportrc.json config file)', () => {
it('should list commits', async () => {
const output = await runBackportAsync(
[
'--branch',
'foo',
'--repo',
'backport-org/repo-with-backportrc-removed',
'--accessToken',
devAccessToken,
],
{ waitForString: 'Select commit' }
);

expect(output).toMatchInlineSnapshot(`
"? Select commit (Use arrow keys)
❯ 1. Rename README.me to README.md
2. Merge pull request #1 from backport-org/add-readme
3. Create README.me
4. Delete .backportrc.json
5. Create .backportrc.json
6. Delete .backportrc.json
7. Create .backportrc.json"
`);
});

it('should attempt to backport by PR', async () => {
const output = await runBackportAsync(
[
'--branch',
'foo',
'--repo',
'backport-org/repo-with-backportrc-removed',
'--pr',
'1',
'--accessToken',
devAccessToken,
],
{ waitForString: "is invalid or doesn't exist" }
);

expect(output).toMatchInlineSnapshot(`
"
Backporting to foo:
The branch \\"foo\\" is invalid or doesn't exist"
`);
});

it('should attempt to backport by commit sha', async () => {
const output = await runBackportAsync(
[
'--branch',
'foo',
'--repo',
'backport-org/repo-with-backportrc-removed',
'--sha',
'be59df6912a550c8cb49ba3e18be3e512f3d608c',
'--accessToken',
devAccessToken,
],
{ waitForString: "is invalid or doesn't exist" }
);

expect(output).toMatchInlineSnapshot(`
"
Backporting to foo:
The branch \\"foo\\" is invalid or doesn't exist"
`);
});
});

describe('repo: different-merge-strategies', () => {
it('list all commits regardless how they were merged', async () => {
jest.setTimeout(TIMEOUT_IN_SECONDS * 1000 * 1.1);
Expand All @@ -278,19 +349,30 @@ describe('inquirer cli', () => {
'different-merge-strategies',
'--accessToken',
devAccessToken,
'-n',
'20',
],
{ waitForString: 'Select commit' }
);

expect(output).toMatchInlineSnapshot(`
"? Select commit (Use arrow keys)
❯ 1. Using squash to merge commits (#3) 7.x
2. Rebase strategy: Second commit 7.x
3. Rebase strategy: First commit
4. Merge pull request #1 from backport-org/merge-strategy
5. Merge strategy: Second commit
6. Merge strategy: First commit
7. Initial commit"
❯ 1. Merge pull request #9 from backport-org/many-merge-commits
2. Merge strategy: Eighth of many merges
3. Merge strategy: Seventh of many merges
4. Merge strategy: Sixth of many merges
5. Merge strategy: Fifth of many merges
6. Merge strategy: Fourth of many merges
7. Merge strategy: Third of many merges
8. Merge strategy: Second of many merges
9. Merge strategy: First of many merges
10.Using squash to merge commits (#3) 7.x
11.Rebase strategy: Second commit 7.x
12.Rebase strategy: First commit
13.Merge pull request #1 from backport-org/merge-strategy
14.Merge strategy: Second commit
15.Merge strategy: First commit
16.Initial commit"
`);
});
});
Expand Down
4 changes: 3 additions & 1 deletion src/runSequentially.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('runSequentially', () => {

rpcExecMock = jest
.spyOn(childProcess, 'exec')
.mockResolvedValue({ stdout: 'success', stderr: '' });
.mockResolvedValue({ stdout: '', stderr: '' });
rpcExecOriginalMock = jest.spyOn(childProcess, 'execAsCallback');

const scope = nock('https://api.github.com')
Expand Down Expand Up @@ -176,6 +176,7 @@ describe('runSequentially', () => {
'/myHomeDir/.backport/repositories/elastic/kibana',
'/myHomeDir/.backport/repositories/elastic/kibana',
'/myHomeDir/.backport/repositories/elastic/kibana',
'/myHomeDir/.backport/repositories/elastic/kibana',
]);
});

Expand All @@ -189,6 +190,7 @@ describe('runSequentially', () => {
'git remote add elastic https://x-access-token:myAccessToken@github.com/elastic/kibana.git',
'git reset --hard && git clean -d --force && git fetch elastic 7.x && git checkout -B backport/7.x/pr-55 elastic/7.x --no-track',
'git fetch elastic master:master --force',
'git rev-list -1 --merges abcd~1..abcd',
'git cherry-pick -x abcd',
'git push sqren_authenticated backport/7.x/pr-55:backport/7.x/pr-55 --force',
'git reset --hard && git checkout my-source-branch-from-options && git branch -D backport/7.x/pr-55',
Expand Down
1 change: 1 addition & 0 deletions src/runSequentially.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export async function runSequentially({
pullRequestNumber: number,
});
} catch (e) {
process.exitCode = 1;
results.push({
targetBranch,
status: 'failure',
Expand Down
154 changes: 123 additions & 31 deletions src/services/git.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,17 @@ import * as childProcess from './child-process-promisified';
import {
cherrypick,
cloneRepo,
getCommitsInMergeCommit,
getIsCommitInBranch,
getSourceRepoPath,
getIsMergeCommit,
} from './git';
import { getShortSha } from './github/commitFormatters';
import { RepoOwnerAndNameResponse } from './github/v4/getRepoOwnerAndNameFromGitRemotes';

jest.unmock('del');
jest.unmock('make-dir');

async function createAndCommitFile({
filename,
content,
execOpts,
}: {
filename: string;
content: string;
execOpts: { cwd: string };
}) {
await childProcess.exec(`echo "${content}" > "${filename}"`, execOpts);
await childProcess.exec(
`git add -A && git commit -m 'Update ${filename}'`,
execOpts
);

return getCurrentSha(execOpts);
}

async function getCurrentSha(execOpts: { cwd: string }) {
const { stdout } = await childProcess.exec('git rev-parse HEAD', execOpts);
return stdout.trim();
}

async function getCurrentMessage(execOpts: { cwd: string }) {
const { stdout } = await childProcess.exec(
'git --no-pager log -1 --pretty=%B',
execOpts
);
return stdout.trim();
}

describe('git.integration', () => {
describe('getIsCommitInBranch', () => {
let firstSha: string;
Expand Down Expand Up @@ -324,8 +295,129 @@ describe('git.integration', () => {
);
});
});

describe('when cloning "backport-org/different-merge-strategies"', () => {
const MERGE_COMMIT_HASH_1 = 'bdc5a17f81e5f32129e27b05c742e055c650bc54';
const MERGE_COMMIT_HASH_2 = '0db7f1ac1233461563d8708511d1c14adbab46da';
const SQUASH_COMMIT_HASH = '74a76fa64b34e3ffe8f2a3f73840e1b42fd07299';
const REBASE_COMMIT_HASH = '9059ae0ca31caa2eebc035f2542842d6c2fde83b';

let sandboxPath: string;
beforeAll(async () => {
sandboxPath = getSandboxPath({
filename: __filename,
specname: 'different-merge-strategies',
});
await resetSandbox(sandboxPath);

await childProcess.exec(
'git clone https://github.com/backport-org/different-merge-strategies.git ./',
{ cwd: sandboxPath }
);
});

describe('getIsMergeCommit', () => {
it('returns true for first merge commit', async () => {
const res = await getIsMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
MERGE_COMMIT_HASH_1
);

expect(res).toBe(true);
});

it('returns true for second merge commit', async () => {
const res = await getIsMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
MERGE_COMMIT_HASH_2
);

expect(res).toBe(true);
});

it('returns false for rebased commits', async () => {
const res = await getIsMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
REBASE_COMMIT_HASH
);

expect(res).toBe(false);
});

it('returns false for squashed commits', async () => {
const res = await getIsMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
SQUASH_COMMIT_HASH
);

expect(res).toBe(false);
});
});

describe('getCommitsInMergeCommit', () => {
it('returns a list of commit hashes - excluding the merge hash itself', async () => {
const res = await getCommitsInMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
MERGE_COMMIT_HASH_1
);

expect(res).not.toContain(MERGE_COMMIT_HASH_1);

expect(res).toEqual([
'f9a760e0d9eb3ebcc64f8cb75ce885714b836496',
'7b92e29e88266004485ce0fae0260605b01df887',
'a1facf8c006fb815d6a6ecd1b2907e6e64f29576',
'b8d4bcfb0fd875be4ab0230f6db40ddf72f45378',
'6f224054db5f7772b04f23f17659070216bae84c',
'7fed54cbd1ba9cb973462670ff82ac80bf8a79f8',
'78c24d0058859e7d511d10ce91ebf279c7b58ac2',
'709ccf707d443dd8c001b3c3ae40fdf037bb43f5',
]);
});

it('returns empty for squash commits', async () => {
const res = await getCommitsInMergeCommit(
{ dir: sandboxPath } as ValidConfigOptions,
SQUASH_COMMIT_HASH
);

expect(res).toEqual([]);
});
});
});
});

async function createAndCommitFile({
filename,
content,
execOpts,
}: {
filename: string;
content: string;
execOpts: { cwd: string };
}) {
await childProcess.exec(`echo "${content}" > "${filename}"`, execOpts);
await childProcess.exec(
`git add -A && git commit -m 'Update ${filename}'`,
execOpts
);

return getCurrentSha(execOpts);
}

async function getCurrentSha(execOpts: { cwd: string }) {
const { stdout } = await childProcess.exec('git rev-parse HEAD', execOpts);
return stdout.trim();
}

async function getCurrentMessage(execOpts: { cwd: string }) {
const { stdout } = await childProcess.exec(
'git --no-pager log -1 --pretty=%B',
execOpts
);
return stdout.trim();
}

function mockRepoOwnerAndName({
repoName,
parentRepoOwner,
Expand Down
Loading

0 comments on commit a82aeac

Please sign in to comment.