Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions bash/update_precommit_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env bash

set -euo pipefail

ORG="brainglobe"
BRANCH_NAME="update/standardize-precommit"
PR_TITLE="ci: standardize pre-commit config"
PR_BODY="Standardizes the pre-commit configuration across BrainGlobe repositories while preserving repo-specific hooks and settings."
WORK_DIR="$(pwd)/.brainglobe_precommit_work"
DRY_RUN=false
ONLY_REPO=""

REPOS=(
"brainglobe-atlasapi"
"brainglobe-ccf-translator"
"brainglobe-heatmap"
"brainrender-napari"
"brainrender"
"cellfinder"
"morphapi"
"brainglobe-registeration"
)

# parsing args
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true ; shift ;;
--repo) ONLY_REPO="$2" ; shift 2 ;;
*) echo "Unknown argument: $1" ; exit 1 ;;
esac
done

log() { echo "[INFO] $*" ; }
warn() { echo "[WARN] $*" >&2 ; }
die() { echo "[ERROR] $*" >&2 ; exit 1 ; }

require_cmd() {
command -v "$1" &>/dev/null || die "'$1' is required but not found in PATH."
}

require_cmd git
require_cmd gh
require_cmd python3

mkdir -p "$WORK_DIR"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_HELPER="$SCRIPT_DIR/../python/precommit_updater.py"
[[ -f "$PYTHON_HELPER" ]] || die "Cannot find Python helper: $PYTHON_HELPER"

process_repo() {
local repo="$1"
local repo_dir="$WORK_DIR/$repo"

log "Processing: $ORG/$repo"

# 1. Clone/update
if [[ -d "$repo_dir/.git" ]]; then
log "Repo exists locally, resetting to origin/main…"
git -C "$repo_dir" fetch origin --prune
git -C "$repo_dir" checkout main 2>/dev/null \
|| git -C "$repo_dir" checkout master 2>/dev/null \
|| { warn "Cannot find main/master for $repo - skipping."; return 0; }
git -C "$repo_dir" reset --hard origin/HEAD
else
log "Cloning $ORG/$repo…"
if ! gh repo clone "$ORG/$repo" "$repo_dir" -- --depth=1 2>&1; then
warn "Could not clone $ORG/$repo - skipping."
return 0
fi
fi

# 2. Find pre-commit config
local config="$repo_dir/.pre-commit-config.yaml"
if [[ ! -f "$config" ]]; then
warn "No .pre-commit-config.yaml found in $repo - skipping."
return 0
fi

log "Config: $config"

# 3. Check if branch already exists remotely
if git -C "$repo_dir" ls-remote --exit-code --heads origin "$BRANCH_NAME" &>/dev/null; then
warn "Branch '$BRANCH_NAME' already exists on $repo - skipping."
return 0
fi

# 4. Create branch
git -C "$repo_dir" checkout -b "$BRANCH_NAME"

# 5. Run python helper
log "Running pre-commit standardizer…"
if ! python3 "$PYTHON_HELPER" "$config"; then
warn "Standardizer failed for $repo - skipping."
git -C "$repo_dir" checkout -f main 2>/dev/null || true
git -C "$repo_dir" branch -D "$BRANCH_NAME" 2>/dev/null || true
return 0
fi

# 6. Check for actual diff
if git -C "$repo_dir" diff --quiet; then
log "No changes needed for $repo - skipping PR."
git -C "$repo_dir" checkout -f main 2>/dev/null || true
git -C "$repo_dir" branch -D "$BRANCH_NAME" 2>/dev/null || true
return 0
fi

log "Diff preview:"
git -C "$repo_dir" diff -- "$(basename "$config")" || true

if [[ "$DRY_RUN" == true ]]; then
log "[DRY RUN] Would commit, push, and open PR for $repo."
return 0
fi

# 7. Commit
git -C "$repo_dir" add "$(basename "$config")"
git -C "$repo_dir" commit -m "$PR_TITLE"

# 8. Push
log "Pushing branch $BRANCH_NAME to origin…"
git -C "$repo_dir" push origin "$BRANCH_NAME"

# 9. Open PR
log "Opening PR…"
gh pr create \
--repo "$ORG/$repo" \
--head "$BRANCH_NAME" \
--base main \
--title "$PR_TITLE" \
--body "$PR_BODY" \
--label "maintenance" \
&& log "PR opened for $repo." \
|| warn "PR creation failed for $repo (maybe label doesn't exist - try without --label)."

log "Done: $repo"
}

# Main
if [[ -n "$ONLY_REPO" ]]; then
process_repo "$ONLY_REPO"
else
for repo in "${REPOS[@]}"; do
process_repo "$repo" || warn "Unhandled error for $repo - continuing."
done
fi

log "All repos processed."
log "Work directory: $WORK_DIR"
63 changes: 63 additions & 0 deletions python/precommit_updater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
Standardize a single .pre-commit-config.yaml in place:
- bump pre-commit-hooks / ruff-pre-commit / mypy / check-manifest / codespell revs
- drop the black hook entirely
- make sure ruff-pre-commit has both `ruff` and `ruff-format`, each with
args: [--config=pyproject.toml]
Everything else (napari-plugin-checks, exclude:, ci:, extra mypy deps, etc.)
is left untouched.
"""
import sys
from ruamel.yaml import YAML

yaml = YAML()
yaml.preserve_quotes = True
yaml.width = 4096
yaml.indent(mapping=4, sequence=4, offset=2)

REVS = {
"https://github.com/pre-commit/pre-commit-hooks": "v6.0.0",
"https://github.com/astral-sh/ruff-pre-commit": "v0.16.1",
"https://github.com/pre-commit/mirrors-mypy": "v1.13.0",
"https://github.com/mgedmin/check-manifest": "0.50",
"https://github.com/codespell-project/codespell": "v2.4.3",
}
BLACK_REPO = "https://github.com/psf/black-pre-commit-mirror"
RUFF_REPO = "https://github.com/astral-sh/ruff-pre-commit"
RUFF_ARGS = ["--config=pyproject.toml"]


def fix_ruff_hooks(entry):
ids = {h["id"] for h in entry["hooks"]}
for h in entry["hooks"]:
h["args"] = list(RUFF_ARGS)
if "ruff-format" not in ids:
fmt = type(entry["hooks"][0])()
fmt["id"] = "ruff-format"
fmt["args"] = list(RUFF_ARGS)
entry["hooks"].append(fmt)


def standardize(path):
with open(path) as f:
data = yaml.load(f)

new_repos = []
for entry in data["repos"]:
url = entry["repo"]
if url == BLACK_REPO:
continue # drop black entirely
if url in REVS:
entry["rev"] = REVS[url]
if url == RUFF_REPO:
fix_ruff_hooks(entry)
new_repos.append(entry)
data["repos"] = new_repos

with open(path, "w") as f:
yaml.dump(data, f)


standardize(sys.argv[1] if len(sys.argv) > 1 else ".pre-commit-config.yaml")
print(f"standardized {sys.argv[1] if len(sys.argv) > 1 else '.pre-commit-config.yaml'}")