-
Notifications
You must be signed in to change notification settings - Fork 82
feat: add e worktree for additional gclient working directories
#836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| import * as childProcess from 'node:child_process'; | ||
| import * as fs from 'node:fs'; | ||
| import * as os from 'node:os'; | ||
| import * as path from 'node:path'; | ||
|
|
||
| import { program } from 'commander'; | ||
|
|
||
| import * as evmConfig from './evm-config.js'; | ||
| import { color, fatal } from './utils/logging.js'; | ||
| import { resolvePath, deleteDir } from './utils/paths.js'; | ||
| import * as depot from './utils/depot-tools.js'; | ||
| import type { SanitizedConfig } from './types.js'; | ||
|
|
||
| interface AddOptions { | ||
| source?: string; | ||
| out?: string; | ||
| sync: boolean; | ||
| force: boolean; | ||
| } | ||
|
|
||
| interface CleanOptions { | ||
| yes: boolean; | ||
| } | ||
|
|
||
| function isDerivedWorktree(root: string): boolean { | ||
| // gclient-new-workdir.py symlinks .gclient to the source checkout; a primary | ||
| // checkout always has a regular file here, so this alone distinguishes them. | ||
| const gclient = path.join(root, '.gclient'); | ||
| try { | ||
| return fs.lstatSync(gclient).isSymbolicLink(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| program | ||
| .name('e worktree') | ||
| .description( | ||
| 'Manage additional gclient working directories that share git objects with an existing checkout', | ||
| ); | ||
|
|
||
| program | ||
| .command('add') | ||
| .argument('<name>', 'Name for the new build configuration') | ||
| .argument('<new_workdir>', 'Directory for the new working tree (must not exist)') | ||
| .description('Create a new worktree and matching build config cloned from an existing one') | ||
| .option('--source <config>', 'Existing build config to clone from (default: current)') | ||
| .option( | ||
| '-o, --out <name>', | ||
| 'Built files will be placed in $root/src/out/$out (default: same as source)', | ||
| ) | ||
| .option('--no-sync', 'Skip running `e sync` after creating the worktree') | ||
| .option('-f, --force', 'Overwrite an existing build config of the same name', false) | ||
| .action((name: string, newWorkdir: string, options: AddOptions) => { | ||
| try { | ||
| if (os.platform() === 'win32') { | ||
| fatal( | ||
| '`e worktree` is not supported on Windows (gclient-new-workdir.py requires symlinks)', | ||
| ); | ||
| } | ||
|
|
||
| const sourceName = options.source ?? evmConfig.currentName(); | ||
| const sourceConfig = evmConfig.fetchByName(sourceName); | ||
| const sourceRoot = sourceConfig.root; | ||
|
|
||
| if (!fs.existsSync(path.join(sourceRoot, '.gclient'))) { | ||
| fatal( | ||
| `Source root ${color.path(sourceRoot)} has no .gclient file. ` + | ||
| `Run ${color.cmd('e sync')} in config ${color.config(sourceName)} first.`, | ||
| ); | ||
| } | ||
|
|
||
| const targetRoot = resolvePath(newWorkdir); | ||
| if (fs.existsSync(targetRoot)) { | ||
| fatal(`Target directory ${color.path(targetRoot)} already exists.`); | ||
| } | ||
|
|
||
| const filename = evmConfig.pathOf(name); | ||
| if (!options.force && fs.existsSync(filename)) { | ||
| fatal( | ||
| `Build config ${color.config(name)} already exists (${color.path(filename)}). ` + | ||
| `Use --force to overwrite.`, | ||
| ); | ||
| } | ||
|
|
||
| depot.ensure(); | ||
|
|
||
| console.log( | ||
| `Creating worktree from ${color.config(sourceName)} (${color.path(sourceRoot)}) ` + | ||
| `→ ${color.path(targetRoot)}`, | ||
| ); | ||
| const script = path.join(depot.path, 'gclient-new-workdir.py'); | ||
| depot.spawnSync( | ||
| sourceConfig, | ||
| 'python3', | ||
| [script, sourceRoot, targetRoot], | ||
| { stdio: 'inherit' }, | ||
| 'gclient-new-workdir.py failed', | ||
| ); | ||
|
|
||
| const newConfig: SanitizedConfig = structuredClone(sourceConfig); | ||
| newConfig.root = targetRoot; | ||
| newConfig.gen.out = options.out ?? sourceConfig.gen.out; | ||
| newConfig.env.CHROMIUM_BUILDTOOLS_PATH = path.join(targetRoot, 'src', 'buildtools'); | ||
|
|
||
| evmConfig.save(name, newConfig); | ||
| console.log(`New build config ${color.config(name)} created in ${color.path(filename)}`); | ||
|
|
||
| evmConfig.setCurrent(name); | ||
| console.log(`Now using config ${color.config(name)}`); | ||
|
|
||
| if (options.sync) { | ||
| console.log( | ||
| `Running ${color.cmd('e sync')} to fetch toolchains and apply patches in the new worktree...`, | ||
| ); | ||
| const e = path.resolve(import.meta.dirname, 'e'); | ||
| const opts: childProcess.ExecFileSyncOptions = { stdio: 'inherit' }; | ||
| childProcess.execFileSync(process.execPath, [e, 'sync'], opts); | ||
| } else { | ||
| console.log( | ||
| `${color.info} Skipped sync. Run ${color.cmd('e sync')} in this config before building.`, | ||
| ); | ||
| } | ||
|
|
||
| console.log(`${color.success} Worktree ready at ${color.path(targetRoot)}`); | ||
| console.log( | ||
| `${color.info} When finished, remove with ${color.cmd(`e worktree clean ${name} --yes`)}`, | ||
| ); | ||
| } catch (e) { | ||
| fatal(e); | ||
| } | ||
| }); | ||
|
|
||
| program | ||
| .command('clean') | ||
| .argument('<name>', 'Build config whose worktree should be deleted') | ||
| .description('Delete a worktree directory and its build config') | ||
| .option('--yes', 'Confirm deletion (required; this removes the entire worktree directory)', false) | ||
| .action((name: string, options: CleanOptions) => { | ||
| try { | ||
| const config = evmConfig.fetchByName(name); | ||
| const root = config.root; | ||
|
|
||
| if (!isDerivedWorktree(root)) { | ||
| fatal( | ||
| `${color.path(root)} does not look like a worktree created by ${color.cmd('e worktree add')} ` + | ||
| `(.gclient must be a symlink). Refusing to delete.`, | ||
| ); | ||
| } | ||
|
|
||
| let active: string | null; | ||
| try { | ||
| active = evmConfig.currentName(); | ||
| } catch { | ||
| active = null; | ||
| } | ||
| if (active === name) { | ||
| fatal( | ||
| `Config ${color.config(name)} is currently in use. ` + | ||
| `Switch to another config with ${color.cmd('e use <other>')} first.`, | ||
| ); | ||
| } | ||
|
|
||
| if (!options.yes) { | ||
| fatal( | ||
| `This will delete ${color.path(root)} and the ${color.config(name)} config. ` + | ||
| `Re-run with ${color.cmd('--yes')} to confirm.`, | ||
| ); | ||
| } | ||
|
|
||
| console.log(`Deleting ${color.path(root)}...`); | ||
| deleteDir(root); | ||
|
|
||
| evmConfig.remove(name); | ||
| console.log(`Removed config ${color.config(name)}`); | ||
|
|
||
| console.log(`${color.success} Worktree ${color.path(root)} removed`); | ||
| } catch (e) { | ||
| fatal(e); | ||
| } | ||
| }); | ||
|
|
||
| program.addHelpText( | ||
| 'after', | ||
| ` | ||
| Examples: | ||
| $ e worktree add testing2 ~/src/electron2 | ||
| $ e worktree add asan ~/src/electron-asan --source testing -o Asan --no-sync | ||
| $ e worktree clean testing2 --yes`, | ||
| ); | ||
|
|
||
| program.parse(process.argv); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If somebody runs
git gcin the worktree, the.git/objectssymlink can get replaced with a real directory, so this check would fail and they'd be unable to clean up via e worktree clean. Maybe add a--forceescape hatch on clean?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think
git gcactually replaces the symlink (it writes through it intopack/), but the wider point is fair. Rather than a--forcethat could take out a real checkout I've relaxed the check to just.gclientbeing a symlink — that's the one thinggclient-new-workdir.pyalways leaves and git never touches.