Skip to content

Commit 2f7f8bd

Browse files
huntiefacebook-github-bot
authored andcommitted
Enable committing under Git checkouts
Summary: `scripts/debugger-frontend/sync-and-build` with `--create-diff` only worked under fbsource, making commit creation (specifically the generated changes table) inconvenient outside of Meta. This diff generalises this script to also handle commit creation under Git. **Changes** - Synced `debugger-frontend` artifacts are now always committed, regardless of version control backend. Internal Mercurial behaviour is forked to a `fbsource-backend.fb.js` script. - `--create-diff` is narrowed to draft Phabricator diff submission (fbsource only). - The script now aborts if there are any working copy changes. Changelog: [Internal] Differential Revision: D116031207
1 parent 2ce2c07 commit 2f7f8bd

3 files changed

Lines changed: 164 additions & 110 deletions

File tree

packages/debugger-frontend/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ node scripts/debugger-frontend/sync-and-build --branch 0.73-stable
3636
```
3737

3838
By default, this will clone and build from [react/react-native-devtools-frontend](https://github.com/react/react-native-devtools-frontend).
39+
40+
The updated files are committed on completion, with a generated summary and changelog of the synced revisions.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
/*::
12+
export type Command = [string, Array<string>];
13+
14+
export type CommitMessageParts = Readonly<{
15+
title: string,
16+
summary: string,
17+
changelogTable: string,
18+
changelogEntry: string,
19+
}>;
20+
21+
// Commands are returned rather than run so that the caller can report them
22+
// consistently with the rest of the sync.
23+
export type CommitBackend = Readonly<{
24+
// Must print nothing when the working directory is clean.
25+
status: Command,
26+
// Joined with blank lines; empty entries are dropped.
27+
messageBlocks: (parts: CommitMessageParts) => ReadonlyArray<string>,
28+
commit: (packagePath: string, messageFile: string) => ReadonlyArray<Command>,
29+
}>;
30+
31+
export type ResolveContext = Readonly<{
32+
createDiff: boolean,
33+
isUnlandable: boolean,
34+
}>;
35+
*/
36+
37+
const GIT /*: CommitBackend */ = {
38+
status: ['git', ['status', '--porcelain', '--', '.']],
39+
messageBlocks: ({title, summary, changelogTable, changelogEntry}) => [
40+
title,
41+
summary,
42+
changelogTable,
43+
`Changelog: ${changelogEntry}`,
44+
],
45+
commit: (packagePath, messageFile) => [
46+
['git', ['add', '-A', '--', packagePath]],
47+
['git', ['commit', '-F', messageFile, '--', packagePath]],
48+
],
49+
};
50+
51+
function moduleExists(modulePath /*: string */) /*: boolean */ {
52+
try {
53+
require.resolve(modulePath);
54+
return true;
55+
} catch {
56+
return false;
57+
}
58+
}
59+
60+
// Resolved before requiring so that a failure to load the module is not
61+
// mistaken for its absence in the open source repo. The filename is
62+
// deliberately not `commit-backend.fb.js`: Flow and Metro resolve `X.fb.js`
63+
// ahead of `X.js`, but Node - which runs this script - does not.
64+
const resolveFb /*: ?(context: ResolveContext) => Promise<?CommitBackend> */ =
65+
moduleExists('./fbsource-backend.fb.js')
66+
? // $FlowFixMe[cannot-resolve-module] - not resolvable in OSS
67+
require('./fbsource-backend.fb.js')
68+
: null;
69+
70+
async function resolveCommitBackend(
71+
context /*: ResolveContext */,
72+
) /*: Promise<CommitBackend> */ {
73+
const fbBackend = await resolveFb?.(context);
74+
if (fbBackend != null) {
75+
return fbBackend;
76+
}
77+
if (context.createDiff) {
78+
throw new Error('--create-diff requires an fbsource checkout');
79+
}
80+
return GIT;
81+
}
82+
83+
module.exports = {
84+
resolveCommitBackend,
85+
};

scripts/debugger-frontend/sync-and-build.js

Lines changed: 77 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
* @format
99
*/
1010

11-
const {PACKAGES_DIR} = require('../shared/consts');
11+
const {PACKAGES_DIR, REPO_ROOT} = require('../shared/consts');
12+
const {resolveCommitBackend} = require('./commit-backend');
1213
// $FlowFixMe[untyped-import]: TODO type ansi-styles
1314
const ansiStyles = require('ansi-styles');
1415
const {execSync, spawnSync} = require('node:child_process');
@@ -40,15 +41,7 @@ const config = {
4041
};
4142

4243
/*::
43-
type DiffBaseInfo = {
44-
packagePath: string,
45-
baseGitRevision: string,
46-
};
47-
48-
type ParsedBuildInfo = {
49-
gitRevision: string,
50-
isLocalCheckout: boolean,
51-
};
44+
import type {CommitBackend} from './commit-backend';
5245
*/
5346

5447
async function main() {
@@ -95,10 +88,15 @@ async function main() {
9588

9689
await checkRequiredTools();
9790
const packagePath = path.join(PACKAGES_DIR, 'debugger-frontend');
98-
let diffBaseInfo;
99-
if (createDiff) {
100-
diffBaseInfo = await checkCanCreateDiff(packagePath);
101-
}
91+
// Neither a local checkout nor a skipped build produces assets that provably
92+
// match the revision the commit will claim.
93+
const isUnlandable = localCheckoutPath != null || noBuild;
94+
const commitBackend = await resolveCommitBackend({
95+
createDiff: createDiff === true,
96+
isUnlandable,
97+
});
98+
await checkCanCommit(packagePath, commitBackend);
99+
const baseGitRevision = await readGitRevision(packagePath);
102100
const {checkoutPath} = await buildDebuggerFrontend(
103101
packagePath,
104102
scratchPath,
@@ -109,14 +107,19 @@ async function main() {
109107
noBuild,
110108
},
111109
);
112-
if (createDiff && diffBaseInfo) {
113-
await createSyncDiff(diffBaseInfo, scratchPath, {checkoutPath, noBuild});
114-
}
110+
await commitSync({
111+
baseGitRevision,
112+
checkoutPath,
113+
commitBackend,
114+
isUnlandable,
115+
packagePath,
116+
scratchPath,
117+
});
115118
await cleanup(scratchPath, keepScratch === true);
116119
if (!noBuild) {
117120
process.stdout.write(
118121
styleText('green', 'Sync done.') +
119-
' Check in any updated files under packages/debugger-frontend.\n',
122+
' Committed updated files under packages/debugger-frontend.\n',
120123
);
121124
}
122125
}
@@ -128,15 +131,16 @@ function showHelp() {
128131
Sync and build the debugger frontend into @react-native/debugger-frontend.
129132
130133
By default, checks out the currently pinned revision of the DevTools frontend.
131-
If an existing checkout path is provided, builds it instead.
134+
If an existing checkout path is provided, builds it instead. The updated files
135+
are committed on completion.
132136
133137
Options:
134138
--branch The DevTools frontend branch to use. Ignored when
135139
providing a local checkout path.
140+
--create-diff Submit the commit as a draft diff (Meta-internal).
136141
--nohooks Don't run gclient hooks in the devtools checkout (useful
137142
for existing checkouts).
138143
--keep-scratch Don't clean up temporary files.
139-
--create-diff Create a diff with the updated files.
140144
--no-build Skip actually building and updating the frontend.
141145
`);
142146
}
@@ -453,70 +457,36 @@ async function spawnSafe(
453457
}
454458
}
455459

