Submodule, subtree, and remote creation workflows
Repository Settings brings dependency topology into one workspace. The Submodules surface can add, clone/update, synchronize, configure, remove, or temporarily open a recorded submodule. The Subtrees surface discovers vendored prefixes from history and can add, pull, push, or split them. The Add Submodule dialog can also create an initialized GitHub or GitHub Enterprise repository and immediately add its returned clone URL.
Behavior and configuration
Submodule add offers hosted-provider browsing, a direct URL, or
Create remote. Every route reviews the
repository-relative checkout path and an optional tracked branch. Remote
creation additionally reviews the exact authenticated account, personal
or loaded organization owner, repository name, description, and
public/private choice, with private selected by default. It requests an
initial commit so Git can clone the new repository immediately, then
runs the ordinary account-aware git submodule add path.
The URL route also offers a searchable branch
picker. A "Load branches" action beside the URL field (also
triggered automatically the first time the field blurs with a valid URL)
runs git ls-remote --symref against the source for
HEAD and refs/heads/* — --heads
alone would drop the HEAD symref advertisement that names the remote
default branch. While the listing loads a polite live-region status is
announced; the loaded list renders as a type="search"
filter input wired to the shared fuzzy/substring/regex filter stack and
full regex builder (surface id add-submodule-branches,
plain-text matching by default) above a select whose first entry is the
remote default branch, visibly marked as the default and pre-selected.
Picking a branch writes it into the free-text branch field; typing free
text deselects the list (a disabled "custom branch" indicator appears);
picking the remote-default entry clears the field so
git submodule add runs without -b, exactly
like leaving the branch empty today. The listing is bound to the exact
URL it was loaded for and hides when the source changes. The free-text
field always remains usable as a manual override, and the Create remote
route keeps its existing no-branch behavior.
The Submodules manager shows URL, tracked branch, current object ID, and initialized/up-to-date/out-of-date/conflicted state. Per-row actions retain their own progress and review; the separate temporary-open workflow is documented in Temporary submodule repository navigation.
The Subtrees manager searches up to 400 recent commits with
git-subtree-dir trailers, keeps the newest record per
prefix, and shows the latest merge and split IDs. Add and pull can
squash, with squash enabled by default for add; pull and push select a
remote or validated custom source and ref. Split requires a reviewed
local branch name and reports the resulting split-head SHA.
Persistence
Submodules use Git's normal .gitmodules, gitlink,
checkout, and .git/modules state. Add and remove leave
ordinary staged changes for the user to commit. A created remote is real
provider state. If Git add fails afterward, the dialog retains the
created repository result for that retry and reuses its clone URL rather
than creating a duplicate.
When .gitmodules is staged but temporarily absent from
the working tree, Desktop restores that exact valid staged blob after
destination validation and before git submodule add.
Commit-time Cheap LFS preparation excludes Git metadata such as
.gitmodules from pointer scanning, so the staged
declaration and gitlink commit normally instead of producing an
unsafe-path error.
Subtrees are ordinary files and commits in the superproject. Their
manager has no separate topology database: it reconstructs known
prefixes from the git-subtree-dir and
git-subtree-split trailers in repository history. Search
mode is local UI metadata; source, ref, squash, and split drafts are
transient.
Failure modes and recovery
Submodule add rejects duplicate paths, occupied files or non-empty
folders, absolute paths, parent traversal, .git segments,
invalid branches, and stale account or organization selections before
mutation. Cancellation stops the owned request/process. A remote-create
failure never invokes Git.
The .gitmodules repair never overwrites an existing
path, never invents configuration, and does not run for an invalid
destination. If the index has no stage-0 blob or Git's config parser
rejects that blob, the original Git error remains visible. An empty but
valid config is restored so Git can append the first stanza. Handle
metadata, write, and close failures all enter the same cleanup path. The
repair removes the file only when device/inode identity proves it still
owns that pathname; if identity cannot be established after a retry, it
preserves the path rather than risk deleting a concurrent
replacement.
Branch listing is non-blocking in every failure mode. An unreachable
or unauthorized remote reports an inline role="alert" error
and leaves manual branch entry (and submission) fully working. An empty
repository is a valid, empty listing — the picker states the remote has
no branches yet and the submodule follows its future default branch. A
remote advertising more than 5,000 heads is truncated at that bound with
an explicit "showing the first N branches" notice; malformed
ls-remote lines are skipped rather than treated as errors.
A stale in-flight listing is aborted and sequence-guarded so it can
never overwrite a newer load, and an invalid search regex keeps every
branch visible while reporting the pattern error inline. Because a
cancelled provider request can have an uncertain server result, the
dialog asks the user to check the host before retrying; once a created
result is known, a later Git failure is reported separately and the next
attempt does not recreate the remote.
Subtree discovery errors remain visible and stale loads cannot
overwrite a newer refresh. If bundled Git does not provide
git subtree, recorded prefixes remain readable but add,
pull, push, and split are disabled. A synchronous manager-wide mutation
lock prevents overlapping actions and fences settings dismissal or
navigation until the running Git operation settles; subtree operations
do not expose cancellation. Authentication or Git failures stay with the
exact row/action, ready for an explicit retry after recovery.
Security considerations
Submodule destination validation resolves the physical repository boundary and refuses traversal, sibling-prefix, symlink, junction, Git-metadata, duplicate, file, and non-empty-directory targets. Git receives the source, branch, and path as positional argv with an option separator, and account identity is passed to the credential trampoline rather than embedded in a URL.
Branch listing revalidates the source URL
(getSubmoduleSourceError) before spawning Git, passes it
after the -- separator, uses the same remote-operation
environment and credential trampoline as the add itself, and honours an
AbortSignal that kills the spawned process. The parsed
branch list is bounded at 5,000 entries so a degenerate or adversarial
remote cannot balloon renderer state. User-authored search patterns are
compiled by the vetted RE2 engine (linear-time, no catastrophic
backtracking) with the shared pattern- and input-length bounds; branch
names and patterns are evaluated locally and never transmitted.
Create remote accepts only an authenticated GitHub-family account and an owner from the loaded account data. Repository name and description are length- and character-bounded, provider cancellation is forwarded, and an unusable returned clone URL is never passed to Git. Subtree prefixes must be forward-slash relative paths with no empty, current, parent, drive, or absolute segments; provider-backed operations use the selected account and existing bounded Git progress path. Every submodule and subtree mutation also rechecks the temporary submodule workspace boundary immediately before Git runs.
Verification
submodule-add-test.ts and
add-submodule-dialog-test.tsx cover source, branch,
physical-path and occupied-target validation, provider/account
selection, review, progress, cancellation, and responsive controls. The
dialog suite also proves the branch picker: on-demand and blur-triggered
loading (once per URL), default-branch pre-selection and its empty
-b semantics, picker/free-text synchronization, filter
narrowing with an honest no-match message, inline non-blocking load
failures, and empty/truncated listings.
ls-remote-heads-test.ts covers the pure
ls-remote --symref parser: branch heads, the HEAD symref
default, CRLF and SHA-256 output, malformed and out-of-scope lines,
empty repositories, and the truncation cap.
collection-surface-registry-test.ts audits the
add-submodule-branches search surface's one-to-one binding
with the shared filter control and regex builder.
submodule-remote-creation-test.ts covers initialized
public/private creation, organization ownership, strict metadata,
cancellation uncertainty, unusable clone URLs, and no API call for
invalid input. The dialog suite also proves that failed Git add retries
reuse the created remote. git/submodule-test.ts proves the
exact staged .gitmodules blob is restored only while the
working file is absent. cheap-lfs/pointer-test.ts proves
normal commit-time pointer scans exclude Git metadata while retaining
safe payload and gitlink paths.
git/subtree-test.ts covers prefix validation, trailer
discovery, account-aware add/pull/push argv, progress, split results,
and capability probing. subtree-manager-test.tsx covers
discovery, filtering, row editors, squash and custom-source choices,
stale loads, busy-state isolation, errors, refresh, and the
unavailable-command fallback.