Commit identity and the three coordinates
Synck uses three different kinds of number to talk about history, and mixing them up is the most common source of confusion. This page explains each one, and shows you how to compute a commit ID yourself, so you can see that there is nothing hidden in it.
Three coordinates, three questions
| Coordinate | Example | Question it answers |
|---|---|---|
| Commit ID | sha256:daf44e21…66fb2f |
Which change is this, exactly? |
Item sequence (item_seq) |
3 |
Where is this event in one item’s history? |
Repository sequence (repo_seq) |
3 |
Where was this commit placed in this repository’s overall history? |
In a small repository the two sequences often have the same value, as they do in the example above. That is a coincidence, and it ends as soon as more than one item is involved. In the sample repository, the third message in the chat is the chat’s event 5 but the repository’s commit 8.
Where you meet them:
- Commit IDs are printed by
commit,logandshow. You can pass them as revisions. - Item sequences appear in
synck versionsand--eventsoutput. You select file versions with--item-seq. - Repository sequences are the numbers at the start of each
logentry. You use them as revisions in commands such asshowanddiff, and with--throughinsynck item.
Computing a commit ID by hand
A commit ID is a SHA-256 hash of a small JSON header, and the header contains a hash of the commit’s events. Here is commit 3 from Your first repository, with its one event:
$ synck show 3 --events
3 | 2026-10-25T19:43:13.345511486+00:00 | Alex | 1 change | 1 event | Move the meeting to 11
commit sha256:daf44e21d62de84d20e3f29cded10b538eeef6c32f793fd70f4f766c1766fb2f
0 | FILE::SET_CONTENT | item 01a17496-a8bb-76e0-8c24-ff89c5d2e99e | item-seq 3
{"base_seq":2,"event_type":"FILE::SET_CONTENT","item_id":"01a17496-a8bb-76e0-8c24-ff89c5d2e99e","set_content":{"blob_id":"sha256:54b5e89382d5bd75aaad652ca43cc9b844e1b5f8b73a0aac462a9b20a2bb731a"},"set_metadata":{"content":{"size":12}}}
The JSON line is the event’s canonical form, the exact bytes Synck hashes and stores. The recipe is:
- Hash each event’s canonical bytes with SHA-256.
- Hash the concatenation of those hashes, in order: that is the
events_digest. - Build the commit header and write it as canonical JSON.
- Hash the header. That is the commit ID.
As a short Python script, commit_id.py:
import hashlib, json, sys
# Usage: synck show REVISION --events | python3 commit_id.py AUTHOR "MESSAGE"
author, message = sys.argv[1], sys.argv[2]
events = [line.strip().encode() for line in sys.stdin if line.strip().startswith("{")]
event_hashes = b"".join(hashlib.sha256(e).digest() for e in events)
header = {
"object_type": "vcs_commit",
"format_version": 2,
"author_id": author,
"message": message,
"parent_commit_id": None,
"event_count": len(events),
"events_digest": "sha256:" + hashlib.sha256(event_hashes).hexdigest(),
}
canonical = json.dumps(header, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
print(canonical)
print("sha256:" + hashlib.sha256(canonical.encode()).hexdigest())
Run it:
$ synck show 3 --events | python3 commit_id.py Alex "Move the meeting to 11"
{"author_id":"Alex","event_count":1,"events_digest":"sha256:823b763e11bc8d6ce313ca5c4c29e736cdd391b468ec498420bcb6d1e854b302","format_version":2,"message":"Move the meeting to 11","object_type":"vcs_commit","parent_commit_id":null}
sha256:daf44e21d62de84d20e3f29cded10b538eeef6c32f793fd70f4f766c1766fb2f
The result is the commit ID Synck printed. It works for commits with several events too:
$ synck show 4 --events | python3 commit_id.py Alex "File the notes under meetings"
...
sha256:8d95faaba98e333785a49432297f19641769a0d7f6988e3bf06b36da0a2b2103
The file content is identified the same way. The blob_id in the event is the
SHA-256 of the file’s bytes:
$ printf 'Meet at 11.\n' | shasum -a 256
54b5e89382d5bd75aaad652ca43cc9b844e1b5f8b73a0aac462a9b20a2bb731a -
So the commit ID depends on the event, the event names the content’s hash, and
the hash depends on every byte of the file. Change one byte anywhere and the
commit ID changes. That chain is what fsck and repository verify walk.
The script is a teaching aid with two simplifications. First, Synck’s canonical JSON is RFC 8785, which agrees with Python’s sorted, compact output for headers like this one but has exact rules for unusual numbers and characters. Second, it handles the plain commit header, format 2. Commits that also carry ref events, such as a new file’s automatic named version, or that belong to an import job, use header formats 3 to 5. Those add a field or tag each event hash with its ledger.
What is in the hash, and what is not
The commit header contains exactly:
| Field | |
|---|---|
object_type |
Always vcs_commit, so a commit header can never be mistaken for another kind of hashed object |
format_version |
Which header layout this is |
author_id |
The author string |
message |
The message, or null |
parent_commit_id |
Always null; see below |
event_count, events_digest |
The events, in order |
import_job_id |
Only for commits made by an import job |
Just as deliberately, these are not part of the ID:
- the repository ID and the repository sequence;
- the timestamps (when it was made, when it was accepted);
- the committer, meaning the service or tool that recorded it, as distinct from the author;
- the operation ID, which is used to recognize a retried request.
Each of these omissions is a design decision, explained below.
No parent, no global head
A Git commit names its parent, and through it the entire chain before it. A
Synck commit does not. Its parent_commit_id is always null, and nothing
else in the header refers to earlier commits.
That is what lets independent changes to different items be accepted
concurrently. If every commit had to name the latest commit before it, every
writer in a repository would have to agree on the latest commit, and they would
all queue behind one global “head”, even when one is saving a document and
the other is posting a chat message. Synck orders each item’s events through
that item’s own sequence and base_seq instead. System
design follows that through.
The cost is that a commit ID alone does not tell you where the commit sits in history, or even which repository it belongs to. The same content could be accepted in two repositories and have the same ID in both. Placement is the job of the repository sequence.
Repository sequences come afterwards
Commits are accepted first and placed afterwards. A separate step, the
sequencer, assigns each accepted commit the next repository sequence, in a
way that guarantees anyone scanning the history in sequence order never skips
a commit that was accepted earlier. Those numbers are what log shows and
what --through selects.
Two consequences:
- Sequences can have gaps. A number may be reserved and never used. Never
assume that position
N - 1exists. Follow the history in order, or useHEAD~1, which counts real commits. - A commit can briefly have no sequence. Between acceptance and
sequencing, a commit is part of the history but not yet in the repository’s
order. In a local repository that window is a moment, unless the
process is interrupted. That is the state
synck repository finalizecompletes.
A hash is not a signature
A commit ID proves what a commit contains. It does not prove who accepted it, or that it belongs to a particular repository: anyone can construct a header with any author and hash it. Proof of authority or inclusion has to come from outside the commit, from the system that accepted it, or from how you received the copy. Author and committer are recorded as separate fields so that “who made this change” and “which system recorded it” can both be preserved, but neither is authenticated by Synck.