VersionDrift
Find every out-of-sync Git repo on your machine, without touching your work.
VersionDrift is a safety-first Git checkup for developers with more repositories than they can keep track of. It scans only the local directories you provide, separates safe fast-forwards from local work that must be protected, and records every decision locally.
$ version-drift scan ~/code --fetch
VersionDrift scanned 17 repositories under ~/code
✓ 10 in sync
↓ 3 safe to update
! 2 local work protected
↑ 1 ahead of upstream
↕ 1 diverged
Safe to update
docs 2 commits behind
website 4 commits behind
Protected. VersionDrift will not touch these
client-api dirty_worktree
prototype diverged_from_upstream
Working files changed: 0
Remote data: refreshed now
The example above illustrates the output format. It is not an adoption claim or benchmark.
Install
Install from PyPI:
uv tool install version-drift
# or
pipx install version-drift
For development:
git clone https://github.com/seojoonkim/version-drift.git
cd version-drift
python -m pip install -e .
Run your first Git checkup
version-drift init ~/code ~/work
version-drift inbox --fetch
init validates and saves roots without scanning repositories. inbox reports only repository states that are new, changed, or resolved since the previous checkup. Both scan and inbox remain read-only for working files, the index, local commits, and branches. Without --fetch, they compare against local remote-tracking refs. With --fetch, they first run a non-destructive fetch so the comparison is current.
Why VersionDrift?
A loop that runs git pull everywhere can stop on dirty work, create conflicts, or conceal work behind an automatic stash. VersionDrift takes the opposite approach:
- Discover repositories only inside roots you provide.
- Classify every repository before taking action.
- Protect anything dirty, ahead, diverged, ambiguous, or missing an upstream.
- Fast-forward only repositories proven safe at execution time.
- Record every decision locally as JSONL.
Safety contract
VersionDrift will never automatically:
- reset your working tree
- stash or drop changes
- clean untracked files
- merge or rebase branches
- force-pull or force-push
- guess a missing upstream
sync --apply is allowed only when a repository is clean, tracks an upstream, and is behind-only. VersionDrift checks the state again immediately before running exactly git pull --ff-only; if the snapshot changed, it aborts.
Commands
Scan one or more roots
version-drift scan ~/code ~/work
--fetch: refresh remote-tracking refs first--json: emit machine-readable output--check: return exit code 1 when drift exists--max-depth N: bound discovery depth
Save repeatable default roots:
version-drift init ~/code ~/work
version-drift scan
Root precedence is explicit command-line roots, saved configuration, VERSION_DRIFT_ROOTS, then the current directory.
Show the daily change inbox
version-drift inbox
version-drift inbox --fetch
version-drift inbox --json
The first check reports every non-synced repository as new. Later checks omit unchanged repositories and report only new, changed, and resolved entries.
Explain repository states
version-drift explain
version-drift explain ~/code/project --json
explain reuses the existing local inspection contract and translates each state into a reason, impact, and safe next action. With explicit paths it explains exactly those paths; without paths it discovers repositories under the configured roots. Its JSON schema is version-drift/explain/1.
explain never fetches, changes Git state, records an event, or updates the inbox snapshot. Only behind_clean repositories are marked sync_eligible, using the same predicate as sync.
Read the local decision history
version-drift history
version-drift history ~/code/project --event scan --limit 20
version-drift history --json
history reads the append-only local event trail newest-first. Optional repository paths match only that path and its descendants, --event is repeatable, and --limit 0 keeps all matching events. Its JSON schema is version-drift/history/1 and includes source, filter, malformed-line, matched, and returned counts.
history never invokes Git, fetches, records an event, updates the inbox snapshot, or creates a missing state directory. A torn or malformed event line is counted and skipped, while an unreadable event file fails cleanly without changing it.
Inspect one repository
version-drift inspect ~/code/project --fetch --json
Preview safe synchronization
version-drift sync ~/code
Apply safe fast-forwards
version-drift sync ~/code --apply
Repositories with local work or ambiguous history remain untouched.
JSON and local decision events
Every scan and sync decision is appended to:
macOS: ~/Library/Application Support/VersionDrift/events.jsonl
Linux: ${XDG_STATE_HOME:-~/.local/state}/version-drift/events.jsonl
The state file defaults outside your Git repositories, so recording a checkup does not dirty the repository where you invoked VersionDrift. The current schema is version-drift/1. Choose another state root with --base-dir or VERSION_DRIFT_DIR; explicit state roots retain the legacy .version-drift/events.jsonl suffix for compatibility.
The latest inbox snapshot is written atomically beside the event file as inbox_snapshot.json. It contains local repository paths and Git state, stays on the machine, and is never written into a scanned repository. A corrupt snapshot is preserved and causes a fail-closed error instead of silently resetting the baseline.
Root configuration is stored outside repositories at ~/Library/Application Support/VersionDrift/config.toml on macOS or ${XDG_CONFIG_HOME:-~/.config}/version-drift/config.toml on Linux.
Working files changed is measured from repository HEAD and worktree snapshots taken before and after the command. Read-only inspection uses a temporary copy of the Git index, ignores non-regular untracked files, and records symlink targets without dereferencing them. A read-only scan should report 0; an applied fast-forward reports the files changed by the accepted upstream commits.
VersionDrift sends no telemetry and never uploads repository paths, remotes, or results.
VersionDrift, Gita, and myrepos
Gita and myrepos are strong choices for broad multi-repository management or arbitrary commands. VersionDrift is deliberately narrower: fail-closed diagnosis plus clean fast-forward-only reconciliation.
Choose VersionDrift when you want a read-only first run, a fixed non-destructive policy, apply-time revalidation, machine-readable decisions, and a local audit trail.
MemKraft integration
MemKraft can optionally use the standalone version_drift engine. VersionDrift remains independently installable and owns the version-drift command.
Contributing
Bug reports and focused pull requests are welcome. Safety invariants are part of the public API and cannot be weakened for convenience. See CONTRIBUTING.md.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file version_drift-0.5.0.tar.gz.
File metadata
- Download URL: version_drift-0.5.0.tar.gz
- Upload date:
- Size: 31.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4beab81ce0c83a8e8add1f0a2d439943d6947fbe8724d4c896c47745498fecc8
|
|
| MD5 |
63b894850fbd5e636f107cfefe3d37b4
|
|
| BLAKE2b-256 |
735aeb8a64292cf94ad39a0f4e59295e16503f762b35dd0943b9f72d90ef7ae4
|
Provenance
The following attestation bundles were made for version_drift-0.5.0.tar.gz:
Publisher:
release.yml on seojoonkim/version-drift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
version_drift-0.5.0.tar.gz -
Subject digest:
4beab81ce0c83a8e8add1f0a2d439943d6947fbe8724d4c896c47745498fecc8 - Sigstore transparency entry: 2550921689
- Sigstore integration time:
-
Permalink:
seojoonkim/version-drift@fc09042bc46025a6833a7cd84f7c806a20c3cdc8 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/seojoonkim
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fc09042bc46025a6833a7cd84f7c806a20c3cdc8 -
Trigger Event:
push
-
Statement type:
File details
Details for the file version_drift-0.5.0-py3-none-any.whl.
File metadata
- Download URL: version_drift-0.5.0-py3-none-any.whl
- Upload date:
- Size: 21.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6132ffec5e0da993ffce1219562d3fe2b2e1ad336c45df765c9ebd7f6293a1ff
|
|
| MD5 |
d41f16d9cc548a2d0cd9e0dba457a813
|
|
| BLAKE2b-256 |
54927df1a4ee0e53d14956ada1fc49c174c0da3543a7498a6cd11c404dc633d2
|
Provenance
The following attestation bundles were made for version_drift-0.5.0-py3-none-any.whl:
Publisher:
release.yml on seojoonkim/version-drift
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
version_drift-0.5.0-py3-none-any.whl -
Subject digest:
6132ffec5e0da993ffce1219562d3fe2b2e1ad336c45df765c9ebd7f6293a1ff - Sigstore transparency entry: 2550921784
- Sigstore integration time:
-
Permalink:
seojoonkim/version-drift@fc09042bc46025a6833a7cd84f7c806a20c3cdc8 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/seojoonkim
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fc09042bc46025a6833a7cd84f7c806a20c3cdc8 -
Trigger Event:
push
-
Statement type: