Skip to content

Commit 3d4537a

Browse files
committed
chore(git-node): avoid dealing with patch files for landing
Working with patch files is, on the one hand, unclean because it requires manual parsing to figure out e.g. the commit message subjects, and on the other hand, poses actual problems when dealing with binary files. Clean the code up by fetching the original commits themselves and applying those commits directly. This also resolves some TODO comments, e.g. we can now check whether the commits that are to be applied match the ones reported through the GitHub API.
1 parent 3c3712a commit 3d4537a

File tree

3 files changed

+87
-53
lines changed

3 files changed

+87
-53
lines changed

lib/landing_session.js

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class LandingSession extends Session {
3030
this.lint = lint;
3131
this.autorebase = autorebase;
3232
this.fixupAll = fixupAll;
33+
this.expectedCommitShas = [];
3334
}
3435

3536
get argv() {
@@ -44,6 +45,8 @@ class LandingSession extends Session {
4445
async start(metadata) {
4546
const { cli } = this;
4647
this.startLanding();
48+
this.expectedCommitShas =
49+
metadata.data.commits.map(({ commit }) => commit.oid);
4750
const status = metadata.status ? 'should be ready' : 'is not ready';
4851
// NOTE(mmarchini): default answer is yes. If --yes is given, we need to be
4952
// more careful though, and we change the default to the result of our
@@ -78,34 +81,44 @@ class LandingSession extends Session {
7881
}
7982

8083
async downloadAndPatch() {
81-
const { cli, req, repo, owner, prid } = this;
84+
const { cli, repo, owner, prid, expectedCommitShas } = this;
8285

83-
// TODO(joyeecheung): restore previously downloaded patches
8486
cli.startSpinner(`Downloading patch for ${prid}`);
85-
const patch = await req.text(
86-
`https://github.com/${owner}/${repo}/pull/${prid}.patch`);
87-
this.savePatch(patch);
88-
cli.stopSpinner(`Downloaded patch to ${this.patchPath}`);
87+
await runAsync('git', [
88+
'fetch', `https://github.com/${owner}/${repo}.git`,
89+
`refs/pull/${prid}/merge`]);
90+
const [base, head] = (await runAsync('git', [
91+
'rev-parse', 'FETCH_HEAD^1', 'FETCH_HEAD^2'], { captureStdout: true }))
92+
.split('\n');
93+
const commitShas = (await runAsync('git', [
94+
'rev-list', `${base}..${head}`], { captureStdout: true }))
95+
.trim().split('\n');
96+
cli.stopSpinner(`Fetched commits as ${shortSha(base)}..${shortSha(head)}`);
8997
cli.separator();
90-
// TODO: check that patches downloaded match metadata.commits
98+
99+
const mismatchedCommits = [
100+
...commitShas.filter((sha) => !expectedCommitShas.includes(sha))
101+
.map((sha) => `Unexpected commit ${sha}`),
102+
...expectedCommitShas.filter((sha) => !commitShas.includes(sha))
103+
.map((sha) => `Missing commit ${sha}`)
104+
].join('\n');
105+
if (mismatchedCommits.length > 0) {
106+
cli.error(`Mismatched commits:\n${mismatchedCommits}`);
107+
process.exit(1);
108+
}
109+
110+
const commitInfo = { base, head, shas: commitShas };
111+
this.saveCommitInfo(commitInfo);
112+
91113
try {
92-
await forceRunAsync('git', ['am', this.patchPath], {
114+
await forceRunAsync('git', ['cherry-pick', `${base}..${head}`], {
93115
ignoreFailure: false
94116
});
95117
} catch (ex) {
96-
const should3Way = await cli.prompt(
97-
'The normal `git am` failed. Do you want to retry with 3-way merge?');
98-
if (should3Way) {
99-
await forceRunAsync('git', ['am', '--abort']);
100-
await runAsync('git', [
101-
'am',
102-
'-3',
103-
this.patchPath
104-
]);
105-
} else {
106-
cli.error('Failed to apply patches');
107-
process.exit(1);
108-
}
118+
await forceRunAsync('git', ['cherry-pick', '--abort']);
119+
120+
cli.error('Failed to apply patches');
121+
process.exit(1);
109122
}
110123

111124
// Check for and maybe assign any unmarked deprecations in the codebase.
@@ -126,7 +139,7 @@ class LandingSession extends Session {
126139
}
127140

128141
cli.ok('Patches applied');
129-
return patch;
142+
return commitInfo;
130143
}
131144

132145
getRebaseSuggestion(subjects) {
@@ -173,21 +186,13 @@ class LandingSession extends Session {
173186
}
174187
}
175188

176-
async tryCompleteLanding(patch) {
189+
async tryCompleteLanding(commitInfo) {
177190
const { cli } = this;
191+
const subjects = (await runAsync('git',
192+
['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
193+
{ captureStdout: true })).trim().split('\n');
178194

179-
const subjects = patch.match(/Subject: \[PATCH.*?\].*/g);
180-
if (!subjects) {
181-
cli.warn('Cannot get number of commits in the patch. ' +
182-
'It seems to be malformed');
183-
return;
184-
}
185-
186-
// XXX(joyeecheung) we cannot guarantee that no one will put a subject
187-
// line in the commit message but that seems unlikely (some deps update
188-
// might do that).
189-
if (subjects.length === 1) {
190-
// assert(subjects[0].startsWith('Subject: [PATCH]'))
195+
if (commitInfo.shas.length === 1) {
191196
const shouldAmend = await cli.prompt(
192197
'There is only one commit in this PR.\n' +
193198
'do you want to amend the commit message?');
@@ -247,7 +252,7 @@ class LandingSession extends Session {
247252
}
248253
await this.tryResetBranch();
249254

250-
const patch = await this.downloadAndPatch();
255+
const commitInfo = await this.downloadAndPatch();
251256

252257
const cleanLint = await this.validateLint();
253258
if (cleanLint === LINT_RESULTS.FAILED) {
@@ -280,7 +285,7 @@ class LandingSession extends Session {
280285

281286
this.startAmending();
282287

283-
await this.tryCompleteLanding(patch);
288+
await this.tryCompleteLanding(commitInfo);
284289
}
285290

286291
async amend() {
@@ -407,13 +412,13 @@ class LandingSession extends Session {
407412
}
408413
if (this.isApplying()) {
409414
// We're still resolving conflicts.
410-
if (this.amInProgress()) {
411-
cli.log('Looks like you are resolving a `git am` conflict');
415+
if (this.cherryPickInProgress()) {
416+
cli.log('Looks like you are resolving a `git cherry-pick` conflict');
412417
cli.log('Please run `git status` for help');
413418
} else {
414419
// Conflicts has been resolved - amend.
415420
this.startAmending();
416-
return this.tryCompleteLanding(this.patch);
421+
return this.tryCompleteLanding(this.commitInfo);
417422
}
418423
return;
419424
}

lib/run.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@ const { spawn, spawnSync } = require('child_process');
44

55
const IGNORE = '__ignore__';
66

7-
function runAsyncBase(cmd, args, options = {}) {
7+
function runAsyncBase(cmd, args, {
8+
ignoreFailure = true,
9+
spawnArgs,
10+
captureStdout = false
11+
} = {}) {
812
return new Promise((resolve, reject) => {
913
const child = spawn(cmd, args, Object.assign({
1014
cwd: process.cwd(),
11-
stdio: 'inherit'
12-
}, options.spawnArgs));
15+
stdio: captureStdout ? ['inherit', 'pipe', 'inherit'] : 'inherit'
16+
}, spawnArgs));
17+
let stdout;
18+
if (captureStdout) {
19+
stdout = '';
20+
child.stdout.setEncoding('utf8');
21+
child.stdout.on('data', (chunk) => { stdout += chunk; });
22+
}
1323
child.on('close', (code) => {
1424
if (code !== 0) {
15-
const { ignoreFailure = true } = options;
1625
if (ignoreFailure) {
1726
return reject(new Error(IGNORE));
1827
}
@@ -21,7 +30,7 @@ function runAsyncBase(cmd, args, options = {}) {
2130
err.messageOnly = true;
2231
return reject(err);
2332
}
24-
return resolve();
33+
return resolve(stdout);
2534
});
2635
});
2736
}

lib/session.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,12 @@ class Session {
171171
return readFile(this.metadataPath);
172172
}
173173

174-
get patchPath() {
175-
return path.join(this.pullDir, 'patch');
174+
get commitInfoPath() {
175+
return path.join(this.pullDir, 'commit-info');
176176
}
177177

178-
get patch() {
179-
return readFile(this.patchPath);
178+
get commitInfo() {
179+
return readJson(this.commitInfoPath);
180180
}
181181

182182
getMessagePath(rev) {
@@ -196,8 +196,8 @@ class Session {
196196
writeFile(this.metadataPath, status.metadata);
197197
}
198198

199-
savePatch(patch) {
200-
writeFile(this.patchPath, patch);
199+
saveCommitInfo(commitInfo) {
200+
writeJson(this.commitInfoPath, commitInfo);
201201
}
202202

203203
saveMessage(rev, message) {
@@ -218,20 +218,21 @@ class Session {
218218
if (this.session.state === AMENDING) {
219219
return true;
220220
} else if (this.isApplying()) {
221-
return !this.amInProgress();
221+
return !this.cherryPickInProgress();
222222
} else {
223223
return false;
224224
}
225225
}
226226

227227
readyToFinal() {
228-
if (this.amInProgress()) {
228+
if (this.amInProgress() || this.cherryPickInProgress()) {
229229
return false; // git am/rebase in progress
230230
}
231231
return this.session.state === AMENDING;
232232
}
233233

234234
// Refs: https://github.com/git/git/blob/99de064/git-rebase.sh#L208-L228
235+
// XXX: This may be unused at this point?
235236
amInProgress() {
236237
const amPath = path.join(this.gitDir, 'rebase-apply', 'applying');
237238
return fs.existsSync(amPath);
@@ -247,6 +248,11 @@ class Session {
247248
return fs.existsSync(normalRebasePath) || fs.existsSync(mergeRebasePath);
248249
}
249250

251+
cherryPickInProgress() {
252+
const cpPath = path.join(this.gitDir, 'CHERRY_PICK_HEAD');
253+
return fs.existsSync(cpPath);
254+
}
255+
250256
restore() {
251257
const sess = this.session;
252258
if (sess.prid) {
@@ -269,6 +275,19 @@ class Session {
269275
}
270276
}
271277

278+
async tryAbortCherryPick() {
279+
const { cli } = this;
280+
if (!this.cherryPickInProgress()) {
281+
return cli.ok('No git cherry-pick in progress');
282+
}
283+
const shouldAbortCherryPick = await cli.prompt(
284+
'Abort previous git cherry-pick sessions?');
285+
if (shouldAbortCherryPick) {
286+
await forceRunAsync('git', ['cherry-pick', '--abort']);
287+
cli.ok('Aborted previous git cherry-pick sessions');
288+
}
289+
}
290+
272291
async tryAbortRebase() {
273292
const { cli } = this;
274293
if (!this.rebaseInProgress()) {
@@ -284,6 +303,7 @@ class Session {
284303

285304
async tryResetBranch() {
286305
const { cli, upstream, branch } = this;
306+
await this.tryAbortCherryPick();
287307
await this.tryAbortAm();
288308
await this.tryAbortRebase();
289309

0 commit comments

Comments
 (0)