Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Worktree Manager

A small web app for giving each of your git worktrees its own Apache port, so you can run several branches side by side and look at them all in the browser.

If you work with git worktree on a PHP project, you have probably done this by hand: pick a port, add a Listen line, write a <VirtualHost> block, fix APP_URL, restart Apache, and hope you did not typo anything. Doing it once is fine. Managing six of them, changing ports, and removing old ones is not.

This tool keeps those three things in sync and gives you a button.

What it does

  • Lists every worktree of your repository and the port it is served on
  • Add, edit, stop, start and delete port assignments from the browser
  • Rewrites APP_URL in each worktree's .env when its port changes
  • Validates before it writes: port conflicts, missing directories, bad paths
  • Tests the Apache config before restarting, and rolls back automatically if the config is bad, so a mistake cannot leave you without a web server
  • Keeps timestamped backups of every config it replaces

It does not create or delete git worktrees. You make those with git worktree add; this manages how they are served.

Requirements

  • PHP 8.1 or newer with the CLI binary (no extensions beyond the defaults)
  • Apache 2.4 with apachectl
  • git
  • sudo access for the one-time install

Tested against XAMPP. The Apache layouts for Debian/Ubuntu, RHEL/Fedora and a source build are auto-detected too, but have had less use — if something is off, php bin/doctor.php will tell you what it resolved.

Install

git clone https://github.com/AlpetGexha/apache-worktree-manager.git
cd apache-worktree-manager

php bin/doctor.php          # check what it detected
sudo ./install.sh           # one-time privileged setup
./start.sh                  # http://localhost:9999

install.sh does four things:

  1. Bakes your resolved paths into the privileged helper and installs it to /usr/local/bin/worktree-apply, owned by root
  2. Writes a NOPASSWD sudoers rule for that one script only, for your user
  3. Moves any Listen lines in the managed port range out of your main Apache config, so a single file owns them
  4. Adds an Include for the managed config, if your Apache does not already pick it up automatically

It backs up your main config first and restores it if Apache then fails its own config test.

On first run the app imports whatever worktree vhosts you already have, so an existing hand-rolled setup is adopted rather than wiped.

Usage

Start it, open http://localhost:9999, and you get one row per worktree.

  • Port rail across the top: every port in range, filled when running, outlined when stopped, grey when free. Click a free one to use it.
  • Save port — change a port. Also rewrites that worktree's .env APP_URL.
  • Stop — unbinds the port and removes the vhost, but keeps the entry so the port stays reserved. Start puts it back.
  • Delete — removes the service. Your worktree and its files are untouched.
  • Preview config — see exactly what will be written before you commit to it.
  • Apply — writes the config and restarts Apache.

Creating a worktree

Create a new worktree builds the command for you rather than running it — worktree creation stays in your hands. Type or pick a branch, tick what the new worktree needs, and copy the result:

cd /var/www/my-project && \
  git worktree add .worktrees/checkout-redesign -b feature/checkout-redesign && \
  mkdir -p .worktrees/checkout-redesign/bootstrap/cache ... && \
  chmod -R 777 .worktrees/checkout-redesign/bootstrap/cache ... && \
  cp .env .worktrees/checkout-redesign/.env && \
  composer install --working-dir=.worktrees/checkout-redesign && \
  php .worktrees/checkout-redesign/artisan key:generate && \
  npm --prefix .worktrees/checkout-redesign install && \
  npm --prefix .worktrees/checkout-redesign run build

Whether -b appears is worked out for you. Type a name git has never seen and the command creates the branch; pick an existing one and it checks that branch out instead. Getting this wrong fails either way — -b on an existing branch is fatal: a branch named 'x' already exists — so it is derived rather than left as a checkbox. The line under the fields tells you which case you are in.

The branch list offers every local branch that is not already checked out, since git will not put one branch in two worktrees; naming one that is gets you a clear error instead of a command that cannot work. The folder name is derived from the branch unless you override it. Steps are chained with &&, so a failure stops the rest.

Create writable dirs covers a trap specific to worktrees. Directories a project ignores outright — Laravel's bootstrap/cache and storage/framework/cache/data — are not in the index, so git worktree add never creates them. composer install then fails on its own post-autoload-dump hook with "The bootstrap/cache directory must be present and writable". The step runs before composer for exactly that reason, and chmods the directories because Apache usually runs as its own account. Set writable_dirs and writable_mode for a different framework, or writable_dirs => [] to skip it.

Copying .env keeps the parent's database — change DB_DATABASE afterwards if you want the worktree isolated. APP_URL you can leave alone: assigning a port here rewrites it.

Run the command, and the new worktree appears in the list above ready for a port. Set worktree_dir in config.php to put worktrees somewhere other than .worktrees/.

Nothing reaches Apache until you press Apply. Until then edits are saved locally and the page tells you changes are not live yet.

Configuration

Everything is auto-detected. To override anything:

cp config.example.php config.php

config.php is gitignored, so your machine's paths stay out of the repository. Every key can also be set as an environment variable with a WM_ prefix (WM_PORT_START=9000). Precedence, lowest to highest:

