A faster git status when working in repositories with many submodules.
Running git status at the top level of a repository with many submodules can take tens of seconds to minutes. This performance
issue is amplified on Windows, which is hindered by Windows Defender as well as an inefficient lstat implementation.
There are a few potential workarounds you should try before using this tool, including
- Running
git gc - Set
ignore = dirtyas needed in your.gitmodulesfile (Usually unacceptable for anything besides vendored dependencies) - Don't use submodules
- Don't use Windows ;)
SubSpy provides a solution to this issue by placing recursive filesystem watches on your repository's .git folder, .gitmodules
file, and all submodule directories. The status for all submodules is cached by an initial indexing operation and updated
any time a change is detected in one of these locations. For sufficiently many submodules, subspy status is typically
100-1000x faster than git status on Windows and 67-360x faster on Linux, with the gap widening as working tree churn increases.
Installing subspy requires the Rust toolchain. Note that the project's current Minimum Supported Rust Version (MSRV) is 1.87.0.
> git clone https://github.com/WillLillis/subspy
> cd subspy
> cargo install --path . --lockedThis installs two binaries: subspy (the standalone CLI) and subspy-git (the shim covered below).
Use the subspy binary directly to gather statuses faster than git or to gather information about the submodules of a repo.
~/very_large_project/ > subspy --help
Usage: subspy <COMMAND>
Commands:
start Start a watch server on a git project [aliases: watch, w]
status Display the status of a watched git project [aliases: st, s]
stop Shutdown a watch server
reindex Reindex a watch server [aliases: re, r]
debug Dump the internal state of the watch server [aliases: dbg, d]
list List submodule metadata [aliases: ls, l]
prompt Submodule status summary for shell prompt integration
Options:
-h, --help Print help
~/very_large_project/ > subspy status # spawns a watch server if needed
-- top level status here --
~/very_large_project/ > subspy status some/subdirectory/
-- other status here --
~/very_large_project/ > subspy stop # Shutdown the watch serverThe subspy prompt subcommand outputs submodule status counts for use in shell prompts. It connects to a running watch
server and returns immediately. If no server is running, it spawns one in the background and produces no output until the
next invocation.
The default output is space-separated fields: <dirty> <staged> <new_commits> <clean> <total>. A custom format string can
be provided with -f:
# Bash / Zsh
subspy_prompt() {
local s
s=$(subspy prompt 2>/dev/null) && [ -n "$s" ] && echo " [$s]"
}
PS1='...\$(subspy_prompt)...'
# With a custom format
subspy_prompt() {
local s
s=$(subspy prompt -f '{dirty}!{new_commits}↑' 2>/dev/null) && [ -n "$s" ] && echo " $s"
}For Starship, use a custom command:
[custom.subspy]
command = "subspy prompt -f '{dirty}!{staged}+{new_commits}↑'"
when = "subspy prompt -f '{dirty}!{staged}+{new_commits}↑'"
format = "[$output]($style) "The subspy-git binary is a git shim that can be used as a drop-in replacement for git. Any git status invocation the shim
can fully service is handled by subspy; every other invocation (fetch, log, diff, etc.) and any status call with a flag
we don't recognize is forwarded to the system git unchanged. This is especially useful in GUI git tools that regularly issue
git status commands.
Git Extensions calls git status --porcelain ... before many operations (checkout,
merge, opening the commit dialog) and on a background timer. In repositories with many submodules this check can take
tens of seconds, blocking the UI.
To use it: open Git Extensions -> Settings -> Git -> Path to git, set the path to the absolute path of subspy-git
(or subspy-git.exe on Windows), and click OK.
Linux / Mono caveat: Git Extensions resets the "Path to git" setting on every startup on non-Windows builds (
CheckSettingsLogic.cs#L176-L177hardcodes"git"), so you'll need to re-enter it each time you launch the application. The Windows build persists the value to the registry and is unaffected.
SourceTree also calls git status many times as part of its regular operation.
It requires a path to a full git install directory (it resolves cmd/git.exe and a number of supporting files at fixed relative
paths within it), not to a single binary, so the shim can't be slotted in. SourceTree is not supported.
The first two subspy measurements show cold and warm performance: the first starts a new watch server and waits for
initial indexing, the second connects to the already-running server.
git status must stat every tracked file to detect modifications, so its runtime scales with the size of the working tree
and how recently files were touched. Operations like branch switches, bulk reformatting, or build systems that update
timestamps force git to re-examine every file. subspy status reads from a cache maintained by filesystem watchers,
so its runtime is constant regardless of working tree churn. The touch measurements below simulate this worst case by
updating the timestamp on every tracked file.
- Windows:
Subspy is most useful on Windows, where git is the slowest. Here's a comparison between subspy status and git status
on a private repo with >200 submodules, running on Windows 11 with git bash. Note that spawning a process that immediately
exits already takes ~80ms on the machine this measurement was performed on.
~/very_large_project/ > time git status
real 0m11.952s
user 0m0.015s
sys 0m0.000s
~/very_large_project/ > time subspy status # Spawns a new watch server
real 0m0.463s
user 0m0.000s
sys 0m0.000s
~/very_large_project/ > time subspy status # Connects to previously spawned server
real 0m0.102s
user 0m0.000s
sys 0m0.000s
~/very_large_project/ > find . -not -path './.git/*' -exec touch {} + # simulate heavy working tree churn
~/very_large_project/ > time git status
real 1m56.328s
user 0m0.000s
sys 0m0.000s
~/very_large_project/ > time subspy status
real 0m0.107s
user 0m0.000s
sys 0m0.015s
- Linux:
Git's performance is typically acceptable on Linux platforms. Testing on boost with 172 submodules in a clean working tree, we see a smaller but still noticeable performance difference:
~/boost/ > time git status
git status 0.08s user 0.19s system 103% cpu 0.267 total
~/boost/ > time subspy status # Spawns a new watch server
subspy status 0.00s user 0.00s system 2% cpu 0.222 total
~/boost/ > time subspy status # Connects to previously spawned server
subspy status 0.00s user 0.00s system 94% cpu 0.004 total
~/boost/ > find . -not -path './.git/*' -exec touch {} + # simulate heavy working tree churn
~/boost/ > time git status
git status 1.08s user 0.38s system 100% cpu 1.442 total
~/boost/ > time subspy status
subspy status 0.00s user 0.00s system 94% cpu 0.004 total- Launching a watch server for a nested submodule (submodules which contain submodules of their own) is not supported.
subspy startmust be run from the top-level of the repository. - On Linux, each watch server consumes inotify watches. For very large repositories or many concurrent servers, you may
need to increase the system limit (e.g.
sudo sysctl fs.inotify.max_user_watches=<value>). - On Windows, AF_UNIX sockets are used for IPC, which requires Windows 10 version 1809 (October 2018 Update) or Windows Server 2019 or later.
- crates.io releases if desired