Skip to content

Commit 21dfff3

Browse files
committed
[wip] git node land prototype
1 parent 6c578ef commit 21dfff3

File tree

5 files changed

+227
-54
lines changed

5 files changed

+227
-54
lines changed

components/git/git-node-land

Lines changed: 138 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ const CLI = require('../../lib/cli');
55
const cli = new CLI(process.stderr);
66
const Request = require('../../lib/request');
77
const req = new Request();
8-
const { runPromise, runAsync, runSync } = require('../../lib/run');
8+
const {
9+
runPromise, runAsync, runSync, forceRunAsync
10+
} = require('../../lib/run');
911
const Session = require('../../lib/landing_session');
1012
const dir = process.cwd();
1113
const args = process.argv.slice(2);
@@ -40,8 +42,23 @@ if (result.length) {
4042
}
4143

4244
async function main(state, args) {
45+
let session;
46+
47+
try {
48+
session = Session.restore(dir);
49+
} catch (err) { // JSON error?
50+
if (state === ABORT) {
51+
session = new Session(dir);
52+
await abort(session);
53+
return;
54+
}
55+
cli.warn(
56+
'Failed to detect previous session. ' +
57+
'please run `git node land --abort`');
58+
return;
59+
}
60+
4361
if (state === START) {
44-
let session = Session.restore(dir);
4562
if (session.hasStarted()) {
4663
cli.warn(
4764
'Previous `git node land` session for ' +
@@ -52,19 +69,14 @@ async function main(state, args) {
5269
session = new Session(dir, parseInt(args[0]));
5370
await start(session);
5471
} else if (state === APPLY) {
55-
const session = Session.restore(dir);
5672
await apply(session);
5773
} else if (state === AMEND) {
58-
const session = Session.restore(dir);
5974
await amend(session);
6075
} else if (state === FINAL) {
61-
const session = Session.restore(dir);
6276
await final(session);
6377
} else if (state === ABORT) {
64-
const session = Session.restore(dir);
65-
session.abort();
78+
await abort(session);
6679
} else if (state === CONTINUE) {
67-
const session = Session.restore(dir);
6880
await continueSession(session);
6981
}
7082
}
@@ -78,35 +90,109 @@ async function start(session) {
7890
const response = await cli.prompt(
7991
`This PR ${status} to land, do you want to continue?`);
8092
if (response) {
81-
session.saveMetadata(status);
93+
session.saveMetadata(result);
8294
session.startApplying();
8395
return apply(session);
8496
} else {
85-
session.abort();
86-
cli.log('Landing session aborted');
97+
await abort(session);
8798
process.exit();
8899
}
89100
}
90101

102+
function getNotYetPushedCommits(session, verbose) {
103+
const upstream = session.upstream;
104+
const branch = session.branch;
105+
var revs;
106+
if (verbose) {
107+
revs = runSync('git',
108+
['log', '--oneline', `${upstream}/${branch}...HEAD`]);
109+
} else {
110+
revs = runSync('git', ['rev-list', `${upstream}/${branch}...HEAD`]);
111+
}
112+
113+
if (!revs.trim()) {
114+
return [];
115+
}
116+
return revs.trim().split('\n');
117+
}
118+
119+
async function tryAbortAm(session, cli) {
120+
if (session.amInProgress()) {
121+
const shouldAbortAm = await cli.prompt(
122+
'Abort previous git am sessions?');
123+
if (shouldAbortAm) {
124+
await forceRunAsync('git', ['am', '--abort']);
125+
cli.ok('Aborted previous git am sessions');
126+
}
127+
} else {
128+
cli.ok('No git am in progress');
129+
}
130+
}
131+
132+
async function tryAbortRebase(session, cli) {
133+
if (session.rebaseInProgress()) {
134+
const shouldAbortRebase = await cli.prompt(
135+
'Abort previous git rebase sessions?');
136+
if (shouldAbortRebase) {
137+
await forceRunAsync('git', ['rebase', '--abort']);
138+
cli.ok('Aborted previous git rebase sessions');
139+
}
140+
} else {
141+
cli.ok('No git rebase in progress');
142+
}
143+
}
144+
145+
async function tryResetHead(session, cli) {
146+
const branch = `${session.upstream}/${session.branch}`;
147+
cli.startSpinner(`Bringing ${branch} up to date`);
148+
await runAsync('git',
149+
['fetch', session.upstream, session.branch]);
150+
cli.stopSpinner(`${branch} is now up-to-date`);
151+
const notYetPushed = getNotYetPushedCommits(session, true);
152+
if (notYetPushed.length) {
153+
const branch = `${session.upstream}/${session.branch}`;
154+
cli.log(`Found strayed commits in ${branch}:\n` +
155+
` - ${notYetPushed.join('\n - ')}`);
156+
const shouldReset = await cli.prompt(`Reset to ${branch}?`);
157+
if (shouldReset) {
158+
await runAsync('git', ['reset', '--hard', branch]);
159+
cli.ok(`Reset to ${branch}`);
160+
}
161+
}
162+
}
163+
164+
async function tryResetBranch(session, cli) {
165+
await tryAbortAm(session, cli);
166+
await tryAbortRebase(session, cli);
167+
168+
const branch = `${session.upstream}/${session.branch}`;
169+
const shouldResetHead = await cli.prompt(
170+
`Do you want to try reset the branch to ${branch}?`);
171+
if (shouldResetHead) {
172+
await tryResetHead(session, cli);
173+
}
174+
}
175+
176+
async function abort(session) {
177+
session.abort();
178+
await tryResetBranch(session, cli);
179+
cli.log(`Aborted \`git node land\` session in ${session.ncuDir}`);
180+
}
181+
91182
async function apply(session) {
92183
if (!session.readyToApply()) {
93184
cli.warn('This session can not proceed to apply patches, ' +
94185
'run `git node land --abort`');
95186
return;
96187
}
97188

98-
if (session.hasAM()) {
99-
const shouldAbortAm = await cli.prompt('Abort previous git am sessions?');
100-
if (shouldAbortAm) {
101-
await runAsync('git', ['am', '--abort']);
102-
}
103-
}
189+
await tryResetBranch(session, cli);
104190

105191
const { repo, owner, prid } = session;
106192
// TODO: restore previously downloaded patches
107193
cli.startSpinner(`Downloading patch for ${prid}`);
108194
const patch = await req.promise({
109-
url: `https://github.com/nodejs/${owner}/${repo}/${prid}.patch`
195+
url: `https://github.com/${owner}/${repo}/pull/${prid}.patch`
110196
});
111197
session.savePatch(patch);
112198
cli.stopSpinner(`Downloaded patch to ${session.patchPath}`);
@@ -118,14 +204,29 @@ async function apply(session) {
118204
session.startAmending();
119205
if (/Subject: \[PATCH\]/.test(patch)) {
120206
const shouldAmend = await cli.prompt(
121-
'There is only one patch to apply.\n' +
207+
'There is only one commit in this PR.\n' +
122208
'do you want to amend the commit message?');
123209
if (shouldAmend) {
124210
const canFinal = await amend(session);
125211
if (canFinal) {
126212
return final(session);
127213
}
128214
}
215+
} else {
216+
const re = /Subject: \[PATCH 1\/(\d+)\]/;
217+
const match = patch.match(re);
218+
if (!match) {
219+
cli.warn('Cannot get number of commits in the patch. ' +
220+
'It seems to be malformed');
221+
return;
222+
}
223+
const upstream = session.upstream;
224+
const branch = session.branch;
225+
cli.log(
226+
`There are ${match[1]} commits in the PR.\n` +
227+
`Please run \`git rebase ${upstream}/${branch} -i\` ` +
228+
'and use `git node land --amend` to amend the commit messages');
229+
// TODO: do git rebase automatically?
129230
}
130231
}
131232

@@ -136,16 +237,18 @@ async function amend(session) {
136237
}
137238

138239
const rev = runSync('git', ['rev-parse', 'HEAD']);
139-
const original = runSync('git', ['show', rev, '-s', '--format=%B']);
140-
const metadata = session.metadata.split('\n');
240+
const original = runSync('git', ['show', 'HEAD', '-s', '--format=%B']).trim();
241+
const metadata = session.metadata.trim().split('\n');
141242
const amended = original.split('\n');
142-
if (amended[amended.length - 1] !== '\n') {
143-
amended.push('\n');
243+
if (amended[amended.length - 1] !== '') {
244+
amended.push('');
144245
}
145246

146247
for (const line of metadata) {
147248
if (original.includes(line)) {
148-
cli.warn(`Found ${line}, skipping..`);
249+
if (line) {
250+
cli.warn(`Found ${line}, skipping..`);
251+
}
149252
} else {
150253
amended.push(line);
151254
}
@@ -154,15 +257,16 @@ async function amend(session) {
154257
const message = amended.join('\n') + '\n';
155258
const messageFile = session.saveMessage(rev, message);
156259
cli.separator('New Message');
157-
cli.log(message);
260+
cli.log(message.trim());
261+
cli.separator();
158262
const takeMessage = await cli.prompt('Use this message?');
159263
if (takeMessage) {
160264
await runAsync('git', ['commit', '--amend', '-F', messageFile]);
161-
session.markAsAmended(rev);
265+
// session.markAsAmended(rev);
162266
return true;
163267
}
164268

165-
cli.log(`Please manually edit ${messageFile}, then run ` +
269+
cli.log(`Please manually edit ${messageFile}, then run\n` +
166270
`\`git commit --amend -F ${messageFile}\` to finish amending the message`);
167271
return false;
168272
};
@@ -172,13 +276,16 @@ async function final(session) {
172276
cli.warn('Not yet ready to final');
173277
return;
174278
}
175-
176279
const upstream = session.upstream;
177280
const branch = session.branch;
178-
const notYetPushed = runSync('git',
179-
['rev-list', `${upstream}/${branch}...HEAD`]).split('\n');
281+
const notYetPushed = getNotYetPushedCommits(session);
282+
const notYetPushedVerbose = getNotYetPushedCommits(session, true);
180283
await runAsync('core-validate-commit', notYetPushed);
181-
cli.log('This session is ready to be completed.');
284+
cli.separator();
285+
cli.log('The following commits are ready to be pushed to ' +
286+
`${upstream}/${branch}`);
287+
cli.log(`- ${notYetPushedVerbose.join('\n- ')}`);
288+
cli.separator();
182289
cli.log(`run \`git push ${upstream} ${branch}\` to finish landing`);
183290
const shouldClean = await cli.prompt('Clean up generated temporary files?');
184291
if (shouldClean) {

lib/cli.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
const ora = require('ora');
77
const { EOL } = require('os');
88
const chalk = require('chalk');
9+
const read = require('read');
910

1011
const warning = chalk.yellow(warningRaw);
1112
const error = chalk.red(cross);
@@ -31,6 +32,28 @@ class CLI {
3132
this.spinner = ora({ stream });
3233
}
3334

35+
prompt(question, defaultAnswer = true) {
36+
const option =
37+
`[${(defaultAnswer ? 'Y' : 'y')}/${(defaultAnswer ? 'n' : 'N')}]`;
38+
return new Promise((resolve, reject) => {
39+
read({prompt: `${question} ${option} `}, (err, answer) => {
40+
if (err) {
41+
reject(err);
42+
}
43+
if (answer === undefined || answer === null) {
44+
reject(new Error('__ignore__'));
45+
}
46+
const trimmed = answer.toLowerCase().trim();
47+
if (!trimmed) {
48+
resolve(defaultAnswer);
49+
} else if (trimmed === 'y') {
50+
resolve(true);
51+
}
52+
resolve(false);
53+
});
54+
});
55+
}
56+
3457
startSpinner(text) {
3558
this.spinner.text = text;
3659
this.spinner.start();

0 commit comments

Comments
 (0)