From 801d356b72894db918cb340007cc1470148ecc94 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Tue, 25 Aug 2026 15:14:11 -0400 Subject: [PATCH 1/3] feat(worktree): develop against a fork and its upstream from one clone 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-` 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 ` 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. --- docs/api/commands.rst | 8 + docs/api/utils.rst | 8 + docs/design/command-structure.rst | 5 + docs/features/django-fork.rst | 89 +++++++ docs/features/repo-management.rst | 74 ++++++ src/dbx_python_cli/cli.py | 2 + src/dbx_python_cli/commands/clone.py | 21 ++ src/dbx_python_cli/commands/install.py | 24 +- src/dbx_python_cli/commands/remove.py | 28 ++- src/dbx_python_cli/commands/switch.py | 8 +- src/dbx_python_cli/commands/sync.py | 12 +- src/dbx_python_cli/commands/worktree.py | 216 +++++++++++++++++ src/dbx_python_cli/config.toml | 4 + src/dbx_python_cli/utils/repo.py | 61 ++++- src/dbx_python_cli/utils/worktree.py | 268 +++++++++++++++++++++ tests/test_worktree_command.py | 302 ++++++++++++++++++++++++ 16 files changed, 1115 insertions(+), 15 deletions(-) create mode 100644 src/dbx_python_cli/commands/worktree.py create mode 100644 src/dbx_python_cli/utils/worktree.py create mode 100644 tests/test_worktree_command.py diff --git a/docs/api/commands.rst b/docs/api/commands.rst index 710e192..eb83da4 100644 --- a/docs/api/commands.rst +++ b/docs/api/commands.rst @@ -130,3 +130,11 @@ dbx_python_cli.commands.test module :members: :undoc-members: :show-inheritance: + +dbx_python_cli.commands.worktree module +--------------------------------------- + +.. automodule:: dbx_python_cli.commands.worktree + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/utils.rst b/docs/api/utils.rst index a73073e..bce4cf5 100644 --- a/docs/api/utils.rst +++ b/docs/api/utils.rst @@ -34,3 +34,11 @@ dbx_python_cli.utils.venv module :members: :undoc-members: :show-inheritance: + +dbx_python_cli.utils.worktree module +------------------------------------ + +.. automodule:: dbx_python_cli.utils.worktree + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/design/command-structure.rst b/docs/design/command-structure.rst index 953eccf..7ab80e2 100644 --- a/docs/design/command-structure.rst +++ b/docs/design/command-structure.rst @@ -97,6 +97,11 @@ Related subcommands that share a common domain: dbx project list dbx project remove + # Git worktree management + dbx worktree add django --upstream + dbx worktree list django + dbx worktree remove django + Command Groups (Two Levels) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/features/django-fork.rst b/docs/features/django-fork.rst index 6d157d0..6414176 100644 --- a/docs/features/django-fork.rst +++ b/docs/features/django-fork.rst @@ -42,6 +42,10 @@ organisation rather than a personal account: [repo.groups.django.preferred_branch] django = "mongodb-6.0.x" + # Add a `django-upstream` worktree on clone so the fork and upstream Django + # can both be worked on from one clone + upstream_worktree = ["django"] + Each key plays a specific role in fork maintenance: ``no_fork`` @@ -65,6 +69,11 @@ Each key plays a specific role in fork maintenance: ``preferred_branch`` The branch ``dbx clone`` switches to automatically after cloning. +``upstream_worktree`` + Lists repos that get a sibling git worktree checked out on the upstream + default branch immediately after cloning. See + :ref:`django-fork-upstream-worktree`. + Cloning the fork ---------------- @@ -87,6 +96,86 @@ To clone only the Django fork itself: dbx clone django +.. _django-fork-upstream-worktree: + +Developing against the fork and upstream from one clone +------------------------------------------------------- + +Fork maintenance regularly needs both sides at once: reading upstream Django to +see what changed, and editing the fork branch to adapt to it. A second clone +would mean a second full fetch of Django's history and two object stores to keep +current. + +A git *worktree* avoids that. One clone can have several branches checked out at +the same time, each in its own directory, all sharing a single ``.git`` object +store. Because ``upstream_worktree = ["django"]`` is set, ``dbx clone`` creates +one automatically: + +.. code-block:: text + + ~/Developer/mongodb/django/ + django/ # primary clone, on mongodb-6.0.x (origin = mongodb-forks) + django-upstream/ # worktree, on upstream-main (tracks upstream/main) + +The worktree's branch is named ``upstream-`` rather than just +``main``, so it cannot collide with a branch of the same name already tracking +``origin`` in the fork. + +Fetching in either directory updates the shared object store, so comparisons +between the two are local and immediate: + +.. code-block:: bash + + cd ~/Developer/mongodb/django/django + git log upstream-main..mongodb-6.0.x # what the fork adds + git diff upstream-main -- django/db/models # what the fork changed + +Managing worktrees manually +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``dbx worktree`` manages worktrees for any repo, whether or not +``upstream_worktree`` is configured: + +.. code-block:: bash + + # Add the upstream worktree (django-upstream, on the upstream default branch) + dbx worktree add django --upstream + + # Check out a specific upstream branch (django-stable-6.1.x) + dbx worktree add django stable/6.1.x --upstream + + # Check out an existing fork branch alongside the current one + dbx worktree add django mongodb-6.2.x + + # Override the directory suffix + dbx worktree add django stable/6.1.x --upstream --label 61 + + # Show every checkout attached to the clone (* marks the primary one) + dbx worktree list django + + # Remove by directory suffix (defaults to `upstream`) + dbx worktree remove django + dbx worktree remove django 61 --force + +Removal goes through ``git worktree remove`` so the registration inside the +primary clone is cleaned up too; deleting the directory by hand would leave a +stale entry that blocks recreating the worktree later. ``dbx remove`` handles +this as well, removing worktrees before the clone they belong to. + +A branch can only be checked out in one worktree at a time. ``dbx worktree add`` +reports git's refusal rather than working around it, so the fork branch you are +editing is never silently moved out from under you. + +.. note:: + + ``django-upstream`` is skipped by ``dbx sync``, ``dbx switch -g django`` and + ``dbx install -g django`` — see :ref:`upstream-worktrees` for why. It still + appears in ``dbx status``, ``dbx log`` and ``dbx branch``, which only read. + + In particular, the fork clone stays the installed one: a Django worktree has + the same package name, so installing it would replace the fork checkout as + the live ``django`` package in the group venv. + Syncing a branch with upstream ------------------------------ diff --git a/docs/features/repo-management.rst b/docs/features/repo-management.rst index 17e8a41..d24e7e0 100644 --- a/docs/features/repo-management.rst +++ b/docs/features/repo-management.rst @@ -241,6 +241,79 @@ completes — equivalent to running ``dbx sync mongo-python-driver`` right after - Like ``dbx sync``, it's a no-op (with a warning) if no ``upstream`` remote ends up configured for the repo - Pass ``--no-sync`` to ``dbx clone`` to skip this for a single invocation even when configured +.. _upstream-worktrees: + +Upstream Worktrees +~~~~~~~~~~~~~~~~~~ + +A git worktree lets a single clone keep several branches checked out at once, in +separate directories sharing one object store. That makes it possible to develop +against a fork *and* its upstream without cloning the upstream separately. + +List a repo in ``upstream_worktree`` to have ``dbx clone`` create one: + +.. code-block:: toml + + [repo.groups.django] + upstream_worktree = ["django"] + +Cloning then produces a sibling directory checked out on the upstream default +branch, under a branch named ``upstream-`` so it cannot collide +with a same-named branch tracking ``origin``: + +.. code-block:: text + + base_dir/django/django/ # fork clone, on its preferred branch + base_dir/django/django-upstream/ # worktree, on upstream-main + +Manage worktrees for any repo with ``dbx worktree``: + +.. code-block:: bash + + dbx worktree add --upstream # upstream default branch + dbx worktree add # an existing branch, alongside the current one + dbx worktree add -u # an upstream branch + dbx worktree list # every checkout (* marks the primary clone) + dbx worktree remove [