Skip to content

feat(worktree): develop against a fork and its upstream from one clone - #69

Merged
aclark4life merged 3 commits into
mainfrom
feat/worktree-support
Aug 25, 2026
Merged

feat(worktree): develop against a fork and its upstream from one clone#69
aclark4life merged 3 commits into
mainfrom
feat/worktree-support

Conversation

@aclark4life

Copy link
Copy Markdown
Collaborator

Problem

dbx clone django gives you one checkout of mongodb-forks/django with an upstream remote. Fork maintenance regularly needs both sides at once — reading upstream Django to see what changed, and editing the fork branch to adapt. A second clone means a second full fetch of Django's history and two object stores to keep current.

Solution

Git worktree support, so one clone has both checked out sharing a single object store:

base_dir/django/django/            # fork clone, on mongodb-6.0.x (origin = mongodb-forks)
base_dir/django/django-upstream/   # worktree, on upstream-main (tracks upstream/main)

Comparisons are then local and immediate: git log upstream-main..mongodb-6.0.x.

  • dbx worktree add|list|remove — manages worktrees for any repo.
  • upstream_worktree group config keydbx clone creates the worktree automatically. Enabled for django.
  • find_all_repos now tags each entry worktree. Worth noting: a linked worktree has .git as a file, so the existing .exists() check was already picking them up as ordinary repos.

The worktree's branch is upstream-<default branch>, not the bare branch name, so it can't collide with a same-named branch already tracking origin in the fork.

Which commands see worktrees

Read-only commands include them; commands that write to a checkout skip them.

Command Worktrees Why
status, log, branch included Read-only — seeing the upstream checkout's state is the point
sync skipped Rebase onto upstream + force-push to origin would push upstream's history into the fork
switch -g <group> skipped One checkout per branch; a bulk switch would fail or move a tree in use
install -g <group> skipped Shares its clone's package name; would replace the clone's editable install in the group venv

sync is guarded in _sync_repository, the worker all four sync paths route through, so an explicitly named worktree is refused too — the force-push isn't recoverable. switch filters group enumeration only; dbx switch <worktree> <branch> stays allowed, since it's reversible and git refuses on its own if the branch is checked out elsewhere.

dbx remove removes worktrees before their clone and goes through git worktree remove, so no stale .git/worktrees entries.

Testing

467 tests pass (24 new). The worktree tests drive real git repositories rather than mocking subprocess, since the behaviour depends on git's own bookkeeping — .git as a file, worktree registrations, refusing a branch checked out twice. One test asserts subprocess.run is never called for a worktree in sync, so the guard can't regress into "runs git, then bails".

Docs updated in features/repo-management.rst, features/django-fork.rst, design/command-structure.rst, and the API pages; sphinx builds clean.

Fork maintenance regularly needs both sides at once: reading upstream
Django to see what changed, and editing the fork branch to adapt. A
second clone means a second full fetch of Django's history and two
object stores to keep current.

Add git worktree support so one clone can have both checked out, sharing
a single object store:

  base_dir/django/django/            # fork clone, on mongodb-6.0.x
  base_dir/django/django-upstream/   # worktree, on upstream-main

- New `dbx worktree add|list|remove`.
- New `upstream_worktree` group config key, honoured by `dbx clone`
  right after the preferred-branch switch. Enabled for django.
- `find_all_repos` now tags each entry `worktree` (a linked worktree has
  .git as a file, so it was already being picked up as an ordinary repo
  by the existing .exists() check).

The worktree's branch is named `upstream-<default branch>` rather than
the bare branch name, so it cannot collide with a same-named branch
already tracking origin in the fork.

Read-only commands (status, log, branch) include worktrees; commands
that write to a checkout skip them:

- sync: rebasing a worktree onto upstream and force-pushing to origin
  would push upstream's history into the fork. Guarded in
  _sync_repository, the worker every sync path routes through, so an
  explicitly named worktree is refused too.
- switch -g: a branch can only be checked out in one worktree at a time.
  Only group enumeration is filtered; `dbx switch <worktree> <branch>`
  stays allowed, since it is reversible and git refuses on its own if
  the branch is checked out elsewhere.
- install -g: a worktree shares its clone's package name and would
  replace the clone's editable install in the group venv.

`dbx remove` removes worktrees before the clone they belong to and goes
through `git worktree remove`, so no stale .git/worktrees entries.
CI runners have no global git user.name/user.email, so the fixture's
seed commit failed with exit 128 ("Author identity unknown") and errored
all 19 tests that build a real repository. Supply the identity through
GIT_AUTHOR_*/GIT_COMMITTER_* in the environment, which keeps it scoped
to these tests instead of writing to the developer's global config, and
force commit.gpgsign=false so a developer's global signing config cannot
prompt or fail here either.

Verified by running the suite with HOME, GIT_CONFIG_GLOBAL and
GIT_CONFIG_SYSTEM stripped, which reproduces the CI failure without the
fix and passes with it.
The CI runners set init.defaultBranch=master. The fixture created the
bare origin with the machine default and then pushed HEAD:main, leaving
the bare repo's HEAD pointing at an unborn `master` while the only real
branch was `main`. upstream/HEAD was therefore unresolvable and
get_remote_head_branch returned None, failing 8 tests.

Pass --initial-branch=main so the branch name is pinned rather than
inherited. Reproduced locally with a global config setting
init.defaultBranch=master: 8 failures without this change (the same 8
CI reported), 24 passing with it.
@aclark4life
aclark4life merged commit d6833d3 into main Aug 25, 2026
12 checks passed
@aclark4life
aclark4life deleted the feat/worktree-support branch August 25, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant