akinolur

Git worktree with local subdomains

tl;dr: git worktrees let you run several coding agents in parallel. Use worktrunk to give each worktree its own dev server with a subdomain and https.

Note: examples below use a docker compose setup. Adjust as needed to your stack.

Prerequisites:

Worktrunk

Worktrunk (wt) is a cli wrapper around git worktrees with hooks mechanism. The main config file is .config/wt.toml in the root of your project.

The workflow with worktrunk is described here, and looks like this:

# create new worktree "feature" from "main" branch
wt switch -c "feature"
# edit code... then merge changes into "main" branch
wt merge
# or
wt remove "feature"

Copying .env and other untracked files

Problem: when creating a git worktree, only tracked files are copied. But running a project often requires files from .gitignore. E.g. .env, or your coding agent's local config, or other common patterns.

Use the wt step copy-ignored command with flag --require-include and create file .worktreeinclude. Track .config/wt.toml and .worktreeinclude files in git.

# .config/wt.toml
[[pre-start]]
copy = "wt step copy-ignored --require-include"
# .worktreeinclude
.env
.claude/settings.local.json

Running wt switch -c "feature" now will bring .env and the other specified files into the worktree folder.

Running the project and cleanup on removal

Problem: after removing a worktree, a project's processes and artifacts can be left behind.

Read about dev server and wt step tether for an example of a dev server that automatically starts with a new worktree and stops when the worktree is removed.

Adjust command to starting a project and to turning it off. E.g. when I work with docker, I start the project manually when it's needed, so as not to waste RAM unnecessarily. And pre-remove hook is used for cleaning up artifacts: containers, images, volumes.

# .config/wt.toml
[[pre-commit]]
lint = "ruff check --fix ."
[[pre-remove]]
remove-containers = "cd {{worktree_path}} && docker compose --env-file .env --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml down -v --rmi local"
[aliases]
up = "docker compose --env-file .env --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml up --build -d"

The wt up alias starts the project. And when work on the worktree is done, call wt merge. At this point linters are run. And if checks pass successfully and there are no conflicts with the main branch, code changes get merged into the main branch. The running project gets stopped, and volumes and images are removed.

Hint: the pre-commit hook is a good place to call linters and other validations.

Worktree-specific variables

Problem: a typical web project needs to listen on a port. Each worktree needs a new port and new variables. To avoid touching the original .env, write new values into .env.local.

# .config/wt.toml
[[post-start]]
env-overlay = """
cat > {{worktree_path}}/.env.local <<EOF
CORS_ALLOWED_ORIGINS=https://{{ branch | sanitize | replace('_', '-') }}.{{ repo }}.localhost:8080
GATEWAY_PORT={{ (repo ~ '-' ~ branch) | hash_port }}
EOF
"""

hash_port provides a stable port within a single worktree. It's better to compute the port from repo + branch rather than just branch, so that branch "main" in project "A" and branch "main" in project "B" get different ports.

Note the replace('_', '-'). Branch names can contain special characters. Formally, domains can only use letters, digits and "-". So keep an eye on which special characters appear in branch names (and repo), and either avoid using them or do the replace.

Hint: docker-compose.yml doesn't automatically use variables from .env.local during interpolation.

# docker-compose.yml
services:
  gateway:
    ports:
      # GATEWAY_PORT needs to be interpolated from .env.local
      - "${GATEWAY_PORT:?set GATEWAY_PORT}:80"

So when running the command you should explicitly specify the file with variables:

docker compose --env-file .env --env-file .env.local up

Subdomain with https per worktree

Problem: I don't want to search the logs for ports or manually type the URL of the running project every time.

The documentation already describes a ready-to-use config. A subdomain from caddy + wt open alias remove the need to think about the port or the URL-building logic. wt open just opens the page.

I made a couple of changes to the original config:

# .config/wt.toml
[[post-start]]
proxy = """
curl -sf --max-time 0.5 http://localhost:2019/config/ >/dev/null || caddy start
curl -sf http://localhost:2019/config/apps/http/servers/wt >/dev/null || \
  curl -sfX PUT http://localhost:2019/config/apps/http/servers/wt \
    -H 'Content-Type: application/json' \
    -d '{"listen":[":8080"],"automatic_https":{"disable_redirects":true},"tls_connection_policies":[{}],"routes":[]}'
curl -sf -X DELETE http://localhost:2019/id/wt:{{ repo }}:{{ branch | sanitize }} >/dev/null || true
curl -sfX PUT http://localhost:2019/config/apps/http/servers/wt/routes/0 \
  -H 'Content-Type: application/json' \
  -d '{"@id":"wt:{{ repo }}:{{ branch | sanitize }}","match":[{"host":["{{ branch | sanitize | replace('_', '-') }}.{{ repo }}.localhost"]}],"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:{{ (repo ~ '-' ~ branch) | hash_port }}"}],"headers":{"request":{"set":{"X-Forwarded-Proto":["https"]}}}}]}'
"""
[aliases]
open = '''python -m webbrowser "https://{{ branch | sanitize | replace('_', '-') }}.{{ repo }}.localhost:8080"'''
up = "docker compose --env-file .env --env-file .env.local -f docker-compose.yml -f docker-compose.local.yml up --build -d"
[list]
url = "https://{{ branch | sanitize | replace('_', '-') }}.{{ repo }}.localhost:8080"

Hints:

Manually running the proxy

Problem: wt switch -c "feature" sets up caddy and creates .env.local. But what about the main worktree? Those hooks aren't triggered there. Or what to do after reboot?!

I couldn't come up with anything better than manually calling the hooks. Usually I work in new short-lived worktrees, so it's rarely needed. But it's useful when I need to run "main" for comparison.

[aliases]
host = "wt hook post-start proxy env-overlay -y"

See also

Send feedback