Submodules
You can use the Submodules tab to manage nested Git repositories within your project.
What a submodule is
A submodule is another Git repository checked out inside this one. The repository that contains it is the superproject.
The superproject does not track a branch of the submodule. It records one exact commit, in an index entry Git calls a gitlink. Git’s own documentation calls that commit “the commit recorded in the superproject”, and Zide shortens it to the recorded commit. Everyone who clones your project gets the same recorded commit, so everyone builds against the same code.
Two things are committed and therefore shared: the declaration in .gitmodules, which gives each submodule’s path, URL, and optional branch, and the gitlink, which gives its recorded commit. Everything else is local to your clone, including the submodule’s own repository in .git/modules/<name> and the URL this clone fetches from. See The three places a submodule URL lives.
Two operations cover normal use.
- Initialize registers the submodule in this clone and checks out the recorded commit.
- Update applies the recorded commit to a submodule that already has a working copy, using the configured update procedure:
checkout,merge, orrebase.
After an ordinary update, the submodule sits on a detached HEAD at the recorded commit. That is the normal state for a submodule, not a fault.
Changing the recorded commit is a separate act, and it is yours. Work inside the submodule, commit there, then stage the submodule in the superproject and commit it. Nothing Zide does automatically writes a new gitlink.
Fetch does neither. It refreshes the submodule’s remote-tracking branches and moves no checkout and no gitlink.
Navigate the Submodules tab
Open the Submodules tab from Git > Submodules, the Submodules row above the file browser tree, or a submodule folder’s Show in Submodules command. The tab belongs to the repository it was opened from, and opening it again brings that tab forward rather than making a second one.
Every Git tool tab carries a Git tools control beside its title. Use it to move between Git Summary, Remotes, Submodules, Subtrees, Git LFS, Sparse Checkout, Repository Config, and Hooks. The menu marks the tool you are on, and choosing a tool opens it for this repository, or brings its tab forward when it is already open. Each tool is its own tab, so two projects can each have their own.
Submodules appear in a tree. To show a submodule’s children, click its Nested submodules row, or focus the row and press Enter or Space. The row shows Checking… while Zide checks for problems. It then shows how many nested submodules need attention, such as 2 submodules need attention. The count includes all levels below that submodule. If none need attention, no count appears. If Zide could not read part of the tree, the row says Part of this could not be read. If the check fails, each row shows Could not check. You can keep working while Zide checks.
Each row shows the path from this repository, so a nested submodule reads as vendor/outer/middle. Every row has its own actions, and an action on a nested submodule is applied by the submodule that contains it, which is the repository that records the commit.
A submodule with no working copy shows no nested section. Its list of submodules lives inside the working copy, so it cannot be read until the submodule is initialized.
Submodule health outlives this tab. It is reported on the file browser’s Submodules row and on the Files activity icon. See Submodule status.
In the File Browser, submodule folders use the submodule marker even when they are nested inside another submodule. Click that marker to open the Submodules tab and reveal the matching card. Expanding an uninitialized submodule shows Initialize Submodule instead of (Empty folder); that action initializes only the selected submodule. The folder’s context menu also offers Show in Submodules, Initialize Submodule or Update Submodule, and the explicitly named Open Submodule as Project. Double-clicking the folder is ordinary File Browser navigation and does not open another project.
The following actions are displayed in the Submodules tab.
| Action | Description |
|---|---|
| Add Submodule | Adds a new submodule. |
| Update All | Initializes missing submodules and updates all submodules to the commits recorded by this repository. |
| Fetch All | In the tab’s More actions menu. Fetches the remotes of every initialized submodule. |
| Edit .gitmodules | In the tab’s More actions menu. Opens the repository’s .gitmodules file in the File editor. |
Update submodules
Click Update All to initialize the submodules that have no working copy and update the initialized ones to the commits your repository records for them. It is available whenever there is either kind of work to do, including right after a fresh clone.
Update All covers the whole tree, including nested submodules, whether or not they are expanded. The submodule.recurse setting in your Git configuration decides what Zide does on its own after a pull or a checkout; it does not narrow an action you asked for directly.
Update All does not initialize a submodule you deinitialized on purpose. Use that submodule’s Initialize action to restore it.
It runs as a single operation. Zide checks every submodule when you click the button and reports a result for each one, including the reason for anything it did not change. The summary counts what happened: how many submodules were initialized, how many were updated, how many were already at the recorded commit, and how many need your attention.
While it runs, each affected card shows its own state in its action area. The buttons stay in the layout invisibly, so the card does not resize as progress comes and goes. Work inside a collapsed subtree appears in that card’s Nested submodules section; expanding it reveals the individual working rows. The Update All button itself becomes a spinner and Cancel without adding a banner above the list.
When it finishes, routine success is represented by the refreshed card state and its normal Open, Update, and menu actions; an initialized submodule does not need an Initialized badge. Results that still require explanation, including Needs attention, Failed, and Not attempted, stay on their rows until the next operation so a problem remains visible after the notification has gone.
Cancelling takes effect at the next safe boundary. The submodules it never reached are marked not attempted rather than skipped, and running Update All again finishes them.
Zide never discards work. A submodule is skipped, with the reason shown in the Console, when any of the following is true.
| Situation | What Zide does |
|---|---|
| The submodule has uncommitted changes to tracked files | Skips it. Commit or stash the changes first. A submodule of its own sitting at a different commit does not count here: an update does not descend, so that nested checkout and its commits are untouched by it. |
| The submodule has a branch checked out | Skips it, because updating would detach that branch. Set submodule.<name>.update to merge or rebase if you want the branch moved instead. |
| The checkout is ahead of the recorded commit, or has diverged from it | Asks first. Moving the checkout back would leave those commits reachable only through the submodule’s reflog. See Update anyway. |
| The submodule is not active | Skips it. This is submodule.<name>.active, or a submodule.active path pattern that does not cover it. |
| You deinitialized the submodule | Leaves it deinitialized. Zide never re-clones it on its own. |
| The submodule has no working copy and its folder is outside the sparse checkout | Skips it. |
submodule.<name>.update is none | Skips it. |
submodule.<name>.update runs a custom command, or a value Zide does not recognize | Refuses to run it, and names the procedure. |
| The recorded commit is not in the submodule yet | Fetches it first, then updates. |
Untracked files alone do not block an update.
Update anyway
One transition is never made silently: moving a clean, detached submodule back onto the recorded commit when its checkout holds commits the recorded commit does not. Git performs it without a word, and afterwards those commits are in the submodule’s reflog and nowhere else, unless a branch or tag already points to them.
Zide plans the operation first and stops before anything runs. A dialog states the loss in its title, names the submodule, and shows the same two labelled commits the card does: Local commits at risk and Update target. Where the count is real it is given; a diverged history gets none, because there is no fast-forward between the two sides to count.
- Cancel holds the focus, and Escape chooses it. Nothing has run.
- Update anyway runs the update you asked for, on exactly the submodules the dialog listed.
- Click a commit or View diff to cancel the update and open the commit or comparison. It opens in the project that started the update, even if you switched projects. Ctrl+click, or Command+click on macOS, opens a new tab in the background.
If Zide could not read part of the submodule tree, the dialog says so, for example Zide could not read 1 level of this submodule tree. Other submodules may also be affected. In that case the title does not give a submodule count, because the list may be incomplete.
To keep the commits instead, cancel and create a branch or tag in the submodule. A gitlink is an object ID, not a ref, so recording one in the superproject does not on its own keep the commits from being pruned.
One dialog covers the whole operation, whether you pressed Update on one row or Update All over the repository. The risk does not read as smaller because it came from a smaller button.
If the repository changes between the plan and the run, the operation is refused rather than trusted, and Zide asks once more with the set it just re-planned. It never retries on its own.
Update submodules automatically
When you switch branches, merge, reset, abort an operation, or pull, Zide checks whether the operation moved the commit your repository records for any submodule, and reconciles only those submodules, under the same rules as Update All. A submodule that has never been initialized is still detected, even when the pull brought nothing new.
The parent operation’s result and the submodule result are reported separately. If the branch switch succeeded but a submodule was skipped, the branch switch still succeeded; the submodule outcome appears in the Console.
After a Pull from the project header, the Pull button flashes amber rather than green when a submodule needs you, because the pull itself did succeed. A single notification says so and offers Review Submodules, which opens this tab on the submodule in question. What lasts is the Submodules row above the file browser tree and the mark on the Files activity icon; every submodule the pull could not move is also marked on its own card here, so you land on the rows in question rather than on a list you have to read through.
Reconciliation does not run when the operation left unresolved conflicts or was rejected.
To change this behavior, set the zide.submodulePolicy key in your repository’s Git configuration.
| Value | Behavior |
|---|---|
auto | Reconciles the submodules that can be updated safely. The default. |
ask | Works out what it would do and reports it, but changes nothing. |
manual | Does nothing automatically. |
This setting only affects what Zide does on its own. Clicking Update All or a per-submodule action is a direct request, so those actions run in every mode.
This is Git configuration, not a Zide setting. Add it in the Configuration table in the Repository Config tab, or with git config --local zide.submodulePolicy ask. Zide writes this repository behavior only at Local or Worktree scope. It belongs to that checkout and is not committed or carried to another clone. An existing value from another Git scope is still honored, but Zide will not offer to rewrite that source in place. A value Zide does not recognize is treated as manual.
Work inside a submodule
Click Open on a row to open the submodule as its own project tab. From there it behaves like any other repository.
Two habits keep a submodule out of trouble.
Check out a branch before you commit. An ordinary update leaves a detached HEAD. Commits made there belong to no branch, so an update that moves the checkout back leaves them reachable only through the reflog. Zide refuses that update and asks first; see Update anyway.
Record your work in the superproject. A submodule commit that only exists in your clone breaks every other clone once the superproject points at it. Stage the submodule in the superproject and commit it, then push the submodule before you push the superproject. Zide checks this for you; see Pushing a project that has submodules.
While you have a branch checked out, the default checkout procedure would detach it, so Zide skips the submodule rather than moving your branch. Set submodule.<name>.update to merge or rebase to have updates carry the branch forward instead.
Choose a repository source
The required Repository source control appears when you clone a repository and when you add a remote, submodule, or subtree. Type any Git address, or click Browse… to open Select Repository.
The field takes the address forms Git takes: https://, http://, ssh://, git://, file://, ftp://, ftps://, the scp-like user@host:owner/repo.git, and a local path. For a server on a non-default SSH port, use ssh://user@host:port/owner/repo.git. The scp-like form cannot carry a port. A local path must be absolute, such as /srv/git/lib.git or C:\dev\origins\lib.git, or start with ./ or ../. When Zide cannot use an address, the message below the field names the problem, such as Add the repository path after the host, such as owner/repo.git.
The picker lists repositories from your connected provider accounts. Use Provider account to switch accounts, then use the filter box or choose a provider view. GitHub uses My Repos and Organizations, GitLab uses My Projects and Groups, and Bitbucket uses Workspaces. On GitHub, My Repos also includes repositories where you are a collaborator.
A provider can have more repositories than Zide loads. When that happens, a note above the list says so, such as 100 repository limit reached or Repository results may be incomplete. The filter box searches only the loaded results. If your repository is not listed, cancel the picker and enter its address in Repository source. See Add a repository for the clone flow.
The summary below the field names the repository and shows where the value came from, such as Selected from GitHub, Selected from this repository, or Entered as a Git address. Beside it, Authentication shows the identity Git will use. It can show Local source, System SSH configuration, System Git configuration, a named account, or System Git credentials. When two or more accounts match, choose one from the Authentication list or choose System Git credentials.
For Local source, the status says This source does not use a provider account. For System SSH configuration, it says Git will authenticate with your SSH configuration. When Zide has no matching account or you choose System Git credentials, it says No matching Zide account is selected. Git will use its configured credential helper.
Choosing a repository through Browse… binds that provider account to the source. If you edit the address afterwards, Zide resolves authentication again from the new address.
Add a submodule
The Add Submodule dialog derives the submodule’s folder name from the repository you choose. It opens with one sentence of orientation, A submodule links another Git repository at a recorded commit. The rest of what the operation does is explained here rather than in the form.
What Add Submodule does
Your project does not get a copy of the other repository. It records which commit of that repository to use. Git calls that recorded pointer a gitlink, and it is why two people who clone your project get the same version of the submodule.
Adding a submodule changes three things.
| What | Where | What it holds |
|---|---|---|
| The working copy | The folder you chose | A real clone of the other repository, checked out at one commit |
.gitmodules | Your repository root | The submodule’s name, address, and tracked branch. This file is committed, so everyone who clones your project gets it |
| The gitlink | Your repository’s index | The exact commit your project records for that folder |
Zide clones the repository into the folder you chose. If that folder already holds the repository, Git uses what is there instead of cloning again.
Nothing is committed for you. .gitmodules and the gitlink are left staged, so you can review them like any other change. Your project does not record the submodule until you commit, and nobody else receives it until you push that commit.
The local path restriction
Git refuses to clone a submodule from a local path or a file:// URL unless you allow it. Since CVE-2022-39253 this is Git’s default, and it applies to submodule clones only. A plain fetch or clone from a local path is unaffected, so Add Remote and Clone Repository never hit it.
Add Submodule reports Your Git configuration does not allow this repository source type. To allow it, run:
git config --global protocol.file.allow alwaysSet this option in your global or system Git configuration. The submodule clone does not read the parent repository’s local configuration.
Only always permits it. The other values, user and never, do not.
Choose the source and destination
To choose the source and destination, complete the following steps.
- In the Submodules tab, click Add Submodule.
- In the required Repository source field, type a Git address or click Browse… to choose one.
- In Parent Folder, enter a repository-relative folder. Leave it empty to use the repository root, shown as
Repository root, or click Browse… to open Choose Parent Folder at the repository root. - Check the Will create in: preview. Zide reports an unusable parent folder, folder name, or resolved path below the preview, so you can correct it before submitting.
- Optional: Expand Advanced Options. Folder name override is marked (optional) and uses
Use repository namewhen empty. Before you choose a repository, the hint says Choose a repository to determine the default folder name. Afterward, it tells you to leave the field blank and names the folder Zide will use. Enter a different name only when you want to override it, or use Clear folder name override to return to the derived name. Use Branch to choose the branch the submodule tracks. - Click Add Submodule.
Add Submodule stays disabled until the source and resolved path are both valid. When it is ready, press Enter in Parent Folder or Folder name override to submit the dialog. Click Cancel to close it without adding the submodule.
Zide adds the submodule, then reports Added and staged <path>. Review and commit the changes. Open the Staging tab to see the two staged entries described above.
Fetch submodule remotes
Open the tab’s More actions menu and click Fetch All to fetch the remotes of every initialized submodule. Uninitialized submodules are skipped.
Fetching updates each submodule’s remote-tracking branches. It does not change the commit a submodule has checked out, and it does not change the commit your repository records for that submodule. To change either of those, use Update or Update All.
Submodules are fetched one at a time, as a single operation. While it runs, the More actions button becomes the spinner and Cancel control. If a submodule cannot be fetched, the remaining submodules are still fetched, and the failure is reported in the Console with the submodule path and the error.
A submodule with several remotes fetches all of them, and one unreachable remote does not stop the others, but it is still reported. The submodule is listed as needing attention, naming the remote that could not be reached, rather than counted as a plain success.
The remote the status is compared against is the exception: if that one fails, the submodule counts as failed, because its status would otherwise be reported from refs that predate the fetch.
To fetch a single submodule, open the row’s menu and click Fetch from remote.
Stop a running operation
While Zide is working, the control that owns the operation becomes the way to stop it: Update All, the tab’s More actions button after Fetch All, or the affected row’s Update or Initialize button reads Cancel. Each affected card shows its own state without inserting a tab-level progress banner.
Zide stops between submodules, never part way through one. Interrupting a checkout would leave a working copy Git itself considers broken, so the submodule being worked on finishes and nothing after it starts.
When the tab has no cards
Three states replace the list, because there is nothing to read alongside them.
| State | Shows |
|---|---|
| No submodules | This repository has no submodules, followed by a sentence on what a submodule is and on the commit this repository records for it. Nothing here is missing; most repositories have none. |
| First read | Reading submodule status… A later refresh leaves the existing cards alone. |
| Read failed | Could not read submodule status, with Git’s own reason and a Retry button. |
A failed read never leaves the previous cards on screen. They describe a repository Zide can no longer confirm, and a stale row presented as current is worse than no row. Retry brings them back once the read succeeds.
Read a submodule’s status
Each submodule card shows its path, then one compact row for where it comes from and which branch it follows. Every card also reserves one status-summary line. A healthy submodule says No issues; a submodule that needs attention names the most important status there. Each submodule counted in the file browser’s Submodules row shows a warning here, such as Not initialized or Not at the recorded commit. Click the summary to expand the detailed badges and reasons. Because the detail opens only when you ask for it, background refreshes do not repeatedly resize the list.
Inside the expanded detail, every block has the same shape: the badge heads it, and what the badge means is an indented bulleted list underneath. Most badges have one thing to say and show one bullet. Update disabled regularly has several, because clearing one reason only reveals the next.
The metadata row shows the submodule’s URL followed by its branch. The URL is the identity: Git records a submodule in .gitmodules by path and URL, and the remote only comes into being when the submodule is cloned. Long URLs collapse from the left so the organisation and repository stay readable, and the full value is on hover or via Copy Remote URL in the row’s menu. A matching conventional origin remote is omitted; a matching remote with another name is shown in parentheses beside the URL.
The branch is the one set by submodule.<name>.branch. When none is set, Zide shows the remote’s default branch with (default). If no clone or remote default is available, it says branch unknown and the tooltip explains why. A plain update is unaffected: it checks out the commit your repository records rather than a branch.
Your repository does not track a submodule’s branch. It records one exact commit. Git calls the entry that holds it a gitlink, and its own documentation calls the commit “the commit recorded in the superproject”; Zide shortens that to the recorded commit and uses it throughout.
A submodule has state on three independent axes, and a badge belongs to exactly one of them. This is why several badges that look contradictory can all be correct at the same time.
| Axis | Compares | Answers |
|---|---|---|
| Working tree | the submodule’s files against its own last commit | Have I edited anything in here? |
| Checkout | the submodule’s checked-out commit against the commit your repository records for it | Is it where this project says it should be? |
| Remote | the commit your repository records against the last commit you fetched | Has upstream moved on? |
The axes remain independent even when the card does not repeat a routine action as a badge. For example, a checkout can be behind while tracked files are modified; in that state Update disabled names the local changes, while the disabled Update tooltip agrees. Zide does not add a second “behind” badge for an action it will not run.
Zide follows one rule here:
- A badge means you have something to sort out. It names what to do, not what Git internally is.
- A highlighted button means one click fixes it, and its tooltip says what, and why.
- Nothing means nothing to do.
The Update button follows the same idea and has two states:
| State | Meaning |
|---|---|
| Highlighted | Press it. This is the routine action for this row. |
| Disabled | Zide will not run an update in this state. Hover it: the tooltip gives the reason. |
A checkout that is ahead of the recorded commit, or diverged from it, is the one case that sits between those two states. The button runs, but nothing happens until you answer for the commits it would leave behind. See Update anyway.
No row shows both a badge and a highlighted button for the same reason. A submodule that is simply behind the commit your project records gets no badge at all: the Update button beside it is highlighted instead, and its tooltip carries the commit count. Repeating that button as text is what made an ordinary repository look like a list of problems.
A healthy submodule therefore says No issues in the same stable slot an exceptional state uses. That is not the same as “could not tell”: every state Zide could not determine has a status of its own.
The collapsed card shows no commit hashes. The counts say what you need to know, and a hash you cannot act on is 7 characters of noise in a scan column.
Expanding a status where a commit is actually at stake is different. Review needed shows the two commits the decision turns on, each as a normal clickable hash that opens its commit tab in the submodule’s own history.
| Label | Is |
|---|---|
| Local commits at risk | The commits only this checkout has, and the commit they end at. |
| Update target | The commit recorded by the superproject, which is where an update would move to. |
They are not labelled Current and New: current is ambiguous between the checkout and the gitlink, and the recorded commit is often the older of the two. Below them, View diff opens the comparison between the two in the submodule’s repository.
No other status renders this block. Evidence appears where it answers the status being explained, not as a panel every card fills in. To look at the recorded commit on any row, use Show Recorded Commit in the row’s menu.
Last operation notices
When an Init, Update, or Fetch report includes an error for a submodule, its card keeps a Last operation failed or Last operation incomplete badge with the reported message beneath it. The notice stays on the card after the toast disappears. The next selected run of the same operation for that submodule clears the notice when its report contains no error.
A Pull from the project header leaves the same notices. It reconciles submodules without this tab being open, so the rows it skipped or failed on are marked here the next time you look, and a later pull that puts a row right clears its notice.
Notices belong to the repository, not to this tab, so closing it and opening it again shows the same unresolved results. Each notice names its own operation, so a successful pull never erases a failed fetch.
A notice clears when the same operation succeeds again for that submodule. Running a different one does not: repairing a URL says nothing about whether a fetch would now work. A notice also goes when the submodule it describes does, either because you removed it or because you deinitialized it, since neither leaves anything to run the operation against. They last for the session in any case; restarting Zide clears them, and re-reading the repository is how the durable facts come back.
Working tree
| Badge | Meaning |
|---|---|
| Uncommitted changes | This submodule has uncommitted changes. Shown on its own only when nothing is stopping an update; when it is what stops one, it appears as a reason under Update disabled. |
Untracked files do not get a badge because they do not normally block an update. They matter only to Deinitialize, which removes the working copy and therefore refuses to discard them.
Untracked files are safe to leave: Git carries them across a checkout untouched. The single exception is a file sitting at a path the commit being checked out needs, and Git refuses that one before changing anything, naming the file. Zide passes that message through rather than paraphrasing it.
Uncommitted changes to tracked files are treated more cautiously than Git itself would. Git carries a modification across unless the checkout touches that exact file; Zide skips the submodule either way, because a bulk update that silently moved a working copy with work in it is a worse surprise than one that skips and tells you.
Checkout
| Badge | Meaning |
|---|---|
| Review needed | The checkout is ahead of the recorded commit or has diverged from it. The commits are only in this checkout, so an update would leave them reachable through the submodule’s reflog and nowhere else. Expand it for the two commits, and see Update anyway. |
| Checkout unknown | The comparison could not be made at all. The reason under the badge says why. |
| On branch name | This submodule has a branch checked out, not a detached HEAD, and something else decides what happens. When the branch stops the update, it is listed under Update disabled instead. |
A detached HEAD is the normal state for a submodule, so an attached branch is always worth flagging. With the default checkout procedure an update would detach it, so Zide skips the submodule and leaves your branch alone. If there is nothing to update, nothing is refused and the sentence says so rather than sounding an alarm.
submodule.<name>.update = merge or rebase exist precisely to move an attached branch forward, so those proceed and the branch stays attached.
The badge reads On a branch, without a name, when Git cannot give one: an unborn branch, or a branch name that is not valid UTF-8.
Remote
Remote status comes from refs/remotes/<remote>/<branch>, a ref in your own clone. It is only as fresh as your last fetch, so every remote sentence ends by saying how old the answer is, and hovering the branch on the card’s second line tells you the same thing when there is no remote badge to carry it. Use Fetch All to bring it up to date.
| Badge | Meaning |
|---|---|
| Newer version available | The remote of this submodule has commits that your project does not use. This tab has no button for it. The reason gives the route, naming the ref and the operation: open the submodule and either pull or check out origin/main there, then stage the submodule in your own repository and commit it. See below for which of the two it tells you. |
| Push submodule first | The remote of this submodule does not have the recorded commit. See below. |
| Diverged from remote | Both have moved apart, so the remote does not have the recorded commit either. The consequence is the same as Push submodule first, but a plain push is rejected: merge the remote into the branch that holds the commit first. No commit count is shown, because there is no fast-forward between them. |
| Fetch | Your clone lacks the remote-tracking branch or one of the commits needed for the comparison. The reason says which one. Fetch the submodule from the row’s menu; if a branch is still missing, check that it exists on the remote. |
| Nothing to compare | Zide has no remote, or no branch, to compare against. A fetch does not help. You must configure one. |
| Remote unknown | The comparison failed for another reason. The reason gives Git’s own words. |
Pull, or check out?
It depends on how the submodule’s HEAD sits, and the card works it out for you.
After an ordinary Update a submodule is left detached at the recorded commit, not on a branch. git pull refuses on a detached HEAD, so the route is a checkout of the remote-tracking ref, which leaves the submodule detached at the newer commit exactly as an update would have. When you have a branch checked out that follows the same ref, the card says pull instead, because a checkout there would throw the branch away. A branch that is not the compared ref gets the checkout route.
Either way the commits are already in your clone. The comparison is made against refs/remotes/<remote>/<branch>, a local ref, so the move itself needs no network.
The trap this badge exists for
It warns about the commonest way a submodule setup breaks: a submodule commit that exists only on your machine, with your project’s pointer already moved to it. Pushing a project that has submodules below is what stops that reaching a remote, and explains the failure it prevents.
The badge uses information from your last fetch. It can warn that a recorded submodule commit may need to be pushed. It cannot confirm that the commit is available on the remote now. See Not sure is treated as not safe.
Fetch and Nothing to compare are separate badges because their remedies are opposite: a missing remote-tracking branch is fixed by fetching, and having no remote at all is not.
Zide compares against origin when it exists, and otherwise against the submodule’s only remote. It compares against the branch configured for the submodule in .gitmodules, falling back to the remote’s default branch when .gitmodules configures none.
No working copy
git submodule status prints -<sha> for every submodule without a working copy, so it cannot tell these apart. They behave differently, and two of them are faults worth acting on.
| Badge | Meaning |
|---|---|
| (no badge) | This submodule has never been initialized here. The Initialize button is the highlighted action on the row, so there is nothing to add. |
| No recorded commit | .gitmodules declares this path, but the project records no submodule commit there. Resolve… opens a compact summary of the submodule and offers Add Submodule, with the configured source and exact destination prefilled, or Remove Submodule. |
| Deinitialized | The submodule is deinitialized, by git submodule deinit or by Zide’s own Deinitialize action. Updates skip it. Only the row’s own Initialize button clones it again. |
| Not checked out here | Git has this submodule registered, but this worktree has no checkout of it. Initialize restores it. This is the usual state of a new linked worktree. It is not a fault. |
| Outside sparse checkout | Updates skip it, and Initialize is disabled. Add its folder to the sparse checkout to initialize it. |
| Needs attention | Either a working copy with no Git directory, or a directory that Git cannot open. The reason says which one. No button in this tab corrects either. |
| Update disabled | An update will not run. The list gives every reason, not just the first, because clearing one only reveals the next. The reasons are: uncommitted changes to tracked files (a nested submodule sitting at a different commit is not one, because an update does not descend into it); a checked-out branch an update would detach; submodule.<name>.update set to none or to a command; submodule.<name>.active false; or no submodule.active pattern covering the path. |
| Repair URL | Two places that should name the same repository disagree. Use the highlighted Repair button. See below. |
Each reason says for itself where the fix is. A reason that starts The Git configuration sets… is a setting someone chose, and you change it in the configuration; a reason that starts This submodule has… is the submodule’s current state, and you clear it by working in the submodule. Because every bullet carries that on its own, a list can mix the two and still be read in any order.
Zide names the setting, so you can find it. It does not tell you which file the value came from: git config reports that inconsistently, and an included file names itself rather than the one you would edit. Repository Config answers that question properly.
One thing it does say is whether a setting is the project’s or yours. A value in .gitmodules is committed, so changing it changes it for everyone who clones the repository, and the reason adds “this is the project’s setting” when that is the case.
Not every setting can be shared. Git reads only path, url, branch, update, ignore, shallow and fetchRecurseSubmodules from .gitmodules. submodule.<name>.active is not among them, so making a submodule inactive is always a local choice and never affects anyone else.
submodule.<name>.update can also be ! followed by a shell command, which Git runs instead of checking a commit out. It is rare, and Zide never runs it for you. The card says the setting is there but does not print the command: it can be a multi-line script, and a truncated piece of one tells you nothing.
Short values that Zide does show from a config file, such as a submodule.active pattern, appear boxed as a small monospace chip. .gitmodules is written by whoever wrote the repository, and a value rendered as ordinary prose could be crafted to read as though Zide wrote it. The box keeps a value looking like a value.
The three places a submodule URL lives
| Where | Used for | |
|---|---|---|
| 1 | .gitmodules | Committed and shared. What your project says. |
| 2 | your .git/config | Cloning the submodule when you initialize it. |
| 3 | the submodule’s own remote.origin.url | Any fetch that runs inside the submodule, which is how Zide fetches. |
They normally all name the same repository. Two badges appear when they do not. The row shows a highlighted Repair button that fixes both, because git submodule sync rewrites 2 and 3 from 1. Its tooltip says Copy the project URL from .gitmodules into this submodule’s local Git configuration.
Repair URL is 1 against 2. Git copies .gitmodules into your .git/config once, when the submodule is initialized, and never again. So when someone repoints a submodule and you pull their change, your clone keeps cloning from the old repository. Zide shows the URL Git will really use and names both.
Submodules declared with a relative URL such as ../lib never show this badge. Git resolves those against your repository’s own remote as it copies them across, so the two forms differ as text on every such submodule while meaning exactly the same thing.
The same Repair URL badge also covers 2 against 3. The usual way to reach it is repointing a submodule at your own fork for local work and forgetting. It matters because it is 3, not 2, that decides where a fetch goes. When the two agree, the card shows the remote’s name in brackets after the URL; when they do not, that bracket disappears and the explanation names both locations.
Pushing a project that has submodules
This is the commonest way a submodule setup breaks, and it always breaks for someone other than the person who caused it.
You commit inside a submodule, bump the pointer in your project, and push the project, forgetting to push the submodule. Everything keeps working for you. Everyone who pulls gets:
fatal: remote error: upload-pack: not our ref 7c4dbd0...fatal: Fetched in submodule path 'lib', but it did not contain 7c4dbd0...They cannot fix it: the commit exists only on your machine.
Zide checks submodule commits before pushing the project to any destination. This check applies to pushes from the project header, branch publishing, the Publish Folder wizard, and Zide Assist. The check can contact submodule remotes. If the project has several push URLs, all checks finish before Zide pushes to any of them.
What it checks
Zide works out which submodule commits your outgoing commits record a pointer to, including one that only an intermediate commit in the push points at. A commit you bumped and then reverted still ships in the middle of the range, and anyone who checks that commit out needs it.
It follows every branch the push would publish, not just the one you are on. git push origin with no branch named pushes whatever your configuration says, and under push.default = matching that is several branches at once; each is checked against the remote branch it lands on.
It also follows submodules inside submodules. If the commit you are publishing a pointer to itself moves a nested submodule, a recursive clone needs that commit too, so it is checked the same way. An unchanged nested pointer costs nothing.
Zide fetches each required submodule’s remote to check whether its commits are available. If the remote differs from the repository named in .gitmodules, Zide stops and reports the mismatch. This can happen when your checkout uses a fork or a URL change has not been synced. The tab’s badges use information from your last fetch. A new fetch checks for later changes, including commits removed by a force push.
The submodule authenticates with its own account, not the project’s, when it has one bound. A private library inside a public project is the ordinary case.
When the push stops
If every required commit is already on its remote, the push runs normally and the console says how many were checked.
If a commit is not on its remote, or Zide could not find out, the push stops and a dialog opens. The title depends on whether Zide can push the submodules for you:
- Push this submodule too? or Push these submodules too? when it can.
- Cannot push this project when it cannot.
The dialog then says why the push stopped. If Zide confirms that all blocked commits are unavailable, it warns that pushing now would break the project for everyone else, because they cannot fetch those commits. If Zide could not check a blocked commit, it says it could not confirm that every required commit is available.
A separate line reports whether anything was pushed. It usually reads Nothing has been pushed yet. If earlier submodule pushes succeeded, it names the submodule or gives a count. If Zide could not confirm them, it warns that some pushes may already have reached their remotes.
Below that is one card per submodule. Each card shows the destination URL, the commits, and the branch to push when there is one.
A push can include several recorded commits for the same submodule. Each must be available on the remote so other people can check out intermediate project commits. Zide lists them together on the submodule’s card. Commits on the same branch need one push. Commits on different branches need separate pushes. Each commit row shows the branch to push or the reason Zide cannot push it.
A remote configured with several push URLs lists all of them in full, because approving sends to all of them, and this dialog’s job includes disclosing every destination. Eight submodules across five hosts appear in one dialog, not eight: this is a single decision about the whole set.
Two actions:
| Action | What it does |
|---|---|
| Continue | Pushes the branch shown for each commit to its own submodule remote, confirms the commit landed, and only then pushes the project. It pushes the branch, so any other commits on it that the remote does not have go too. Any failure stops before the project is pushed. |
| Cancel | No further push happens, and the project is not pushed. If a publication had already run and failed part way, the line above the cards names what already reached a remote. |
Cancel has keyboard focus when the dialog opens with a Continue button.
There is no “push anyway”. Pushing the commit, changing the pointer, and stopping are the three outcomes that leave a repository other people can clone.
Continue appears only when the push would actually work, and only when it would work for every blocked submodule: one that cannot be pushed withholds it from the whole set, since publishing some and failing on the rest leaves the project unpushed anyway. When some of the listed submodules could be pushed and others could not, a line above the cards says so, because the healthy cards carry no reason of their own and the rule that withheld Continue belongs to the set. If the submodule is not checked out here, if the commit is not in your clone either, or if the branch that holds it has diverged from its own remote, the dialog offers Cancel and Show in Submodules instead, and each commit row names its own reason, in place of the branch it would otherwise have carried.
The dialog does not try to teach the repair, and does not spend a sentence pointing at its own button either. It says why the push stopped; Show in Submodules does the rest, opening the tab that explains the fix per state on a card with its own Open button. Show in Submodules opens this tab, in the project the push came from, on the rows the push stopped on. Neither button pushes anything.
Not sure is treated as not safe
Zide stops the push if it cannot verify a required commit. This can happen if a fetch fails, credentials are missing, or history is incomplete. It also stops if the remote is ambiguous, a push refspec does not resolve to a branch, or the push contains more than 1,000 outgoing commits. Zide reports that it could not verify availability.
Which branch gets pushed
Zide never invents a branch. It pushes an existing branch in the submodule that already contains the required commit, in this order: the branch .gitmodules records for that submodule, then the submodule’s own checked-out branch, then the only local branch that contains the commit.
Zide stops if no local branch contains the required commit. It also stops if several branches contain it but neither the configured branch nor the checked-out branch does.
Zide checks whether the remote branch can fast-forward to the selected branch. If it cannot, Zide stops and shows the reason, such as main has diverged from origin/main. If the remote branch does not exist, pushing creates it.
That comparison reads the remote-tracking ref, which records where the remote stood at your last fetch. Where a submodule remote pushes can be somewhere else: a pushurl, a second fetch URL, or a pushInsteadOf rewrite all send the push to a URL nothing was compared against, and several destinations receive it inside a single Git operation. Zide will not claim a fast-forward it cannot check, so it withholds Continue for that submodule and says why on the commit row. Pushing that submodule yourself still works.
The card shows the chosen branch before you approve anything.
Doing it automatically
Git’s own push.recurseSubmodules is honored, and it is the only way a plain push publishes anything without asking:
| Value | What Zide does |
|---|---|
unset, no, or check | Validate only. An unpublished commit pauses the push. This is the default whatever Git is set to, because publishing to another repository is not something to do silently. |
on-demand | Publish the required commits, then push the project. The report names the configuration file the setting came from. |
only | Publish the required commits and do not push the project. That is what the setting means, so it is reported as a success. |
Set it in Repository Config if you want it. Zide Assist never publishes on an approval alone: approving a push is you agreeing to push that repository, not to send commits into a different one, so only this setting authorizes it there.
Submodule actions
In the submodule row, click Open to open the submodule as a separate project tab. Click Update to update that submodule only. A submodule that has not been cloned here shows Initialize, which likewise initializes only that submodule. When nested declarations exist, the row menu offers the explicit alternate action Update Recursively or Initialize Recursively.
If .gitmodules lists a submodule but the project records no commit for it, the row shows No recorded commit and Resolve…. Select Resolve… to open the Resolve Submodule dialog. It shows the path, source, and any branch configured in .gitmodules. Choose Add Submodule to review the source and record a commit at that path. Choose Remove Submodule to remove the unused entry. Removal stages the metadata change for review.
Every row has a menu, including one that has never been initialized. Actions that need a working copy stay visible and explain why they are unavailable rather than disappearing.
| Action | Description |
|---|---|
| Show in File Browser | Reveals the submodule’s directory in the File Browser. It remains visible but is disabled when no working directory exists. |
| Show Recorded Commit | Opens the commit your repository records, in the submodule’s own history. Unavailable when your repository records no commit for it, or when that commit is not in this clone yet. |
| Update Recursively / Initialize Recursively | Updates or initializes this row and its nested submodules. Shown when nested work is possible; the card’s visible Update or Initialize remains scoped to the row itself. |
| Fetch from remote | Updates the submodule’s remote-tracking branches. Needs a working copy. |
| Copy Remote URL | Copies the submodule’s full URL, which the card collapses to fit. |
| Change URL | Updates the submodule remote URL. Available before the submodule is cloned, because it edits .gitmodules. |
| Change tracked branch | Opens Choose How to Track a Branch. Choose Follow remote default, Match parent project branch, a specific branch from the remote, or a branch from the last fetch when the remote cannot be reached. Choose Other branch… to type a branch that is not listed. Follow remote default removes submodule.<name>.branch, so Zide follows the remote’s default branch. Also available before the submodule is cloned. |
Repair remote URL from .gitmodules | Syncs the remote URL with the .gitmodules file. This menu item appears only when the card does not currently need repair. It needs the submodule to be registered, so it is unavailable until it is initialized. |
| Deinitialize | Removes the working copy and unregisters the submodule. Git keeps its repository in .git/modules, so Initialize can restore it without a new clone. Nothing is committed and no remote is contacted. Zide disables this action if the submodule has uncommitted changes, untracked files, or a nested submodule at a different commit than recorded. It also disables the action if this submodule is at a different commit than the project records, or its working tree cannot be read. The tooltip explains the reason. |
| Remove submodule | Removes the submodule. For an incomplete declaration with no working directory or recorded commit, it removes only the .gitmodules entry and stages that change. |