Git as a Package Registry: How and Why

Every major package manager in every popular language has a central registry. npm has npmjs.com. Python has PyPI. Rust has crates.io. Even C++ tools like Conan and vcpkg maintain curated central repositories of packages.

cmod takes a fundamentally different approach: Git is the registry. There is no central server, no account to create, no approval queue to wait in. Your Git repository is your package.

This isn't laziness or a missing feature. It's a deliberate architectural decision with significant advantages for the C++ ecosystem specifically. Here's why.

The Problems with Central Registries

Central registries have served the JavaScript and Python ecosystems well, but they come with structural problems that are particularly acute for C++:

Single point of failure

When npm goes down, the JavaScript ecosystem halts. When PyPI has issues, CI pipelines break worldwide. A central registry is a critical dependency for every project that uses it. For C++ — which runs nuclear power plants, medical devices, and flight control systems — this centralized fragility is unacceptable.

Name squatting and typosquatting

Central registries create a flat namespace where names are first-come, first-served. This leads to name squatting (reserving popular names) and typosquatting attacks (publishing malicious packages with names like lod4sh instead of lodash). The npm ecosystem has been hit by this repeatedly.

Supply chain attacks via account compromise

When identity is tied to a registry account rather than a repository, compromising one account can push malicious code to millions of downstream users. The event-stream and ua-parser-js incidents in npm demonstrate this risk.

Governance and politics

Who decides what gets published? Who moderates? Who pays for the infrastructure? Central registries inevitably become political, with questions about moderation, funding, and control that distract from the technical mission.

Why Git Works as a Registry

Git repositories already have everything a package registry needs:

  • Identity: A Git URL is a globally unique, human-readable identifier. github.com/fmtlib/fmt is unambiguous.
  • Versioning: Git tags map directly to semantic versions. Tag v10.2.1 means version 10.2.1.
  • History: Full version history with commit hashes provides cryptographic identity for every state of the code.
  • Distribution: Git hosting is commoditized. GitHub, GitLab, Bitbucket, Codeberg, self-hosted Gitea — the infrastructure exists and is battle-tested.
  • Authentication: SSH keys and tokens already control access. Private dependencies work with the same authentication you use for your own code.
  • Redundancy: Every clone is a full copy. If GitHub goes down, your local clone still has everything.

How cmod Uses Git

Module identity

In cmod, a module's identity is derived from its Git URL using reverse-domain notation:

github.com/fmtlib/fmt  →  com.github.fmtlib.fmt
gitlab.com/org/lib     →  com.gitlab.org.lib

This eliminates name collisions entirely. There's no race to register names. Your repository URL is your package name.

Version resolution

cmod resolves versions by listing Git tags that match semver patterns:

[dependencies]
"github.com/fmtlib/fmt" = ">=10.0"

cmod fetches the repository's tag list, finds all tags matching the constraint, and selects the best version. The resolved commit hash is written to cmod.lock:

[[dependencies]]
url = "github.com/fmtlib/fmt"
version = "10.2.1"
commit = "a0b8a4e19f..."

Cloning and caching

On first resolution, cmod performs a shallow clone of each dependency. Subsequent builds use the cached clone and only fetch new tags when running cmod update. This means:

  • First build: clones are fetched (fast — shallow clones)
  • Subsequent builds: no network access needed
  • CI with --locked: dependencies come from exact commit hashes, no tag resolution needed

Branch and path dependencies

For development workflows, cmod supports branch pinning and local path dependencies:

[dependencies]
# Pin to a branch (useful during development)
"github.com/user/experimental" = { branch = "feature-x" }

# Local path (for monorepo or co-development)
"../shared-lib" = { path = "../shared-lib" }

Publishing Is Pushing a Tag

In a central registry world, publishing requires:

  1. Create an account
  2. Set up authentication tokens
  3. Run a publish command
  4. Wait for the registry to process and index
  5. Hope nobody squatted your name

In cmod, publishing is:

git tag v1.0.0
git push origin v1.0.0

That's it. Anyone who adds your repository URL as a dependency can now resolve ^1.0.0 and get your tagged release. The cmod publish command automates this:

cmod publish            # Creates and pushes a Git tag
cmod publish --dry-run  # Preview what would happen

Addressing Common Concerns

"How do you discover packages?"

The same way you discover Git repositories: search engines, GitHub search, awesome lists, word of mouth, documentation links. The cmod search command queries GitHub's search API to find modules by name or description. A future community-maintained index could aggregate module metadata without being a single point of failure.

"What about private dependencies?"

Private dependencies work seamlessly because cmod uses the same Git authentication you already have configured. If you can git clone a repository, cmod can resolve it as a dependency. SSH keys, deploy tokens, and credential helpers all work.

"Isn't cloning slow?"

cmod uses shallow clones and caches aggressively. After first resolution, builds require zero network access. In practice, the initial clone of typical C++ libraries takes seconds, not minutes — and it only happens once per dependency per machine.

"What if a repository is deleted?"

This is a real concern, but it applies to central registries too (the left-pad incident proved this). cmod mitigates this with:

  • Lockfiles with commit hashes: Even if tags are moved, the exact commit is pinned
  • Vendor command: cmod vendor copies all dependency source code into your project for fully offline builds
  • Local cache: Once cloned, dependencies persist in the local cache

The Go Precedent

cmod isn't the first tool to use this approach. Go modules use a similar model: module identity is a URL, versions are Git tags, and there's no mandatory central registry. The Go module proxy exists as an optimization layer, not a requirement. This model has worked at massive scale for the Go ecosystem since 2019.

cmod takes this further by adding mandatory lockfiles, cryptographic verification, and the security guarantees that C++'s use in critical infrastructure demands.

Looking Forward

The Git-as-registry model opens up possibilities that central registries can't easily provide:

  • Federated discovery: Community-maintained indexes can aggregate metadata without controlling distribution
  • Mirror flexibility: Teams can mirror dependencies to internal Git servers for air-gapped environments
  • Zero vendor lock-in: Moving from GitHub to GitLab or self-hosted infrastructure requires no ecosystem migration

Git is the most successful version control system ever built. It's already the backbone of open-source collaboration. Using it as a package registry isn't a compromise — it's a recognition that the infrastructure we need already exists.

Get started with cmod and experience Git-native dependency management for yourself.