Skip to content
Merged
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
Empty file removed .deps-installed
Empty file.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ xnotes/*
.env
.envrc
.venv
.deps-installed
.vscode/settings.json

# mypy
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ Do not attribute work to Claude, Copilot or any other AI agent anywhere in this

This rule takes precedence over any default tool behaviour that would add attribution.

## Worktrees

Create worktrees with `scripts/new-worktree.sh`, never with `git worktree add` or an agent's built-in worktree tool:

```bash
./scripts/new-worktree.sh <name> [<branch>] # <branch> defaults to feat/<name>
```

It creates `../span-panel-api-<name>` beside the primary checkout from a freshly fetched `origin/main`, prepares it with `setup-hooks.sh`, and links `.env`. That location is the one the integration's developer notes expect when installing a library
worktree into its environment. [DEVELOPMENT.md](DEVELOPMENT.md#worktrees) has the details.

- **Never share a virtual environment between checkouts.** This repository installs its own distributions in editable mode, each recording one checkout's source path, so a shared `.venv` imports whichever checkout synced last and tests pass against the
wrong code without an error.
- **Install the pre-commit hook from the primary checkout.** Hooks are shared by every worktree and record the interpreter that installed them. `new-worktree.sh` installs the hook from the primary checkout when it is missing or bound elsewhere, and refuses
to create the worktree if that fails. `setup-hooks.sh` in a worktree checks the shared hook rather than rebinding it to an environment that is deleted with the worktree.

## The rule every field follows

A value the panel did not publish is `None` (or unknown), never a constant. A `0.0`, `False`, `""` or empty collection in a snapshot field is a reading the panel published, or an answer the library can derive from what the panel declared, and the docstring
Expand Down
15 changes: 14 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,20 @@ To install pre-commit hooks:
./setup-hooks.sh
```

This installs dependencies (if needed) and configures git pre-commit hooks.
This syncs dependencies and, in the primary checkout, installs the git pre-commit hook. Run from a worktree, it syncs that worktree's own environment and checks the shared hook instead of reinstalling it: hooks are shared by every worktree and record the
interpreter that installed them, so a hook installed from a worktree stops working when that worktree is removed. `./setup-hooks.sh --check-hook` only checks the shared hook, and exits non-zero when it is missing or not bound to the primary checkout.

## Worktrees

Create worktrees with the script rather than `git worktree add`:

```bash
./scripts/new-worktree.sh <name> [<branch>] # <branch> defaults to feat/<name>
```

It fetches `origin`, fast-forwards `main` where that is safe, installs the shared pre-commit hook from the primary checkout if it is missing or bound elsewhere, creates `../span-panel-api-<name>` on a branch that does not track a remote, links `.env` from
the primary checkout, and runs `setup-hooks.sh` inside it. The worktree gets a virtual environment of its own rather than a link to the primary checkout's: editable installs record one checkout's source path, so a shared environment would import whichever
checkout synced last.

## Workspace layout

Expand Down
121 changes: 121 additions & 0 deletions scripts/new-worktree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash
#
# Create a worktree for a branch of work beside the primary checkout, and prepare
# it with setup-hooks.sh.
#
# Usage: scripts/new-worktree.sh <name> [<branch>]
#
# <name> The worktree is created at ../<repository>-<name>, beside the primary
# checkout: the layout the integration's developer notes expect when
# installing a library worktree into its environment.
# <branch> The branch to create. Defaults to feat/<name>.
#
# The branch starts from origin/main as just fetched and does not track it, so a
# bare `git push` can never land on main. Local main is fast-forwarded as well where
# that is safe, and the script refuses when local main holds commits origin/main
# lacks, because branching from origin/main would silently leave them out.
#
# The worktree gets a virtual environment of its own rather than a link to the
# primary checkout's. This repository installs its own distributions in editable
# mode, and each records the path of one checkout's sources, so a shared
# environment imports whichever checkout synced last and tests pass against the
# wrong code without an error. uv clones packages from its cache, so the second
# environment costs little. Local configuration that is not bound to a path, `.env`,
# is linked instead.
#
# The shared pre-commit hook is installed from the primary checkout first when it is
# missing or bound elsewhere, so no worktree starts out committing unchecked.

set -euo pipefail

usage() {
echo "Usage: $0 <name> [<branch>]" >&2
exit 2
}

[[ $# -ge 1 && $# -le 2 ]] || usage
name=$1
if [[ ! "$name" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Worktree name '$name' may use only letters, digits, dots, underscores and hyphens." >&2
exit 2
fi
branch=${2:-feat/$name}

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
checkout=$(cd "$script_dir/.." && pwd)
common_dir=$(git -C "$checkout" rev-parse --path-format=absolute --git-common-dir)
primary_root=$(dirname "$common_dir")
target="$(dirname "$primary_root")/$(basename "$primary_root")-$name"

if [[ -e "$target" ]]; then
echo "$target already exists." >&2
exit 1
fi
if git -C "$primary_root" show-ref --verify --quiet "refs/heads/$branch"; then
echo "Branch $branch already exists." >&2
exit 1
fi

echo "Fetching origin..."
git -C "$primary_root" fetch origin

if git -C "$primary_root" show-ref --verify --quiet refs/heads/main; then
ahead=$(git -C "$primary_root" rev-list --count origin/main..main)
if ((ahead > 0)); then
echo "Local main has $ahead commit(s) that origin/main does not. Reconcile main first;" >&2
echo "branching from origin/main would silently leave them out." >&2
exit 1
fi

# Where main is checked out, move it only if that checkout is clean.
main_checkout=$(git -C "$primary_root" worktree list --porcelain |
awk '/^worktree /{wt = substr($0, 10)} /^branch refs\/heads\/main$/{print wt}')
if [[ -z "$main_checkout" ]]; then
# update-ref rather than `branch --force`: that would also rewrite main's
# upstream to origin/main, silently re-pointing a main that tracks another
# remote. The old value guards against main moving since it was checked.
git -C "$primary_root" update-ref -m "new-worktree.sh: fast-forward main to origin/main" \
refs/heads/main "$(git -C "$primary_root" rev-parse refs/remotes/origin/main)" \
"$(git -C "$primary_root" rev-parse refs/heads/main)"
elif [[ -n "$(git -C "$main_checkout" status --porcelain --untracked-files=no)" ]]; then
echo "main is checked out with uncommitted changes at $main_checkout; leaving it as it is."
elif ! git -C "$main_checkout" merge --ff-only --quiet origin/main; then
# Untracked files are not counted as changes above, because a fast-forward
# refuses to overwrite one; this is where that refusal lands.
echo "Could not fast-forward main at $main_checkout; leaving it as it is."
fi
fi

# Every worktree commits through the one hook in the common directory. Missing, its
# commits skip every check without a word; bound to an environment that is gone,
# they fail. The primary checkout is the one place it can be installed to last, so
# install it there before the worktree exists, and refuse if that does not take.
if ! (cd "$checkout" && bash ./setup-hooks.sh --check-hook >/dev/null); then
echo "Installing the shared pre-commit hook from the primary checkout..."
if ! (cd "$primary_root" && ./setup-hooks.sh); then
echo "Could not install the pre-commit hook from $primary_root; not creating the worktree." >&2
exit 1
fi
if ! (cd "$checkout" && bash ./setup-hooks.sh --check-hook); then
echo "The pre-commit hook is still not bound to $primary_root; not creating the worktree." >&2
exit 1
fi
fi

echo "Creating $target on $branch from origin/main..."
git -C "$primary_root" worktree add --no-track -b "$branch" "$target" origin/main

if [[ -f "$primary_root/.env" && ! -e "$target/.env" ]]; then
ln -s "$primary_root/.env" "$target/.env"
echo "Linked .env from the primary checkout."
fi

# The setup script beside this one rather than the target's copy. The branch starts
# from origin/main, which can predate the worktree-aware setup, and running an older
# copy there would rebind the shared hook to the new worktree.
echo "Preparing the worktree..."
(cd "$target" && bash "$checkout/setup-hooks.sh")

echo
echo "Worktree ready: $target"
echo "Branch: $branch, from $(git -C "$target" rev-parse --short HEAD), not tracking a remote"
123 changes: 103 additions & 20 deletions setup-hooks.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,114 @@
#!/bin/bash
#
# Prepare this checkout for development: install dependencies and make sure the
# git pre-commit hook is in place.
#
# Usage: ./setup-hooks.sh [--update | --check-hook]
#
# --update Upgrade the lockfile before syncing.
# --check-hook Only check the shared pre-commit hook, and exit non-zero when it
# is missing or not bound to the primary checkout.
#
# Safe to run from the primary checkout and from a linked worktree, which differ
# in the one respect that matters here. Git hooks live in the repository's common
# directory and every worktree shares them, and `pre-commit install` writes the
# interpreter that runs it into the hook. Installing from a worktree therefore
# binds every checkout's commits to that worktree's virtual environment -- which
# is deleted with the worktree, after which every commit fails with "`pre-commit`
# not found". So only the primary checkout installs the hook. A worktree checks
# that the shared hook is bound to the primary checkout and says how to fix it if
# not, and leaves it alone either way.

# Check if we should force update (pass --update flag)
FORCE_UPDATE=false
if [[ "$1" == "--update" ]]; then
FORCE_UPDATE=true
fi
usage() {
echo "Usage: $0 [--update | --check-hook]" >&2
exit 2
}

mode=setup
case "${1:-}" in
"") ;;
--update) mode=update ;;
--check-hook) mode=check-hook ;;
*) usage ;;
esac
[[ $# -le 1 ]] || usage

git_dir=$(git rev-parse --absolute-git-dir) || exit 1
common_dir=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1
primary_root=$(dirname "$common_dir")

# Ensure dependencies are installed first
if [[ ! -f ".deps-installed" ]] || [[ "pyproject.toml" -nt ".deps-installed" ]] || [[ "uv.lock" -nt ".deps-installed" ]] || [[ "$FORCE_UPDATE" == "true" ]]; then
echo "Installing/updating dependencies..."
# Whether the shared hook exists and runs with the primary checkout's environment.
# Silent when it does; otherwise says what is wrong and how to fix it.
check_shared_hook() {
local hook install_python

if [[ "$FORCE_UPDATE" == "true" ]]; then
echo "Forcing update to latest versions..."
uv lock --upgrade
uv sync
else
uv sync
hook=$(git rev-parse --path-format=absolute --git-path hooks/pre-commit)
if [[ ! -f "$hook" ]]; then
echo "No pre-commit hook is installed for this repository yet, so commits skip every"
echo "check. Install it from the primary checkout, where it outlives every worktree:"
echo " (cd \"$primary_root\" && ./setup-hooks.sh)"
return 1
fi

if [[ $? -ne 0 ]]; then
echo "Failed to install dependencies. Please check the output above."
install_python=$(sed -n "s/^INSTALL_PYTHON=//p" "$hook")
install_python=${install_python#\'}
install_python=${install_python%\'}

if [[ "$install_python" != "$primary_root"/.venv/* ]]; then
echo "The shared pre-commit hook runs with ${install_python:-an unknown interpreter}, not the"
echo "primary checkout's environment, so commits will fail once that environment is gone."
echo "Rebind it from the primary checkout:"
echo " (cd \"$primary_root\" && ./setup-hooks.sh)"
return 1
fi

if [[ ! -x "$install_python" ]]; then
echo "The shared pre-commit hook runs with $install_python, which no longer exists, so"
echo "commits will fail. Reinstall it from the primary checkout:"
echo " (cd \"$primary_root\" && ./setup-hooks.sh)"
return 1
fi
}

if [[ "$mode" == "check-hook" ]]; then
check_shared_hook || exit 1
echo "Shared pre-commit hook is bound to the primary checkout."
exit 0
fi

if [[ "$mode" == "update" ]]; then
echo "Forcing update to latest versions..."
if ! uv lock --upgrade; then
echo "Failed to upgrade the lockfile. Please check the output above."
exit 1
fi
touch .deps-installed
fi

# Install pre-commit hooks
uv run pre-commit install
# Every run syncs. uv is idempotent and clones packages from its cache, so an
# environment that is already current costs little. This used to be gated on a
# stamp file that had been committed to the repository, which meant a fresh
# worktree checked the stamp out and whether it got an environment at all came
# down to the order in which checkout happened to write files.
echo "Installing/updating dependencies..."
if ! uv sync; then
echo "Failed to install dependencies. Please check the output above."
exit 1
fi

echo "Git hooks installed successfully!"
if [[ "$git_dir" == "$common_dir" ]]; then
if ! uv run pre-commit install; then
echo "Failed to install the pre-commit hook. Please check the output above."
exit 1
fi
if ! check_shared_hook; then
echo "Failed to bind the pre-commit hook to this checkout's environment."
exit 1
fi
echo "Git hooks installed successfully!"
exit 0
fi

# A linked worktree from here on.
if check_shared_hook; then
echo "Shared pre-commit hook is bound to the primary checkout; leaving it alone."
fi
Loading