Lesson 10 / 12
Multiple Working Trees
An extra working tree shares the object store and copies 0 objects: in a thousand-commit history, the clone scheme writes 22560 objects for four branches while the tree scheme stays at 5640; 11466 of the 16920 gained objects are binary.
Contents
The previous lesson dropped clone cost to a third but did not eliminate the cost: the table still said clone, and the clone was paid again every time it was done. This carries an assumption with it too — the assumption that working on two branches of the same repository together requires a second copy.
The assumption is not true. A repository has two components, and they can be duplicated separately: an object store and a working tree. A clone copies both at once. This lesson’s question is: if the object store is left single and the working tree is duplicated, how many objects go uncopied compared to a clone per branch?
One Object Store, Multiple Trees
A worktree is a repository commit written to the file system. Multiple working trees is the scheme that lets more than one working tree be set up bound to the same object store. An extra tree is opened as a directory; inside it, a small file pointing not to a repository directory but to the main repository is placed, and from that moment the tree reads its objects from the main repository.
# taught commands — example dump, not executed git worktree add ../tree-report report git worktree add --detach ../tree-review <commit-id> git worktree list git worktree remove ../tree-report git worktree prune
add opens the new directory and checks out the given branch there. --detach checks out
not a branch but a commit directly; the tree stands in a detached-HEAD state, and this is
the state wanted for a temporary inspection. list reports every tree and which branch each
one is on. remove removes a tree, and prune cleans the administrative record of trees
whose directory was deleted by hand.
One constraint has to be known from the start: a branch can be checked out in only one tree at a time. Trying to check the same branch out in a second tree is refused. The constraint is not arbitrary — if two trees wrote commits to the same branch, which end the branch showed would depend on the tree, and it would lose reference uniqueness.
What Gets Shared
The distinction is built in one sentence: what belongs to history is shared, what belongs to work is specific to the tree.
The shared side is the object store and the references. A commit written in one tree drops into the store as an object and becomes reachable from the other trees from that moment — no transfer is required. Branches, tags, and the reflog are shared too.
The tree-specific side is the working state: the HEAD reference, the staging area, and the
files in the working area. Every tree keeps its own HEAD, so it can stand on a separate
branch; every tree keeps its own staging area, so a change staged in one does not show up in
another. An uncommitted change stays inside the tree and is not shared.
Configuration stands between the two. The repository’s configuration file is shared, and a change made in one tree affects them all; making a setting specific to just one tree is an option that has to be separately turned on. Repository attributes are shared too — that file, the subject of the next lesson, is a tracked file and belongs to history, so it applies the same rule across all trees.
This distinction directly gives the measurement’s setup: an extra tree copies no object, it only writes an administrative record and checks files out to the working area. A clone, by contrast, copies the entire object store.
The Measurement’s Assumptions
- LR14 — The measurement uses the shared setup’s four scales. The same work, on the same number of branches, is done under two separate schemes; the produced commit and object set is the same in both.
- LR15 — In the clone scheme, a separate clone is opened per branch and each clone copies the entire object store; partial clone and shallow clone are not used in this measurement.
- LR16 — In the tree scheme, the object store is single; an extra tree copies 0 objects. The only thing it writes is 3 administrative records per tree, and these records are not objects, they are counted separately.
- LR17 — The files checked out to the working area are the same in both schemes; four branches materialize four file sets in both schemes. This is why it is outside the measurement.
- LR18 — “Binary uncopied” is the share of the gained objects coming from binary bodies; this is the same count as the previous lesson’s object-type distinction.
- LR19 — The unit of the first and second parts is touched object. The unit of the third part is step, and a step is a single fetch that moves one branch’s tip from one copy to another.
Measurement
"""Multiple working trees: what object sharing gains over a clone per branch. Part 1 - four trees, four scales: clone-scheme and tree-scheme objects. Part 2 - gain as tree count grows (1000-commit history). Part 3 - access from one tree to a commit written in another: unit step. """ SEED = 20260814 FILES = ("metrics.py", "report.py", "identity.py", "config.py", "document.md") SCALES = (50, 200, 1000, 4000) TREES = (1, 2, 4, 8) ADMIN_RECORD = 3 # admin record written per extra tree; copies no object def rng(seed): d = seed % 2147483646 + 1 def draw(n): nonlocal d d = (d * 48271) % 2147483647 return d % n return draw def history(n, seed=SEED): """n-commit linear history; each commit touches one file.""" draw, commits = rng(seed), [] for i in range(n): file = FILES[draw(5)] binary = draw(11) == 0 commits.append({"no": i + 1, "file": file, "binary": binary, "objects": 2 + (40 if binary else 0)}) return commits def clone_cost(t, include_binary=True): return sum(x["objects"] for x in t if include_binary or not x["binary"]) def clone_scheme(t, trees): """One clone per branch: each clone copies its own object store.""" return trees * clone_cost(t) def tree_scheme(t, trees): """Multiple working trees: one object store, extra trees copy no object.""" return clone_cost(t) def access_step(trees, scheme): """Access from a commit written in one tree to the other trees: fetch step.""" return 0 if scheme == "tree" else trees - 1 print("commits clone objects 4-clone objects 4-tree objects gained objects binary uncopied") for n in SCALES: t = history(n) k, a = clone_scheme(t, 4), tree_scheme(t, 4) binary = 3 * (clone_cost(t) - clone_cost(t, False)) print(f"{n:6d} {clone_cost(t):13d} {k:15d} {a:14d} {k - a:15d} {binary:16d}") print() t = history(1000) print("trees clone-scheme objects tree-scheme objects gained objects objects per extra tree admin records") for trees in TREES: k, a = clone_scheme(t, trees), tree_scheme(t, trees) extra = (k - clone_cost(t)) // (trees - 1) if trees > 1 else 0 print(f"{trees:4d} {k:20d} {a:20d} {k - a:15d} {extra:22d} " f"{(trees - 1) * ADMIN_RECORD:13d}") print() print("trees fetch steps in clone scheme fetch steps in tree scheme") for trees in TREES: print(f"{trees:4d} {access_step(trees, 'clone'):27d} " f"{access_step(trees, 'tree'):27d}") print() print(f"single object store {clone_cost(t)} objects; four trees share the same store, " f"objects copied by the extra trees {tree_scheme(t, 4) - clone_cost(t)}") print(f"if the same work were done with four clones, objects copied " f"{clone_scheme(t, 4) - clone_cost(t)}")
commits clone objects 4-clone objects 4-tree objects gained objects binary uncopied
50 380 1520 380 1140 882
200 920 3680 920 2760 1638
1000 5640 22560 5640 16920 11466
4000 22600 90400 22600 67800 45990
trees clone-scheme objects tree-scheme objects gained objects objects per extra tree admin records
1 5640 5640 0 0 0
2 11280 5640 5640 5640 3
4 22560 5640 16920 5640 9
8 45120 5640 39480 5640 21
trees fetch steps in clone scheme fetch steps in tree scheme
1 0 0
2 1 0
4 3 0
8 7 0
single object store 5640 objects; four trees share the same store, objects copied by the extra trees 0
if the same work were done with four clones, objects copied 16920
What Sharing Gains
The first table’s unit is touched object. In a thousand-commit history, working on four branches writes 22560 objects under the clone scheme; under the tree scheme, the total stays at 5640. Gained objects: 16920 — the equivalent of three full clones. At four thousand commits, the same computation gives 67800 objects.
Where the gain comes from is written in the last column. Of the 16920 objects gained at a thousand commits, 11466 come from binary bodies; the previous lesson’s 0.6777 share drops in here exactly the same way. The more binary assets a repository has, the more opening a second clone costs — and that entire cost is never paid with a shared object store.
The pattern is the same at small scales too, but the magnitudes can mislead. At fifty
commits, the gain is 1140 objects; next to four thousand commits’ 67800, this looks
small, but the ratio between the two schemes is identical in every row. The gain is directly
the product (trees − 1) × clone; scale only grows the multiplier, it does not change the
relationship. The smallest measurable difference at this scale is 1/50, and no number in
the table rests on a claim below that resolution.
The second table gives the same thing by tree count, and the middle column stays fixed: two trees, four trees, eight trees — all 5640. The object copied per extra tree is 0 in every row; the 5640 on the right is what the clone scheme pays per extra copy. The only thing paid under the tree scheme is the administrative record at the far right: 21 records for eight trees. These are not objects and do not enter history.
The metric that follows from this is one sentence: working-tree count does not grow the object store. Under the clone scheme, cost grows linearly with tree count; under the tree scheme, it is fixed. This is the one pattern among the costs measured in this course that is entirely independent of scale: bisect grew slowly with scale, the rewrite ratio was fixed but its total grew; here, an extra copy’s object cost is zero at every scale and every tree count.
Visibility and the Fetch Step
The third table’s unit is step, and what it measures is not objects but access.
Under the clone scheme, a commit written to a branch stands in that clone’s object store. For the other copies to access it, a fetch is required; in a four-copy scheme, a commit reaching all of them is 3 steps, in an eight-copy one, 7. Under the tree scheme, this number is 0 in every row — the commit is already in the shared object store, and the other tree sees it the moment it is written.
This is the scheme’s second gain, independent of object count, and it matters more in practice. A fix written in one tree can be tested immediately in another; cherry-pick, comparison, and merge operations happen between trees without going over the network. Under the clone scheme, the same work requires a round trip to a remote repository.
The limit comes from the same place. A shared object store means a shared failure surface: a garbage collection done in one tree concerns every tree, and a repository corrupted in one tree affects all of them at once. Under the clone scheme, copies are isolated from each other, and this isolation is the one thing bought in exchange for the 16920 objects paid.
A Third Scheme: Switching in a Single Tree
The measurement compared two schemes, but the third one is the most common in practice: a single working tree and switching between branches. This scheme copies 0 extra objects — same as the tree scheme — and requires 0 fetch steps. Looked at by the object unit, it is the cheapest scheme.
Its cost is paid somewhere else. Every switch rewrites the files in the working area; if there is an uncommitted change, the switch is either blocked or the change is carried along. The Branching and Collaboration course counted this case in detail: which change gets carried, which is blocked, and which is left behind was measured there and is not repeated here. The stash is the standard way to get past this obstacle — it sets an uncommitted change aside and reopens it after the switch.
What multiple working trees solves is exactly this: there is no switch. Two branches are open at the same time, in two separate directories; a test can run in one while an edit is made in the other. The stash is not needed, because an uncommitted change does not need to give up its place.
In exchange, every tree materializes its own working state, and this is not limited to files. Dependency directories, build outputs, and caches are recreated per tree; these are outside the measurement because they do not belong to history, but they really do take up room on disk, and in a four-tree scheme they are produced four times. The 16920 gained in the object unit does not cover this cost. The choice between the two schemes therefore does not come down to a single number: the measured object gain is real and large, the unmeasured working state grows with tree count.
Removing a Tree and Recovery
When a working tree is removed, objects are not removed — the objects did not belong to the
tree. The only thing that can be lost is what was uncommitted in that tree: the changes
in the working area and the staging area’s contents. The remove command refuses if there
is an uncommitted change in the tree; deleting the directory by hand asks nothing. The
difference between these two paths is whether there is a warning.
The second loss path is quieter. A commit written in a tree in a detached-HEAD state is not
bound to any branch; once the tree is removed, that commit becomes unreachable and drops
once the garbage-collection window closes. The recovery path: the reflog is shared, so
the commit’s tip can be read from another tree and bound to a branch before the window
closes. The cheaper path is opening that tree not with --detach but with a branch name — a
branch is a reference, and a reference is independent of the tree’s lifetime.
For a tree standing on a removable medium or a temporarily mounted directory, there is a separate safeguard: the tree is locked. A locked tree’s administrative record is not cleaned up even when the directory is unreachable; otherwise the cleanup operation would ignore the tree, and the branch in that tree would become checkable out somewhere else. The lock does not prevent loss, it prevents a misdiagnosis.
The third is the administrative record itself: the record of a tree whose directory was
deleted by hand stays in the repository and keeps preventing that branch from being checked
out in another tree. prune cleans these records and touches no object.
Summary
- A repository has two components: an object store and a working tree. A clone copies both; multiple working trees duplicates only the second.
- The object store, branches, tags, and reflog are shared;
HEAD, the staging area, and the working area are tree-specific. A branch can be checked out in only one tree at a time. - In a thousand-commit history, the clone scheme writes 22560 objects for four branches, the tree scheme stays at 5640; 11466 of the 16920 gained objects come from binary bodies.
- The object copied per extra tree is 0 at every scale; the only thing paid under the tree scheme is 3 administrative records per tree, and the object store does not grow with tree count.
- In the step unit: a commit reaching every copy requires 3 and 7 fetches under the clone scheme, 0 under the tree scheme; what is lost in exchange is the isolation between copies.
Next Step
The measurement left one assumption out of scope: the assumption that the files checked out to the working area are the same under both schemes. This said that how files are written is not a choice — but there is a transformation layer applied to a file during checkout, and the blob in the object store and the file in the working area do not have to be identical to each other. The next lesson measures that layer: when line-ending conversion is not configured, which commit, in which file, writes a new object while actually changing nothing at all?
To keep your progress and take notes, Log in
My notes
Log in to take notes.