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, log and show. You can pass them as revisions.
  • Item sequences appear in synck versions and --events output. You select file versions with --item-seq.
  • Repository sequences are the numbers at the start of each log entry. You use them as revisions in commands such as show and diff, and with --through in synck 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:

  1. Hash each event’s canonical bytes with SHA-256.
  2. Hash the concatenation of those hashes, in order: that is the events_digest.
  3. Build the commit header and write it as canonical JSON.
  4. 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 - 1 exists. Follow the history in order, or use HEAD~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 finalize completes.

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.