| #!/usr/bin/env python3 |
| |
| # Copyright 2026 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| Chromium Worktree Automation Tool |
| Automates the creation of git worktrees for Chromium checkouts. |
| """ |
| |
| import argparse |
| import json |
| import os |
| import random |
| import shutil |
| import subprocess |
| import sys |
| from pathlib import Path |
| |
| # Common VSCode Title Bar Tint colors from using-gitworktree.md |
| VSCODE_COLOR_PRESETS = { |
| "red": { |
| "titleBar.activeBackground": "#3a1414", |
| "titleBar.activeForeground": "#d3adad", |
| "titleBar.inactiveBackground": "#401a1a", |
| "titleBar.inactiveForeground": "#ab8585" |
| }, |
| "green": { |
| "titleBar.activeBackground": "#143a14", |
| "titleBar.activeForeground": "#add3ad", |
| "titleBar.inactiveBackground": "#1a401a", |
| "titleBar.inactiveForeground": "#85ab85" |
| }, |
| "blue": { |
| "titleBar.activeBackground": "#14143a", |
| "titleBar.activeForeground": "#adc3d3", |
| "titleBar.inactiveBackground": "#1a1a40", |
| "titleBar.inactiveForeground": "#8585ab" |
| } |
| } |
| |
| |
| def run_cmd(cmd, cwd=None, capture=False, check=True): |
| """Utility to run shell commands.""" |
| print(f"Executing: {' '.join(cmd)} in {cwd or os.getcwd()}") |
| try: |
| result = subprocess.run(cmd, |
| cwd=cwd, |
| check=True, |
| text=True, |
| capture_output=capture) |
| return result.stdout.strip() if capture else None |
| except subprocess.CalledProcessError as e: |
| if check: |
| print(f"Error executing command: {e}") |
| if e.stderr: |
| print(f"Stderr: {e.stderr}") |
| sys.exit(1) |
| return None |
| |
| |
| def validate_parent_repo(parent_path): |
| """Ensures the parent path is a valid gclient checkout with a src repo.""" |
| target = Path(parent_path).resolve() |
| |
| if not target.is_dir(): |
| print(f"Error: Parent path {target} is not a directory.") |
| sys.exit(1) |
| |
| # If the user pointed to 'src', the gclient root is one level up |
| if target.name == "src" and (target.parent / ".gclient").exists(): |
| parent_dir = target.parent |
| src_dir = target |
| else: |
| parent_dir = target |
| src_dir = target / "src" |
| |
| gclient_config = parent_dir / ".gclient" |
| |
| if not gclient_config.exists(): |
| print(f"Error: .gclient file not found in {parent_dir}") |
| sys.exit(1) |
| |
| if not (src_dir / ".git").exists(): |
| print(f"Error: Git repository not found in {src_dir}") |
| sys.exit(1) |
| |
| return parent_dir, gclient_config, src_dir |
| |
| |
| def check_cache_dir(gclient_config): |
| """Check if cache_dir is defined in .gclient.""" |
| content = gclient_config.read_text() |
| if "cache_dir" not in content: |
| print("\nWARNING: 'cache_dir' not found in your .gclient file.") |
| print("Without cache_dir, gclient sync will download ~20GB of submodules.") |
| print("It is highly recommended to add it to your main .gclient first.\n") |
| return False |
| return True |
| |
| |
| def create_worktree(parent_src, variant_path, branch_name, base_branch=None): |
| """Runs git worktree add.""" |
| print(f"Creating git worktree at {variant_path}...") |
| cmd = ["git", "worktree", "add", str(variant_path), "-b", branch_name] |
| if base_branch: |
| cmd.append(base_branch) |
| run_cmd(cmd, cwd=parent_src) |
| |
| if base_branch: |
| print(f"Setting upstream for '{branch_name}' to '{base_branch}'...") |
| run_cmd(["git", "branch", "--set-upstream-to", base_branch, branch_name], |
| cwd=variant_path) |
| |
| |
| def setup_vscode_color(variant_src, color_name): |
| """Configures VSCode title bar colors in settings.json.""" |
| color_config = VSCODE_COLOR_PRESETS.get(color_name.lower()) |
| if not color_config: |
| print( |
| f"Warning: Color preset '{color_name}' not found. Skipping VSCode coloring." |
| ) |
| return |
| |
| vscode_dir = variant_src / ".vscode" |
| vscode_dir.mkdir(exist_ok=True) |
| settings_file = vscode_dir / "settings.json" |
| |
| settings = {} |
| if settings_file.exists(): |
| try: |
| settings = json.loads(settings_file.read_text()) |
| except Exception: |
| pass |
| |
| settings["workbench.colorCustomizations"] = color_config |
| settings_file.write_text(json.dumps(settings, indent=2)) |
| print(f"Configured VSCode coloring to '{color_name}' in {settings_file}") |
| |
| |
| def get_next_variant_name(parent_dir): |
| """Suggests an incremental name (e.g. bling_1, bling_2).""" |
| for i in range(1, 31): |
| name = f"{parent_dir.name}_{i}" |
| if not (parent_dir.parent / name).exists(): |
| return name |
| |
| # Fallback to a 5-digit random number to avoid collision |
| random_id = random.randint(10000, 99999) |
| return f"{parent_dir.name}_{random_id}" |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Create a Chromium git worktree.") |
| parser.add_argument( |
| "parent_repo", |
| help="Path to the parent repository (containing .gclient and src/)") |
| parser.add_argument("--name", help="Name for the new worktree (folder name)") |
| parser.add_argument("--branch", |
| help="Specific branch name to create (defaults to name)") |
| parser.add_argument("--base", |
| help="Base branch to fork from (defaults to HEAD)") |
| parser.add_argument( |
| "--internal-base", |
| help="Base branch to fork from in internal (defaults to pinned commit)") |
| parser.add_argument( |
| "--light", |
| action="store_true", |
| help="Light worktree: skip gclient sync (saves ~20GB, good for agents)") |
| parser.add_argument("--color", |
| help="VSCode title bar color (red, green, blue)") |
| |
| args = parser.parse_args() |
| |
| parent_dir, gclient_config, parent_src = validate_parent_repo( |
| args.parent_repo) |
| has_cache_dir = check_cache_dir(gclient_config) |
| |
| # Determine new path |
| if args.name: |
| variant_name = args.name |
| else: |
| variant_name = get_next_variant_name(parent_dir) |
| print(f"No name provided, using suggested name: {variant_name}") |
| |
| variant_root = parent_dir.parent / variant_name |
| variant_src = variant_root / "src" |
| |
| if variant_root.exists(): |
| print( |
| f"Error: Target path {variant_root} already exists. Choose a different name." |
| ) |
| sys.exit(1) |
| |
| branch_name = args.branch if args.branch else variant_name |
| |
| # 1. Create the root directory |
| variant_root.mkdir(parents=True) |
| |
| # 2. Add worktree |
| create_worktree(parent_src, variant_src, branch_name, args.base) |
| |
| # 3. Copy .gclient |
| print(f"Copying .gclient to {variant_root}") |
| shutil.copy2(gclient_config, variant_root / ".gclient") |
| |
| # 4. Configure VSCode coloring |
| if args.color: |
| setup_vscode_color(variant_src, args.color) |
| |
| # 5. gclient sync (skip if light) |
| if not args.light: |
| print( |
| "\nStarting gclient sync... this may take a few minutes if cache_dir is enabled." |
| ) |
| run_cmd(["gclient", "sync"], cwd=variant_src) |
| else: |
| print( |
| "\nSkipping gclient sync (--light mode enabled). Tree is approx. 4GB.") |
| |
| # 6. Fork internal repo if requested |
| if args.internal_base: |
| internal_src = variant_src / "internal" |
| parent_internal_src = parent_src / "internal" |
| if internal_src.exists(): |
| # Check if the branch exists locally |
| has_branch = run_cmd( |
| ["git", "show-ref", "--verify", f"refs/heads/{args.internal_base}"], |
| cwd=internal_src, |
| capture=True, |
| check=False) is not None |
| |
| if not has_branch: |
| print( |
| f"Base branch '{args.internal_base}' not found in {internal_src}. Attempting to fetch from parent..." |
| ) |
| run_cmd(["git", "remote", "add", "parent", |
| str(parent_internal_src)], |
| cwd=internal_src) |
| run_cmd(["git", "fetch", "parent", args.internal_base], |
| cwd=internal_src) |
| base_ref = "FETCH_HEAD" |
| upstream_ref = f"parent/{args.internal_base}" |
| else: |
| base_ref = args.internal_base |
| upstream_ref = args.internal_base |
| |
| print(f"Forking branch '{branch_name}' in internal from '{base_ref}'") |
| run_cmd(["git", "checkout", "-b", branch_name, base_ref], |
| cwd=internal_src) |
| run_cmd(["git", "branch", "--set-upstream-to", upstream_ref, branch_name], |
| cwd=internal_src) |
| else: |
| print( |
| f"Warning: internal not found at {internal_src}. Skipping internal fork." |
| ) |
| |
| print(f"\nSUCCESS! Your Chromium worktree is ready at:") |
| print(f"{variant_src}") |
| |
| print(f"\nShell Integration Tips:") |
| alias_name = variant_name.replace(f"{parent_dir.name}_", "") |
| print(f"Add this to your ~/.zshrc for quick jumping:") |
| print(f" function {alias_name}() {{ cd {variant_src} }}") |
| print(f"\nYou can now cd into it and start working.") |
| |
| if not has_cache_dir: |
| print("\n💡 Pro Tip: Speed up `gclient sync` for worktrees!") |
| print( |
| "Configure a single universal Git object cache directory to perform \"shared clones\"." |
| ) |
| print("Add `cache_dir` to your `.gclient` file:") |
| print("```py") |
| print("solutions = [") |
| print(" {") |
| print(" \"name\": \"src\",") |
| print( |
| " \"url\": \"https://chromium.googlesource.com/chromium/src.git\",") |
| print(" \"managed\": False,") |
| print(" \"custom_deps\": {},") |
| print(" \"custom_vars\": {},") |
| print(" },") |
| print("]") |
| print("cache_dir = \"~/dev/git_cache\" # Add this line") |
| print("```\n") |
| |
| |
| if __name__ == "__main__": |
| main() |