| #!/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("\nSUCCESS! Your Chromium worktree is ready at:") | |
| print(f"{variant_src}") | |
| print("\nShell Integration Tips:") | |
| alias_name = variant_name.replace(f"{parent_dir.name}_", "") | |
| print("Add this to your ~/.zshrc for quick jumping:") | |
| print(f" function {alias_name}() {{ cd {variant_src} }}") | |
| print("\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() |