git clone, written from scratch. No dependencies — not even git.
nanoclone speaks Git's wire protocol directly: it negotiates over Smart HTTP v2,
downloads a packfile, resolves the delta chains inside it, verifies every object
against its SHA-1, and writes a working tree plus a valid .git index.
The result is a repository that real Git considers clean:
$ python -m nanoclone clone https://github.com/psf/requests
Cloning into 'requests'...
branch main -> ...
received pack: ~13232.5 KiB in ~2.8s
resolved ~25624 objects
checked out 130 files
$ cd requests && git status --porcelain # no output: Git sees nothing to do
$ git fsck --strict # every object connects and hashes outOne honest footnote about that last command: on psf/requests it prints
badTimezone: invalid author/committer line for commit 5e6ecdad. That is not
this program's doing — a real git clone of the same repository produces the
identical complaint, because one commit in that history was written with a
malformed timezone years ago. Every object nanoclone wrote is intact and
correctly hashed; the defect it faithfully reproduced was already there.
Nothing here shells out to git. The imports are hashlib, zlib, struct,
urllib, os, pathlib, argparse, sys, time, plus dataclasses and
typing — all standard library, nothing installed.
Git is usually a black box. Cloning a repository looks like one command, but underneath it is four distinct formats stacked on top of each other, and every one of them is small enough to read in an afternoon:
| Layer | What it is |
|---|---|
| pkt-line | A 4-byte hex length prefix in front of every message. Three lengths are reserved as control packets. |
| Protocol v2 | A request/response RPC over HTTP. ls-refs asks what exists, fetch asks for objects. |
| Packfile | Every object, zlib-deflated. Most are stored as deltas against another object. |
| Object model | Commits point at trees, trees point at blobs. Each object's name is the SHA-1 of its contents. |
That last property is what makes this project verifiable rather than merely plausible: if a single bit of our parsing is wrong, the hash changes, and the object id we compute stops matching the one Git computes. Correctness is not a matter of opinion here.
Requires Python 3.10 or newer. There is nothing to install:
$ git clone https://github.com/Wasserpuncher/nanoclone && cd nanoclone
$ python -m nanoclone --help# Clone the remote's default branch
$ python -m nanoclone clone https://github.com/octocat/Hello-World
# Pick a branch and a destination
$ python -m nanoclone clone https://github.com/octocat/Hello-World hi -b testThe two above are marked not-to-be-run, and only because they would leave a
Hello-World/ and a hi/ behind in whatever directory you checked this out
into. Everything below is run on every CI build.
# List the refs a remote advertises, without downloading anything
$ python -m nanoclone ls-remote https://github.com/octocat/Hello-World
7fd1a60b01f91b314f59955a4e4d4e80d8edf11d HEAD -> refs/heads/master
7fd1a60b01f91b314f59955a4e4d4e80d8edf11d refs/heads/master
# Take apart a packfile: types, sizes, offsets, and which object each delta patches
$ python -m nanoclone verify-pack .git/objects/pack/pack-*.pack- Discover.
GET /info/refs?service=git-upload-packwith the headerGit-Protocol: version=2. The server answers with its capabilities. ls-refs. APOSTasking which refs exist. The reply tells us the commit id of each branch, and — viasymref-target— which branchHEADpoints at.fetch. APOSTsayingwant <commit>anddone. Sendingdoneimmediately skips negotiation, so the server replies with one complete packfile. Its bytes arrive multiplexed on sideband channel 1, with progress on channel 2 and errors on channel 3.- Unpack. The packfile is
PACK, a version, an object count, then that many deflated objects, then a SHA-1 checksum over everything before it. - Resolve deltas. Two of the seven object types are not objects at all but
patches:
OFS_DELTAnames its base by a backwards offset,REF_DELTAby the base's SHA-1. A delta is a tiny program of copy this range from the base and insert these literal bytes. Because aREF_DELTAmay point at a base stored later in the pack, resolution runs as a fixpoint rather than a single pass. - Check out. Walk the commit's tree, write blobs with the right modes
(including symlinks and the executable bit), and write a
.git/indexthat caches each file'sstatdata — which is what letsgit statusreturn instantly, and what makes it report a clean tree instead of a modified one.
Claiming "it works" is cheap. The test suite (python -m unittest discover -s tests)
makes Git itself the judge — 39 tests, no mocks of the format:
- Differential tests against
git. A fixture repository is built with the realgitbinary and repacked twice — once forcingOFS_DELTA, once forcingREF_DELTA. nanoclone parses the resulting packfiles, and every object id, type and byte of content is compared againstgit cat-file --batch-all-objects. Agreeing with Git on all 42 SHA-1 hashes means agreeing on every byte. verify-packcross-check. Our per-offset resolution is compared togit verify-pack -v, entry by entry, including which entries are deltas.- Git audits our checkout. We write a repository by hand, then ask Git:
git fsck --strictmust pass,git status --porcelainmust be empty, andgit rev-parse HEADmust agree. Symlinks, the executable bit, empty files and UTF-8 filenames are checked explicitly. - A real clone. Opt-in (
NANOCLONE_NETWORK_TESTS=1) tests clone a public repository over the network and assert the resulting tree hash equals the one produced bygit cloneof the same URL. - Adversarial input. Corrupt checksums, truncated streams, reserved pkt-line
length
0003, the reserved delta opcode0x00, copy instructions that run past the end of their base, and unresolvable deltas are all rejected with a specific error rather than a wrong answer.
Measured on the 13.8 MB packfile that git clone fetches for psf/requests
(26 818 objects, 17 560 of them deltas), Python 3.14, warm page cache. That is
more objects than the 25 624 in the transcript above, and deliberately so: this
row measures the full clone — every ref — while nanoclone clone asks for one
branch. Both numbers drift upward as the repository grows.
| Phase | Time |
|---|---|
| Inflate + parse every object | 0.28 s |
| Resolve all 17 560 deltas | 1.16 s |
| Write 26 818 loose objects | 7.27 s |
A full clone of that repository, network included, takes about 14 seconds.
An earlier version spent 63 seconds in the parse phase. The cause was one
line: handing zlib buf[pos:] copies the entire remainder of the packfile, once
per object, which is quadratic. Feeding fixed-size chunks of a memoryview
instead made that phase 225× faster. It is documented in packfile.py so the
next reader doesn't reintroduce it.
These are deliberate — the goal was to understand the protocol, not to replace Git:
- HTTP(S) only. No SSH, no
git://.scp-style URLs are rewritten to HTTPS. - Protocol v2 required. Every modern host (GitHub, GitLab, Gitea, Codeberg) speaks it; a v0-only server is rejected with a clear message.
- Clone only. No push, no incremental fetch, no negotiation with
havelines, no shallow or partial clones. - SHA-1 repositories only. Experimental SHA-256 repositories are not supported.
- Objects are written loose, not as a packfile with an index. This is simpler
to read, but it costs disk: the
psf/requestsclone occupies 124 MB in.git, where real Git needs 15 MB. It also means no.idxfile is produced. - No
.gitattributesprocessing: no CRLF conversion, no clean/smudge filters, no Git LFS. Submodule commits are recorded in the index, but their contents are not fetched.
nanoclone implementiert git clone von Grund auf neu — ausschließlich mit der
Python-Standardbibliothek, ohne jede Abhängigkeit und ohne das git-Binary
aufzurufen. Umgesetzt sind das pkt-line-Format, der Smart-HTTP-Transport
(Protokoll v2), der Packfile-Parser samt Auflösung von OFS- und REF-Deltas sowie
ein Checkout, der einen gültigen Git-Index schreibt.
Der Korrektheitsbeweis liegt in den Tests: Ein Repository wird mit echtem git
gebaut und neu gepackt, nanoclone liest es, und jede einzelne Objekt-ID, jeder
Typ und jedes Byte wird gegen git cat-file verglichen. Anschließend prüft
git selbst unser Ergebnis — git fsck --strict muss bestehen und
git status muss ein sauberes Arbeitsverzeichnis melden.
The implementation follows the official format specifications:
MIT — see LICENSE.