built-in defaults  ->  auto-detection  ->  config.php  ->  environment
Key Default What it is
repo_root nearest git repo Repository whose worktrees are managed
apachectl detected Path to the apachectl binary
managed_conf detected The config file this tool owns and rewrites
main_conf detected Main Apache config, for Listen cleanup
backup_dir next to managed_conf Where config backups are kept
helper_path /usr/local/bin/worktree-apply Where the helper is installed
port_start / port_end 8081 / 8199 Range offered to new services
docroot_suffix public Subdirectory Apache serves (web, or '')
server_name_pattern worktree-%s.local ServerName; %s is the slug
worktree_dir .worktrees Where the create command puts new worktrees
writable_dirs Laravel's set Dirs a fresh worktree needs but git ignores
writable_mode 777 Mode applied to those dirs
bind 0.0.0.0 Interface the app listens on
port 9999 Port the app listens on
auth_token none Shared secret; see Security

Run php bin/doctor.php any time to see the resolved values, which source they came from, and whether anything is broken.

Not a Laravel project?

Set docroot_suffix to whatever your framework serves from — web for Drupal, public for Laravel and Symfony, '' to serve the worktree root directly. The .env APP_URL rewrite is skipped automatically when a worktree has no .env.

Security

Read this before exposing the port. By default the app binds 0.0.0.0, which means anyone who can reach your machine on port 9999 can change your Apache config and restart it. That is fine on a laptop behind a firewall and a bad idea on a shared network.

Two ways to lock it down, in config.php:

'bind' => '127.0.0.1',              // local only
'auth_token' => 'some-long-random', // or require a shared secret

With a token set, open http://host:9999/?token=some-long-random once — the browser remembers it for the session and every API call carries it.

How privilege is handled

The web app never runs as root. All privileged work is one script, /usr/local/bin/worktree-apply, which:

  • takes no arguments — the only thing crossing the privilege boundary is "go"
  • has its paths baked in at install time, not read from a config file the app could write. If it read them at runtime, anyone able to reach the app could make root copy an arbitrary file to an arbitrary location.
  • is owned by root and not writable by the app's user
  • is the sole entry in a NOPASSWD sudoers rule scoped to your user

What the app will and will not serve

A DocumentRoot can only ever point at a directory that git worktree list reports for the configured repository. Arbitrary paths are rejected, which is also what stops the .env rewrite from touching files outside your worktrees.

Names and paths containing quotes, backslashes or control characters are rejected, so nothing can inject directives into a config that root then loads.

How it works

Three things have to agree for a worktree preview to work, and normally nothing keeps them in sync:

  1. Listen 8082 — so Apache binds the port
  2. A <VirtualHost *:8082> — so Apache knows what to serve
  3. APP_URL in that worktree's .env — so generated links are right

This tool keeps services.json as the source of truth and regenerates the whole managed config from it on every apply, Listen lines included. There is no patching of a live config with regexes, and no second place for the Listen lines to drift out of sync — which is why install.sh moves them out of your main config.

Apply is: write staged config → back up the current one → install it → run apachectl -t → restart on pass, restore the backup on fail. A hard restart is used rather than a graceful reload, because Apache does not re-read Listen directives on a reload.

Files

Path What it is
server.php Router and JSON API. Front controller for php -S
lib.php Config resolution, generation, validation. Pure, tested
index.html The UI. Vanilla JS, no build step, no dependencies
bin/worktree-apply.template The privileged helper, before path substitution
bin/doctor.php Prints resolved config and checks the environment
bin/dump-config.php Config as shell variables, for the install scripts
install.sh / uninstall.sh Privileged setup and teardown
start.sh Launcher
tests.php Assertions over lib.php
services.json Local state. Gitignored, rebuilt on first run

Tests

php tests.php

No framework and no dependencies — the suite is plain assertions over the pure functions in lib.php, covering config resolution, config generation, the .env rewrite, port allocation, validation and the injection guards. Exits non-zero on failure, so it drops straight into CI.

Troubleshooting

"Apply needs one-time setup"install.sh has not been run. The banner has the exact command.

"sudo: a password is required" — the helper is installed, but the sudoers rule names a different account than the one running the app. install.sh grants it to whoever invoked it, which is not always who starts the server. Either start the app as that user, or grant the current one:

sudo ./install.sh --user "$(id -un)"      # or --user alice,deploy for several

Never put a sudo password in a config file to work around this. The rule exists precisely so no password is needed, and a plaintext root password readable by the web app would be worth more than everything this tool guards.

Apply fails and says it rolled back — Apache rejected the generated config and your previous one was restored, so nothing is broken. The helper's output includes Apache's own error message.

A port shows as "down" after applying — Apache bound the port but the app behind it is erroring. Check that worktree's own logs; the vhost's error log is worktree-<slug>-error_log in your Apache log directory.

A deleted worktree still shows as available — deleting the folder by hand leaves git's entry behind, flagged prunable. Worktrees in that state are no longer offered, and a banner gives you the command:

git worktree prune

Using git worktree remove <path> instead of rm -rf avoids this. Note that a prunable entry still holds its branch, so the branch will not reappear in the create picker until you prune.

A worktree does not appear in the dropdown — it only lists worktrees that git worktree list reports for repo_root, excluding the main checkout. Run php bin/doctor.php to see what git reports.

"That path is not a worktree of this repository" — deliberate. Only real worktrees can be served. Check repo_root is the repository you think it is.

Changes are not showing up — nothing reaches Apache until you press Apply.

Uninstall

sudo ./uninstall.sh

Removes the helper, the sudoers rule and the managed config, then restarts Apache. Your worktrees, their files and this directory are left alone.

License

Not yet chosen — add a LICENSE file if you want others to be able to use this.

About

Give every git worktree its own Apache port. Manage vhosts, ports and APP_URL from one local web UI — validated before it writes, with automatic rollback if Apache rejects the config.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages