blob: 7a351d488434833f72b5bf27a8ff88ca239a5c9d [file]
#!/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()