mirror of
https://github.com/react/react-native.git
synced 2026-09-28 05:13:06 +08:00
Support committing debugger-frontend sync under Git checkouts (#57997)
Summary: Pull Request resolved: https://github.com/react/react-native/pull/57997 `scripts/debugger-frontend/sync-and-build` with `--create-diff` only worked under fbsource, making commit creation (in particular the generated changes table) inconvenient outside of Meta. This diff lifts commit creation functionality to both Git and Mercurial (`sl`), and makes writing a local commit the default behaviour. **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] Reviewed By: vzaidman Differential Revision: D116031207 fbshipit-source-id: 8ef3ed465b91d8f71ffa44fd1cba16f28860e3a9
This commit is contained in:
committed by
meta-codesync[bot]
parent
3eab03df4c
commit
526016d950
@@ -36,3 +36,5 @@ node scripts/debugger-frontend/sync-and-build --branch 0.73-stable
|
||||
```
|
||||
|
||||
By default, this will clone and build from [react/react-native-devtools-frontend](https://github.com/react/react-native-devtools-frontend).
|
||||
|
||||
The updated files are committed on completion, with a generated summary and changelog of the synced revisions.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
/*::
|
||||
export type Command = [string, Array<string>];
|
||||
|
||||
export type CommitMessageParts = Readonly<{
|
||||
title: string,
|
||||
summary: string,
|
||||
changelogTable: string,
|
||||
changelogEntry: string,
|
||||
}>;
|
||||
|
||||
// Commands are returned rather than run so that the caller can report them
|
||||
// consistently with the rest of the sync.
|
||||
export type CommitBackend = Readonly<{
|
||||
// Must print nothing when the working directory is clean.
|
||||
status: Command,
|
||||
// Joined with blank lines; empty entries are dropped.
|
||||
messageBlocks: (parts: CommitMessageParts) => ReadonlyArray<string>,
|
||||
commit: (packagePath: string, messageFile: string) => ReadonlyArray<Command>,
|
||||
}>;
|
||||
|
||||
export type ResolveContext = Readonly<{
|
||||
createDiff: boolean,
|
||||
noBuild: boolean,
|
||||
}>;
|
||||
*/
|
||||
|
||||
const GIT /*: CommitBackend */ = {
|
||||
status: ['git', ['status', '--porcelain', '--', '.']],
|
||||
messageBlocks: ({title, summary, changelogTable, changelogEntry}) => [
|
||||
title,
|
||||
summary,
|
||||
changelogTable,
|
||||
`Changelog: ${changelogEntry}`,
|
||||
],
|
||||
commit: (packagePath, messageFile) => [
|
||||
['git', ['add', '-A', '--', packagePath]],
|
||||
['git', ['commit', '-F', messageFile, '--', packagePath]],
|
||||
],
|
||||
};
|
||||
|
||||
function moduleExists(modulePath /*: string */) /*: boolean */ {
|
||||
try {
|
||||
require.resolve(modulePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolved before requiring so that a failure to load the module is not
|
||||
// mistaken for its absence in the open source repo. The filename is
|
||||
// deliberately not `commit-backend.fb.js`: Flow and Metro resolve `X.fb.js`
|
||||
// ahead of `X.js`, but Node - which runs this script - does not.
|
||||
const resolveFb /*: ?(context: ResolveContext) => Promise<?CommitBackend> */ =
|
||||
moduleExists('./fbsource-backend.fb.js')
|
||||
? // $FlowFixMe[cannot-resolve-module] - not resolvable in OSS
|
||||
require('./fbsource-backend.fb.js')
|
||||
: null;
|
||||
|
||||
async function resolveCommitBackend(
|
||||
context /*: ResolveContext */,
|
||||
) /*: Promise<CommitBackend> */ {
|
||||
const fbBackend = await resolveFb?.(context);
|
||||
if (fbBackend != null) {
|
||||
return fbBackend;
|
||||
}
|
||||
if (context.createDiff) {
|
||||
throw new Error('--create-diff requires an fbsource checkout');
|
||||
}
|
||||
return GIT;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveCommitBackend,
|
||||
};
|
||||
@@ -8,7 +8,8 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
const {PACKAGES_DIR} = require('../shared/consts');
|
||||
const {PACKAGES_DIR, REPO_ROOT} = require('../shared/consts');
|
||||
const {resolveCommitBackend} = require('./commit-backend');
|
||||
// $FlowFixMe[untyped-import]: TODO type ansi-styles
|
||||
const ansiStyles = require('ansi-styles');
|
||||
const {execSync, spawnSync} = require('node:child_process');
|
||||
@@ -40,15 +41,7 @@ const config = {
|
||||
};
|
||||
|
||||
/*::
|
||||
type DiffBaseInfo = {
|
||||
packagePath: string,
|
||||
baseGitRevision: string,
|
||||
};
|
||||
|
||||
type ParsedBuildInfo = {
|
||||
gitRevision: string,
|
||||
isLocalCheckout: boolean,
|
||||
};
|
||||
import type {CommitBackend} from './commit-backend';
|
||||
*/
|
||||
|
||||
async function main() {
|
||||
@@ -95,10 +88,12 @@ async function main() {
|
||||
|
||||
await checkRequiredTools();
|
||||
const packagePath = path.join(PACKAGES_DIR, 'debugger-frontend');
|
||||
let diffBaseInfo;
|
||||
if (createDiff) {
|
||||
diffBaseInfo = await checkCanCreateDiff(packagePath);
|
||||
}
|
||||
const commitBackend = await resolveCommitBackend({
|
||||
createDiff: createDiff === true,
|
||||
noBuild,
|
||||
});
|
||||
await checkCanCommit(packagePath, commitBackend);
|
||||
const baseGitRevision = await readGitRevision(packagePath);
|
||||
const {checkoutPath} = await buildDebuggerFrontend(
|
||||
packagePath,
|
||||
scratchPath,
|
||||
@@ -109,14 +104,19 @@ async function main() {
|
||||
noBuild,
|
||||
},
|
||||
);
|
||||
if (createDiff && diffBaseInfo) {
|
||||
await createSyncDiff(diffBaseInfo, scratchPath, {checkoutPath, noBuild});
|
||||
}
|
||||
await commitSync({
|
||||
baseGitRevision,
|
||||
checkoutPath,
|
||||
commitBackend,
|
||||
noBuild,
|
||||
packagePath,
|
||||
scratchPath,
|
||||
});
|
||||
await cleanup(scratchPath, keepScratch === true);
|
||||
if (!noBuild) {
|
||||
process.stdout.write(
|
||||
styleText('green', 'Sync done.') +
|
||||
' Check in any updated files under packages/debugger-frontend.\n',
|
||||
' Committed updated files under packages/debugger-frontend.\n',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,15 +128,16 @@ function showHelp() {
|
||||
Sync and build the debugger frontend into @react-native/debugger-frontend.
|
||||
|
||||
By default, checks out the currently pinned revision of the DevTools frontend.
|
||||
If an existing checkout path is provided, builds it instead.
|
||||
If an existing checkout path is provided, builds it instead. The updated files
|
||||
are committed on completion.
|
||||
|
||||
Options:
|
||||
--branch The DevTools frontend branch to use. Ignored when
|
||||
providing a local checkout path.
|
||||
--create-diff Submit the commit as a draft diff (Meta-internal).
|
||||
--nohooks Don't run gclient hooks in the devtools checkout (useful
|
||||
for existing checkouts).
|
||||
--keep-scratch Don't clean up temporary files.
|
||||
--create-diff Create a diff with the updated files.
|
||||
--no-build Skip actually building and updating the frontend.
|
||||
`);
|
||||
}
|
||||
@@ -453,70 +454,36 @@ async function spawnSafe(
|
||||
}
|
||||
}
|
||||
|
||||
async function checkCanCreateDiff(
|
||||
async function checkCanCommit(
|
||||
packagePath /*: string */,
|
||||
) /*: Promise<DiffBaseInfo> */ {
|
||||
process.stdout.write('Checking that we can create a diff' + '\n');
|
||||
try {
|
||||
const {stdout: hgRootStdout} = await spawnSafe('hg', ['root'], {
|
||||
cwd: packagePath,
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
const repoRoot = hgRootStdout.toString().trim();
|
||||
const projectid = (
|
||||
await fs.readFile(path.join(repoRoot, '.projectid'), 'utf8')
|
||||
).trim();
|
||||
if (projectid !== 'fbsource') {
|
||||
throw new Error(
|
||||
'Expected .projectid to contain "fbsource" but found: ' + projectid,
|
||||
);
|
||||
}
|
||||
await spawnSafe('jf', ['-v'], {cwd: packagePath, stdio: 'ignore'});
|
||||
} catch (e) {
|
||||
process.stderr.write(
|
||||
'Must be in an fbsource checkout (Meta-only) to create a diff\n',
|
||||
commitBackend /*: CommitBackend */,
|
||||
) {
|
||||
process.stdout.write('Checking that we can commit' + '\n');
|
||||
const [statusCmd, statusArgs] = commitBackend.status;
|
||||
const {stdout} = await spawnSafe(statusCmd, statusArgs, {
|
||||
cwd: packagePath,
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
});
|
||||
const pendingChanges = stdout.toString().trim();
|
||||
if (pendingChanges !== '') {
|
||||
throw new Error(
|
||||
`Must have a clean working copy under ${path.relative(REPO_ROOT, packagePath)} to commit:\n${pendingChanges}`,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
const {stdout: hgStatusStdout} = await spawnSafe(
|
||||
'hg',
|
||||
['status', 'BUILD_INFO'],
|
||||
{cwd: packagePath, stdio: ['ignore', 'pipe', 'inherit']},
|
||||
);
|
||||
if (hgStatusStdout.toString().trim() !== '') {
|
||||
throw new Error(
|
||||
'Must have a clean base BUILD_INFO file to create a diff',
|
||||
);
|
||||
}
|
||||
const {gitRevision: baseGitRevision} = await readBuildInfo(packagePath);
|
||||
return {
|
||||
packagePath,
|
||||
baseGitRevision,
|
||||
};
|
||||
} catch (e) {
|
||||
process.stderr.write('Must have a BUILD_INFO file to create a diff\n');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function readBuildInfo(
|
||||
async function readGitRevision(
|
||||
packagePath /*: string*/,
|
||||
) /*: Promise<ParsedBuildInfo> */ {
|
||||
) /*: Promise<string> */ {
|
||||
const buildInfo = await fs.readFile(
|
||||
path.join(packagePath, 'BUILD_INFO'),
|
||||
'utf8',
|
||||
);
|
||||
const GIT_REV_RE = /^Git revision: ([0-9a-f]{40})/m;
|
||||
const gitRevision = nullthrows(
|
||||
return nullthrows(
|
||||
GIT_REV_RE.exec(buildInfo),
|
||||
'Could not extract git revision from BUILD_INFO',
|
||||
)[1];
|
||||
const isLocalCheckout = !/^Is local checkout: false$/m.test(buildInfo);
|
||||
return {
|
||||
isLocalCheckout,
|
||||
gitRevision,
|
||||
};
|
||||
}
|
||||
|
||||
function generateChangelogTable(
|
||||
@@ -542,7 +509,6 @@ function generateChangelogTable(
|
||||
const limitedCommits = commits.slice(0, maxCommits);
|
||||
|
||||
const tableRows = [
|
||||
'',
|
||||
'### Changelog',
|
||||
'',
|
||||
'| Commit | Author | Date/Time | Subject |',
|
||||
@@ -582,19 +548,26 @@ function generateChangelogTable(
|
||||
return changelogTable;
|
||||
}
|
||||
|
||||
async function createSyncDiff(
|
||||
diffBaseInfo /*: DiffBaseInfo */,
|
||||
scratchPath /*: string */,
|
||||
async function commitSync(
|
||||
{
|
||||
baseGitRevision,
|
||||
checkoutPath,
|
||||
commitBackend,
|
||||
noBuild,
|
||||
} /*: Readonly<{checkoutPath: string, noBuild: boolean}> */,
|
||||
packagePath,
|
||||
scratchPath,
|
||||
} /*: Readonly<{
|
||||
baseGitRevision: string,
|
||||
checkoutPath: string,
|
||||
commitBackend: CommitBackend,
|
||||
noBuild: boolean,
|
||||
packagePath: string,
|
||||
scratchPath: string,
|
||||
}> */,
|
||||
) {
|
||||
process.stdout.write('Creating a sync diff\n');
|
||||
const {packagePath, baseGitRevision} = diffBaseInfo;
|
||||
process.stdout.write('Committing updated files\n');
|
||||
const baseGitRevisionShort = baseGitRevision.slice(0, 7);
|
||||
const {gitRevision: newGitRevision, isLocalCheckout} =
|
||||
await readBuildInfo(packagePath);
|
||||
const newGitRevision = await readGitRevision(packagePath);
|
||||
const newGitRevisionShort = newGitRevision.slice(0, 7);
|
||||
|
||||
// Generate the changelog table
|
||||
@@ -604,39 +577,30 @@ async function createSyncDiff(
|
||||
newGitRevision,
|
||||
);
|
||||
|
||||
const commitMessage = [
|
||||
(isLocalCheckout || noBuild ? 'DO NOT LAND ' : '') +
|
||||
`[RN] Update debugger-frontend from ${baseGitRevisionShort}...${newGitRevisionShort}`,
|
||||
'',
|
||||
'Summary:',
|
||||
`Changelog: [Internal] - Update \`@react-native/debugger-frontend\` from ${baseGitRevisionShort}...${newGitRevisionShort}`,
|
||||
'',
|
||||
`Resyncs \`@react-native/debugger-frontend\` from GitHub - see \`rn-chrome-devtools-frontend\` [changelog](${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}).`,
|
||||
'',
|
||||
changelogTable,
|
||||
'',
|
||||
'Test Plan: CI',
|
||||
'',
|
||||
'Reviewers: #rn-debugging',
|
||||
'',
|
||||
'Tags: msdkland[metro]',
|
||||
'',
|
||||
].join('\n');
|
||||
const revisionRange = `${baseGitRevisionShort}...${newGitRevisionShort}`;
|
||||
const title =
|
||||
(noBuild ? 'DO NOT LAND ' : '') +
|
||||
`[RN] Update debugger-frontend from ${revisionRange}`;
|
||||
const changelogEntry = `[Internal] - Update \`@react-native/debugger-frontend\` from ${revisionRange}`;
|
||||
const compareUrl = `${DEVTOOLS_FRONTEND_REPO_URL}/compare/${baseGitRevision}...${newGitRevision}`;
|
||||
const summary =
|
||||
'Resyncs `@react-native/debugger-frontend` from GitHub - see ' +
|
||||
'`rn-chrome-devtools-frontend` ' +
|
||||
`[changelog](${compareUrl}).`;
|
||||
|
||||
const commitMessage =
|
||||
commitBackend
|
||||
.messageBlocks({title, summary, changelogTable, changelogEntry})
|
||||
.filter(block => block !== '')
|
||||
.join('\n\n') + '\n';
|
||||
|
||||
const commitMessageFile = path.join(scratchPath, 'commit-msg');
|
||||
await fs.writeFile(commitMessageFile, commitMessage);
|
||||
await spawnSafe(
|
||||
'hg',
|
||||
['commit', packagePath, '--addremove', '-l', commitMessageFile],
|
||||
{cwd: packagePath},
|
||||
);
|
||||
await spawnSafe('jf', ['submit', '--draft'], {
|
||||
cwd: packagePath,
|
||||
});
|
||||
if (noBuild) {
|
||||
await spawnSafe('jf', ['action', '--abandon'], {
|
||||
cwd: packagePath,
|
||||
});
|
||||
for (const [cmd, args] of commitBackend.commit(
|
||||
packagePath,
|
||||
commitMessageFile,
|
||||
)) {
|
||||
await spawnSafe(cmd, args, {cwd: packagePath});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user