456-
async function checkCanCreateDiff(
460+
async function checkCanCommit(
457461
packagePath /*: string */,
458-
) /*: Promise<DiffBaseInfo> */ {
459-
process.stdout.write('Checking that we can create a diff' + '\n');
460-
try {
461-
const {stdout: hgRootStdout} = await spawnSafe('hg', ['root'], {
462-
cwd: packagePath,
463-
stdio: ['ignore', 'pipe', 'inherit'],
464-
});
465-
const repoRoot = hgRootStdout.toString().trim();
466-
const projectid = (
467-
await fs.readFile(path.join(repoRoot, '.projectid'), 'utf8')
468-
).trim();
469-
if (projectid !== 'fbsource') {
470-
throw new Error(
471-
'Expected .projectid to contain "fbsource" but found: ' + projectid,
472-
);
473-
}
474-
await spawnSafe('jf', ['-v'], {cwd: packagePath, stdio: 'ignore'});
475-
} catch (e) {
476-
process.stderr.write(
477-
'Must be in an fbsource checkout (Meta-only) to create a diff\n',
478-
);
479-
throw e;
480-
}
481-
try {
482-
const {stdout: hgStatusStdout} = await spawnSafe(
483-
'hg',
484-
['status', 'BUILD_INFO'],
485-
{cwd: packagePath, stdio: ['ignore', 'pipe', 'inherit']},
462+
commitBackend /*: CommitBackend */,
463+
) {
464+
process.stdout.write('Checking that we can commit' + '\n');
465+
const [statusCmd, statusArgs] = commitBackend.status;
466+
const {stdout} = await spawnSafe(statusCmd, statusArgs, {
467+
cwd: packagePath,
468+
stdio: ['ignore', 'pipe', 'inherit'],
469+
});
470+
const pendingChanges = stdout.toString().trim();
471+
if (pendingChanges !== '') {
472+
throw new Error(
473+
`Must have a clean working copy under ${path.relative(REPO_ROOT, packagePath)} to commit:\n${pendingChanges}`,
486474
);
487-
if (hgStatusStdout.toString().trim() !== '') {
488-
throw new Error(
489-
'Must have a clean base BUILD_INFO file to create a diff',
490-
);
491-
}
492-
const {gitRevision: baseGitRevision} = await readBuildInfo(packagePath);
493-
return {
494-
packagePath,
495-
baseGitRevision,
496-
};
497-
} catch (e) {
498-
process.stderr.write('Must have a BUILD_INFO file to create a diff\n');
499-
throw e;
500475
}
501476
}
502477

503-
async function readBuildInfo(
478+
async function readGitRevision(
504479
packagePath /*: string*/,
505-
) /*: Promise<ParsedBuildInfo> */ {
480+
) /*: Promise<string> */ {
506481
const buildInfo = await fs.readFile(
507482
path.join(packagePath, 'BUILD_INFO'),
508483
'utf8',
509484
);
510485
const GIT_REV_RE = /^Git revision: ([0-9a-f]{40})/m;
511-
const gitRevision = nullthrows(
486+
return nullthrows(
512487
GIT_REV_RE.exec(buildInfo),
513488
'Could not extract git revision from BUILD_INFO',
514489
)[1];
515-
const isLocalCheckout = !/^Is local checkout: false$/m.test(buildInfo);
516-
return {
517-
isLocalCheckout,
518-
gitRevision,
519-
};
520490
}
521491

522492
function generateChangelogTable(
@@ -542,7 +512,6 @@ function generateChangelogTable(
542512
const limitedCommits = commits.slice(0, maxCommits);
543513

544514
const tableRows = [
545-
'',
546515
'### Changelog',
547516
'',
548517
'| Commit | Author | Date/Time | Subject |',
@@ -582,19 +551,26 @@ function generateChangelogTable(
582551
return changelogTable;
583552
}
584553

585-
async function createSyncDiff(
586-
diffBaseInfo /*: DiffBaseInfo */,
587-
scratchPath /*: string */,
554+
async function commitSync(
588555
{
556+
baseGitRevision,
589557
checkoutPath,
590-
noBuild,
591-
} /*: Readonly<{checkoutPath: string, noBuild: boolean}> */,
558+
commitBackend,
559+
isUnlandable,
560+
packagePath,
561+
scratchPath,
562+
} /*: Readonly<{
563+
baseGitRevision: string,
564+
checkoutPath: string,
565+
commitBackend: CommitBackend,
566+
isUnlandable: boolean,
567+
packagePath: string,
568+
scratchPath: string,
569+
}> */,
592570
) {
593-
process.stdout.write('Creating a sync diff\n');
594-
const {packagePath, baseGitRevision} = diffBaseInfo;
571+
process.stdout.write('Committing updated files\n');
595572
const baseGitRevisionShort = baseGitRevision.slice(0, 7);
596-
const {gitRevision: newGitRevision, isLocalCheckout} =
597-
await readBuildInfo(packagePath);
573+
const newGitRevision = await readGitRevision(packagePath);
598574
const newGitRevisionShort = newGitRevision.slice(0, 7);
599575

600576
// Generate the changelog table
@@ -604,39 +580,30 @@ async function createSyncDiff(
604580
newGitRevision,
605581
);
606582

607-
const commitMessage = [
608-
(isLocalCheckout || noBuild ? 'DO NOT LAND ' : '') +
609-
`[RN] Update debugger-frontend from ${baseGitRevisionShort}...${newGitRevisionShort}`,
610-
'',
611-
'Summary:',
612-
`Changelog: [Internal] - Update \`@react-native/debugger-frontend\` from ${baseGitRevisionShort}...${newGitRevisionShort}`,
613-
'',
614-
`Resyncs \`@react-native/debugger-frontend\` from GitHub - see \`rn-chrome-devtools-frontend\` [changelog](${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}).`,
615-
'',
616-
changelogTable,
617-
'',
618-
'Test Plan: CI',
619-
'',
620-
'Reviewers: #rn-debugging',
621-
'',
622-
'Tags: msdkland[metro]',
623-
'',
624-
].join('\n');
583+
const revisionRange = `${baseGitRevisionShort}...${newGitRevisionShort}`;
584+
const title =
585+
(isUnlandable ? 'DO NOT LAND ' : '') +
586+
`[RN] Update debugger-frontend from ${revisionRange}`;
587+
const changelogEntry = `[Internal] - Update \`@react-native/debugger-frontend\` from ${revisionRange}`;
588+
const compareUrl = `${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}`;
589+
const summary =
590+
'Resyncs `@react-native/debugger-frontend` from GitHub - see ' +
591+
'`rn-chrome-devtools-frontend` ' +
592+
`[changelog](${compareUrl}).`;
593+
594+
const commitMessage =
595+
commitBackend
596+
.messageBlocks({title, summary, changelogTable, changelogEntry})
597+
.filter(block => block !== '')
598+
.join('\n\n') + '\n';
625599

626600
const commitMessageFile = path.join(scratchPath, 'commit-msg');
627601
await fs.writeFile(commitMessageFile, commitMessage);
628-
await spawnSafe(
629-
'hg',
630-
['commit', packagePath, '--addremove', '-l', commitMessageFile],
631-
{cwd: packagePath},
632-
);
633-
await spawnSafe('jf', ['submit', '--draft'], {
634-
cwd: packagePath,
635-
});
636-
if (noBuild) {
637-
await spawnSafe('jf', ['action', '--abandon'], {
638-
cwd: packagePath,
639-
});
602+
for (const [cmd, args] of commitBackend.commit(
603+
packagePath,
604+
commitMessageFile,
605+
)) {
606+
await spawnSafe(cmd, args, {cwd: packagePath});
640607
}
641608
}
642609

0 commit comments

Comments
 (0)