A Git-backed version control system where the thing being versioned is a DaVinci Resolve timeline. Companion to the product teardown, which covers why it was built this way. This one covers how.
The constraint that shapes everything
Git is good at merging text and useless at merging video. A DaVinci Resolve project file is binary, and the media it references runs to hundreds of gigabytes. Version controlling either one directly is a non-starter.
So Vit versions neither. It versions the edit decisions: which clip sits where, what grade is on it, how loud it is, where the markers are. Those are small, structured, and text-representable. Serialized to JSON, a full timeline snapshot is a few kilobytes, and Git handles it the way it handles source code.
Raw footage never enters the repository. Teams keep sharing it however they already do, on a drive or in Dropbox, and manifest.json records paths and checksums so Resolve can relink. Vit owns the coordination layer and stays out of the storage layer.
Domain-split JSON
This is the design decision the rest of the system rests on.
A naive implementation writes one timeline.json. Every collaborator then edits the same file, and every parallel edit is a merge conflict. You'd spend the whole project resolving conflicts that a smarter file layout would never have produced.
Vit splits the timeline into six files along the lines the team already divides along:
| File | Contents | Typical owner |
|---|---|---|
cuts.json | Clip placements, in/out points, transforms, speed | Editor |
color.json | Color grading per clip | Colorist |
audio.json | Levels, panning | Sound designer |
effects.json | Effects, transitions | Editor / VFX |
markers.json | Markers, notes | Anyone |
metadata.json | Frame rate, resolution, track counts | Rarely changed |
The result is that most merges have no conflict to resolve. The colorist touches color.json, the sound designer touches audio.json, the editor touches cuts.json. Git sees three disjoint changesets and merges them without being asked to make a judgment call.
The file layout is doing the work that a conflict resolution algorithm would otherwise have to do. Structuring the data so the problem doesn't arise beats solving the problem well.
Clips are linked across files by a shared id field, which is what makes cross-domain reasoning possible when it's needed.
Architecture
Resolve Panel (primary UI) ─┐
├─→ vit-core (Python) ─→ git (system binary) ─→ GitHub
vit CLI (setup, power use) ─┘
Vit shells out to the real git binary rather than reimplementing version control. Branches are Git branches, commits are Git commits, the remote is an ordinary GitHub repository. That means the history is inspectable with any Git tool, and nothing is locked inside a proprietary format.
vit-core modules:
| Module | Responsibility |
|---|---|
serializer.py | Resolve timeline to JSON |
deserializer.py | JSON back to Resolve timeline |
json_writer.py | Domain-split file I/O, deterministic formatting |
core.py | Git operations wrapper |
validator.py | Post-merge rule-based validation |
ai_merge.py | Semantic conflict resolution |
differ.py | Human-readable timeline diffs |
JSON is always written with indent=2, sort_keys=True. Deterministic serialization matters more here than it looks: without it, reopening a project and re-serializing produces spurious diffs, and the history fills with commits that changed nothing.
Where Git stops being enough
Domain-split JSON eliminates the common conflicts. It does not eliminate a subtler class of problem, where Git merges cleanly and produces an invalid timeline.
| Problem | How it happens | Why Git misses it |
|---|---|---|
| Orphaned reference | Editor deletes item_003 in cuts.json, colorist graded item_003 in color.json | Both files merge fine, the grade now points at nothing |
| Audio/video desync | Editor trims a clip, sound designer mixed for the old length | Different files, no textual conflict |
| Overlapping clips | Two editors add clips to the same track at the same timecode | Git takes both, the timeline is invalid |
| Track count mismatch | One branch adds a V3 track, the other doesn't | metadata.json conflicts, cuts.json is structurally wrong |
| Speed/audio mismatch | Speed changed on one branch, audio adjusted for the old speed on another | Values diverge silently |
| Stale duration | Speed changed while a parallel branch edited the same clip's duration | record_end_frame no longer matches the speed-adjusted source |
Every one of these is a case where the merge succeeds and the editor opens a broken timeline.
validator.py catches them with rule-based checks after every merge. Detection is deterministic, not model-driven, which matters because this is the layer that decides whether anything is wrong at all.
AI-assisted semantic merge
Only when validation finds an issue, or Git itself conflicts, does an LLM get involved.
The flow:
- Attempt the Git merge
- Run
validator.pyon the result - If clean, done. The AI is never called in the common case.
- If not, extract BASE, OURS and THEIRS for the affected files
- Send all domain JSON, the schema context, and the specific detected issues to Gemini 2.5 Flash
- Receive structured per-domain decisions with confidence levels
- On low confidence, present the options to the user instead of choosing
- Show the user exactly what changed
- Write and commit only on approval
The resolution rules live in the prompt:
- A clip deleted in one branch has its references removed from all domain files
- Audio clip boundaries must match their corresponding video clip boundaries
- No two clips may overlap on the same track at the same timecode
- Preserve as much work from both branches as possible
- When ambiguous, prefer the more recent commit
Two deliberate limits on how much the model is trusted. It never writes without user approval. And in the Resolve panel, AI is enrichment only: branch comparison and commit classification fall back to heuristics when no API key is present, and full AI conflict resolution runs only in the CLI. The panel works completely without an API key.
The honest tradeoff: these are prompt instructions, not a trained model, so the same conflict can resolve differently on two runs and there's no way to unit test the behavior the way a merge algorithm can be tested. Acceptable for the cases that reach it, which are rare, user-reviewed, and reversible. Not acceptable for anything running unattended.
Resolve API constraints
The Resolve scripting API sets hard limits on what can be captured and restored, and the serializer is shaped around them:
- Extended properties (rotation, crop, flip) are static only. No keyframe data is reachable.
- Retiming is constant-speed only. Speed ramps can't be read.
- Color is write-only. There is no
GetCDLorGetLUT, so grades can be applied but not fully read back. - No deletion API for timelines or clips.
- Timeline restore triggers clip duplication if
SetName()is called on the existing timeline, so restore uses a three-phase create, populate, rename flow.
The last one is the kind of constraint that only shows up in practice. The obvious implementation silently duplicates the user's clips.
Testing
The serializer is tested against a mock Resolve object, which is what makes any of this testable without a running copy of Resolve. Coverage spans the Git wrapper, merge behavior, validation rules, AI merge, diff formatting, and full serialize/deserialize roundtrips.
python -m pytest tests/
Scope boundaries
Deliberately out of scope: web UI, hosted platform, database, media storage or sync, file locking, real-time collaboration, authentication, LUT versioning, and other editing applications.
Every one of those is a real product. None of them is this one.