Skip to content

Commit 8643dcb

Browse files
committed
git: implement git node backport
1 parent ecae0ca commit 8643dcb

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

components/git/backport.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
const yargs = require('yargs');
4+
const { parsePRFromURL } = require('../../lib/links');
5+
const CLI = require('../../lib/cli');
6+
const { runPromise } = require('../../lib/run');
7+
const BackportSession = require('../../lib/backport_session');
8+
9+
const epilogue = `====================== Example =======================
10+
Backporting https://github.com/nodejs/node/pull/12344 to v10.x
11+
12+
# Sync master with upstream for the commits, if they are not yet there
13+
$ git checkout master
14+
$ git node sync
15+
16+
# Backport existing commits from master to v10.x-staging
17+
$ git checkout v10.x-staging
18+
$ git node sync
19+
$ git node backport 12344 --to 10
20+
=====================================================
21+
`;
22+
23+
function builder(yargs) {
24+
return yargs
25+
.options({
26+
to: {
27+
describe: 'release to backport the commits to',
28+
type: 'number',
29+
required: true
30+
}
31+
})
32+
.positional('identifier', {
33+
type: 'string',
34+
describe: 'ID or URL of the pull request'
35+
})
36+
.epilogue(epilogue)
37+
.wrap(90);
38+
}
39+
40+
async function main(config) {
41+
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
42+
const cli = new CLI(logStream);
43+
const dir = process.cwd();
44+
const session = new BackportSession(cli, dir, config.prid, config.to);
45+
return session.backport();
46+
}
47+
48+
function handler(argv) {
49+
let parsed = {};
50+
const prid = Number.parseInt(argv.identifier);
51+
if (!Number.isNaN(prid)) {
52+
parsed.prid = prid;
53+
} else {
54+
parsed = parsePRFromURL(argv.identifier);
55+
if (!parsed) {
56+
return yargs.showHelp();
57+
}
58+
}
59+
60+
const config = require('../../lib/config').getMergedConfig();
61+
const merged = Object.assign({}, argv, parsed, config);
62+
return runPromise(main(merged));
63+
}
64+
65+
module.exports = {
66+
command: 'backport <identifier>',
67+
describe: 'Backport a PR',
68+
builder,
69+
handler
70+
};

lib/backport_session.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict';
2+
3+
const Session = require('./session');
4+
const { runSync, runAsync } = require('./run');
5+
const { getPrURL } = require('./links');
6+
7+
const { IGNORE } = require('./run');
8+
9+
class BackportSession extends Session {
10+
constructor(cli, dir, prid, target) {
11+
// eslint-disable-next-line no-useless-constructor
12+
super(cli, dir, prid);
13+
this.target = target
14+
}
15+
16+
async backport() {
17+
const commits = this.grep();
18+
const { cli } = this;
19+
20+
if (commits.length === 0) {
21+
cli.error('Could not find any commit matching the PR');
22+
throw new Error(IGNORE);
23+
}
24+
25+
cli.ok('Found the following commits:');
26+
for (const commit of commits) {
27+
cli.log(` - ${commit.sha} ${commit.title}`);
28+
}
29+
30+
const newBranch = `backport-${this.prid}-to-${this.target}`;
31+
const shouldCheckout = await cli.prompt(
32+
`Do you want to checkout to a new branch \`${newBranch}\`` +
33+
' to start backporting?');
34+
if (shouldCheckout) {
35+
await runAsync('git', ['checkout', '-b', newBranch]);
36+
}
37+
38+
const cherries = commits.map(i => i.sha).reverse();
39+
const pendingCommands = [
40+
`git cherry-pick ${cherries.join(' ')}`,
41+
`git push -u <your-fork-remote> ${newBranch}`
42+
];
43+
const shouldPick = await cli.prompt(
44+
'Do you want to cherry-pick the commits?');
45+
if (!shouldPick) {
46+
this.hintCommands(pendingCommands);
47+
return;
48+
}
49+
50+
cli.log(`Running \`${pendingCommands[0]}\`...`);
51+
pendingCommands.shift();
52+
await runAsync('git', ['cherry-pick', ...cherries]);
53+
this.hintCommands(pendingCommands);
54+
}
55+
56+
hintCommands(commands) {
57+
this.cli.log('Tips: run the following commands to complete backporing');
58+
for (const command of commands) {
59+
this.cli.log(`$ ${command}`);
60+
}
61+
}
62+
63+
grep() {
64+
const { cli, owner, repo, prid } = this;
65+
const url = getPrURL(owner, repo, prid);
66+
67+
cli.log(`Looking for commits of ${url}...`);
68+
const re = `--grep=PR-URL: ${url}$`;
69+
const commits = runSync('git', [
70+
'log', '--all', re, '--format=%h %s'
71+
]).trim();
72+
if (!commits) {
73+
return [];
74+
}
75+
76+
return commits.split('\n').map((i) => {
77+
const match = i.match(/(\w+) (.+)/);
78+
return {
79+
sha: match[1],
80+
title: match[2]
81+
}
82+
});
83+
}
84+
}
85+
86+
module.exports = BackportSession;

lib/links.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,20 @@ LinkParser.parsePRFromURL = function(url) {
9292
return undefined;
9393
};
9494

95+
LinkParser.getPrURL = function(owner, repo, prid) {
96+
return `https://github.com/${owner}/${repo}/pull/${prid}`;
97+
};
98+
99+
const PR_URL_RE = /PR-URL: https:\/\/github.com\/.+/;
100+
LinkParser.parsePrURL = function(text) {
101+
if (typeof text !== 'string') {
102+
return undefined;
103+
}
104+
const match = text.match(PR_URL_RE);
105+
if (!match) {
106+
return undefined;
107+
}
108+
return LinkParser.parsePRFromURL(match[0]);
109+
};
110+
95111
module.exports = LinkParser;

0 commit comments

Comments
 (0)