Draftling ships a small but real Git client. It keeps a genuine commit
history on the SD card and exchanges objects with a remote server over
the smart HTTP protocol
(gitprotocol-http):
GET .../info/refs?service=git-upload-pack, POST .../git-upload-pack
and POST .../git-receive-pack, with pkt-line framing and packfile v2
transfer. There is no dependency on any host-specific REST API, so any
standard Git HTTP host works (GitHub, Gitea, Forgejo, GitLab, cgit with
smart HTTP, a bare repo behind git http-backend, ...).
The device is IPv4/IPv6 dual-stack: if the WiFi network hands out a
global IPv6 address (RA/SLAAC, not just the automatic link-local one),
every request to the Git server tries its AAAA record first and
transparently falls back to A (IPv4) if the host has none. A "6"
badge over the WiFi status icon indicates that a global IPv6 address is
active.
/sdcard/
note1.md working tree (the files you edit)
note2.md
.git/ real Git repository
HEAD
refs/heads/<branch>
refs/remotes/origin/<branch>
objects/xx/… loose, zlib-compressed objects
tmp/ transient fetch.pack / push.pack
The object store uses standard loose objects (<type> <size>\0<payload>
deflated with zlib), so the .git directory can be copied onto a
computer and inspected with the real git if needed. zlib is provided
by the miniz implementation in the ESP32 ROM; SHA-1 by Mbed TLS / PSA
Crypto; TLS + HTTP by esp_http_client / esp-tls.
Triggered by Ctrl+G, by the F1 → Git Sync menu entry, or automatically
before deep sleep. The sync task runs these steps in order:
- Advertise – contact the remote and read its branch tip.
- Clone – on the very first sync, if the remote branch exists its full history is fetched and checked out. (History is fetched in full; there is no shallow clone.)
- Commit -- the current
*.md,*.fountainand*.txtfiles in the working directory are written as a tree and committed on top of the local branch tip. If nothing changed, no commit is made. - Fetch – if the remote tip is not already in local history, the
missing commits are fetched (sending
havelines for the local commits so the server can send a minimal packfile). - Integrate
- remote unchanged → nothing to do;
- local unchanged → fast-forward to the remote tip;
- both advanced → rebase: each local commit is replayed on top of the remote tip through a three-way merge.
- Merge & conflicts – the three-way merge is done per file:
-
a file changed on only one side is taken from that side;
-
a file changed identically on both sides is taken as-is;
-
a file changed on both sides gets a diff3 line merge. Cleanly separable edits are merged silently. Overlapping edits are written into the file with conflict markers
<<<<<<< ours (remote) …the remote version… ======= …your local version… >>>>>>> theirs (local)and the commit is still created – conflicts are committed as-is, never lost. The commit message gets a
[draftling] N conflict(s)note. Resolve them by editing the file and syncing again. -
a delete/modify clash keeps the surviving content and marks a conflict.
-
- Checkout – the merged tree is written back to the SD card. The editor reloads the open buffer and the file browser refreshes so you immediately see the merged result.
- Push – the new commits are packed and pushed to the remote with
git-receive-pack. The push is a plain fast-forward update ofrefs/heads/<branch>; if the remote moved again while the sync was running, the push is refused and you are asked to sync once more.
repo_url=https://github.com/user/repo
branch=main
token=ghp_xxxxxxxxxxxx
path=notes/
username=x-access-token
author_name=Jane Doe
author_email=jane@example.com
| Key | Required | Meaning |
|---|---|---|
repo_url |
yes | Repository URL, .git suffix optional. |
token |
yes | HTTP Basic password / access token. |
branch |
no | Branch to sync (default main). |
path |
no | Map the working tree onto this sub-directory of the repo. Intermediate trees are created/preserved; the rest of the repo is left untouched. |
username |
no | HTTP Basic username (default x-access-token). |
author_name, author_email |
no | Identity written into commits (default Draftling <draftling@localhost>). |
Wall-clock time for commit timestamps is taken from a best-effort SNTP query on the first sync after boot; if that fails, timestamps are floored at 2025-01-01. Git orders history by the commit graph, not by timestamp, so an inaccurate clock does not corrupt anything.
The file browser's Del / Alt+D command only deletes a file whose
current content is already on the Git server: it must be identical to
the file's version in the last commit that a sync pushed to (or
fetched from) the server. A new file, or one edited since the last
successful sync, has to be synced first (Ctrl+G). The check is made
on the device, without contacting the server. The next sync then
commits the deletion, and the file stays available in the repository
history.
Alt+I in the editor or the file browser shows the file information
page. Its "Git sync" section compares the file with its version in the
last commit that a sync pushed to (or fetched from) the server, on the
device and without contacting the server: "same as on the server",
"not on the server yet", or "changed since last sync" with the number
of lines added and deleted. In the editor the comparison uses the
text being edited, unsaved changes included.
This is deliberately a minimal client:
- One branch, no tags, no submodules, no signed commits.
- The working tree is the flat set of
*.md,*.fountainand*.txtfiles in the sync directory (non-recursive). Sub-directories in the repository underpathare not checked out. Dotfiles (.git,*.meta,wifi.cfg,git.cfg) are never committed. - Deleting every local
*.md/*.fountain/*.txtfile will commit the deletion of the whole synced sub-tree on the next sync – that is correct Git behaviour, but worth knowing. - The initial sync fetches full history; for a large repository that can be slow and use significant SD space.
- Line-level merges above ~1400 lines on any side fall back to a whole-file conflict block.
- Authentication is HTTP Basic only (token in
git.cfg); SSH is not supported.