Install and the minimal store
The in-memory backend, for getting the shape of the API without a filesystem or Git involved.
$ opam install irmin irmin-mem
Irmin.Mem.KV builds a key/value store functor over a content type - string here, the simplest instantiation.
module Store = Irmin_mem.KV.Make (Irmin.Contents.String)let config = Irmin_mem.config ()let info () = Irmin.Info.Default.v ~author:"me" 0L
Opening the repo and the main branch. Irmin is Lwt-based throughout - every store operation returns a promise.
let main () =let open Lwt.Syntax inlet* repo = Store.Repo.v config inlet* t = Store.main repo inlet* () = Store.set_exn t ~info [ "a"; "b"; "c" ] "hello" inlet* v = Store.get t [ "a"; "b"; "c" ] inLwt_io.printl vlet () = Lwt_main.run (main ())
Run it.
$ dune exec ./main.exehello
Paths, keys, and the tree shape
A key is a string list - a path through a tree, the same mental model as a filesystem or a Git repo's working directory.
let path_to_readme = [ "docs"; "README.md" ]let path_to_config = [ "config"; "app.json" ]
set_exn raises on failure; set returns a result - reach for the result-returning form at any boundary where the caller should decide how to handle a write conflict rather than crash.
let* r = Store.set t ~info [ "config"; "app.json" ] {|{"port":8080}|} inmatch r with| Ok () -> Lwt_io.printl "written"| Error (`Conflict msg) -> Lwt_io.printl ("conflict: " ^ msg)| Error (`Too_many_retries n) -> Lwt_io.printl (Printf.sprintf "gave up after %d retries" n)
mem checks existence without fetching the value; list enumerates one tree level, non-recursively.
let* exists = Store.mem t [ "config"; "app.json" ] inlet* entries = Store.list t [ "config" ] inList.iter (fun (step, _) -> print_endline step) entries
remove_exn deletes a key; a whole subtree disappears once every key under it is gone - there is no separate directory-delete operation, because a directory is just a prefix, not a first-class node with its own existence.
let* () = Store.remove_exn t ~info [ "config"; "app.json" ] in
History: every write is a commit
Store.history walks the commit graph reachable from a point in the store, the same shape as git log.
let* history = Store.history t inStore.History.iter_vertex(fun commit ->let info = Store.Commit.info commit inPrintf.printf "%s: %s\n"(Irmin.Info.Default.date info |> Int64.to_string)(Irmin.Info.Default.message info))history
last_modified finds the most recent commit that touched a given key - the equivalent of git log -1 -- path.
let* commits = Store.last_modified t [ "config"; "app.json" ] inmatch commits with| commit :: _ -> Lwt_io.printl (Store.Commit.hash commit |> Store.Hash.to_string)| [] -> Lwt_io.printl "never touched"
Reading the store as it existed at an older commit - a store handle bound to a specific commit rather than a moving branch head.
let* head = Store.Head.get t inlet* old_tree = Store.Commit.tree head inlet* old_value = Store.Tree.find old_tree [ "config"; "app.json" ] in
Branches
Branches are named pointers into the commit graph, same as Git - of_branch opens a store handle bound to one, creating it on first write if it does not exist yet.
let* feature = Store.of_branch repo "feature-x" inlet* () = Store.set_exn feature ~info [ "flag" ] "on" in
Listing every branch in the repo.
let* branches = Store.Branch.list repo inList.iter print_endline branches
merge_with_branch merges another branch into the current one - three-way, using the common ancestor, exactly like git merge.
let* result = Store.merge_with_branch t "feature-x" ~info inmatch result with| Ok () -> Lwt_io.printl "merged cleanly"| Error (`Conflict msg) -> Lwt_io.printl ("merge conflict: " ^ msg)
Deleting a branch once merged - the commits themselves stay reachable from history unless garbage collected separately.
let* () = Store.Branch.remove repo "feature-x" in
Merge semantics for content types
The default Irmin.Contents.String merges by last-write-wins on conflict - there is no sensible way to merge two arbitrary strings. A real content type defines its own merge function, which is what makes concurrent, structured writes to the same key actually reconcile instead of one side clobbering the other.
A custom content type: a counter, whose merge adds up concurrent increments from both branches instead of picking one arbitrarily.
module Counter = structtype t = intlet t = Irmin.Type.intlet merge ~old a b =let open Irmin.Merge.Infix inold () >>=* fun old ->let old = match old with Some o -> o | None -> 0 inlet delta_a = a - old inlet delta_b = b - old inIrmin.Merge.ok (old + delta_a + delta_b)let merge = Irmin.Merge.(option (v t merge))end
Wiring the custom content type into a store, in place of Irmin.Contents.String.
module CounterStore = Irmin_mem.KV.Make (Counter)
Two branches each increment the same counter from 5 to 8 (+3) and from 5 to 9 (+4) independently - merging combines the deltas to 12, rather than last-write-wins silently dropping one branch's work.
let* main = CounterStore.main repo inlet* () = CounterStore.set_exn main ~info [ "visits" ] 5 inlet* a = CounterStore.of_branch repo "a" inlet* b = CounterStore.of_branch repo "b" inlet* () = CounterStore.set_exn a ~info [ "visits" ] 8 inlet* () = CounterStore.set_exn b ~info [ "visits" ] 9 inlet* () = CounterStore.merge_with_branch a "b" ~info |> Lwt.map (fun _ -> ()) inlet* v = CounterStore.get a [ "visits" ] in(* v = 12 *)
A JSON content type via irmin.Contents.Json_value, with merge falling back to Irmin.Merge.default (which is last-write-wins) unless overridden - reasonable for config blobs where field-level merging is not worth the complexity.
module JsonStore = Irmin_mem.KV.Make (Irmin.Contents.Json_value)let* t = JsonStore.main repo inlet* () = JsonStore.set_exn t ~info[ "config" ] (`O [ ("port", `Float 8080.) ]) in
Diffing
Store.status/Store.Tree.diff give the changed keys between two trees - the programmatic equivalent of git diff.
let* diff = Store.Tree.diff old_tree new_tree inList.iter(fun (key, change) ->match change with| `Added v -> Printf.printf "+ %s\n" (String.concat "/" key)| `Removed v -> Printf.printf "- %s\n" (String.concat "/" key)| `Updated (old, new_) -> Printf.printf "~ %s\n" (String.concat "/" key))diff
Watches
watch registers a callback fired on every commit to the store - the mechanism a service uses to react to changes made by another process sharing the same repo, rather than polling.
let* watch = Store.watch t (fun diff ->match diff with| `Updated (_, (commit, _)) ->Lwt_io.printl ("new commit: " ^ Store.Commit.hash commit |> Store.Hash.to_string)| `Added _ | `Removed _ -> Lwt.return_unit)in
watch_key narrows the callback to changes under one specific path, avoiding a wake-up on every unrelated commit.
let* watch = Store.watch_key t [ "config"; "app.json" ] (fun _diff ->Lwt_io.printl "config changed")in
Unregistering it when done.
let* () = Store.unwatch watch in
The FS and Git backends
Persisting to disk without a real Git repository underneath - a simpler on-disk format, faster than Git for pure storage.
$ opam install irmin-fs
Same Make functor, different config module - the store's own API is unchanged, only the backend.
module FSStore = Irmin_fs_unix.KV.Make (Irmin.Contents.String)let config = Irmin_fs.config "/tmp/irmin-store"let* repo = FSStore.Repo.v config in
The actual Git backend - every commit is a real Git commit, readable by git log/git show on the same directory.
$ opam install irmin-git
Irmin_git.FS.KV wires a store directly onto an on-disk Git repository - external tooling and Irmin can both read and write it.
module GitStore = Irmin_git.FS.KV (Irmin_git.Mem_info) (Irmin.Contents.String)let config = Irmin_git.config ~bare:true "/tmp/my-repo"let* repo = GitStore.Repo.v config inlet* t = GitStore.main repo inlet* () = GitStore.set_exn t ~info [ "hello.txt" ] "world" in
Confirming it from the shell - an ordinary Git repository, because it is one.
$ cd /tmp/my-repo && git log --onelinea3f1c2e hello.txt
Pushing to and pulling from a real remote - the same clone/fetch/push vocabulary as the git CLI, exposed as Store functions.
let remote = Irmin_git.remote "https://github.com/me/my-repo.git" inlet* result = GitStore.Sync.pull t remote `Set inlet* _ = GitStore.Sync.push t remote in
Transactions: with_tree
Store.with_tree applies a function over the tree and atomically commits the result, retrying on a concurrent conflicting write - the right way to do a read-modify-write instead of a bare get followed by a bare set.
let* () =Store.with_tree_exn t [ "counters" ] ~info ~strategy:`Merge (fun tree ->let tree = Option.value tree ~default:(Store.Tree.empty ()) inlet* current = Store.Tree.find tree [ "hits" ] inlet next = 1 + Option.value current ~default:0 inlet* tree = Store.Tree.add tree [ "hits" ] next inLwt.return_some tree)in
Building a batch of changes on an in-memory tree, then committing them all in one write - cheaper than one Store.set per key when updating many paths at once.
let* tree = Store.Tree.empty () |> Lwt.return inlet* tree = Store.Tree.add tree [ "a" ] "1" inlet* tree = Store.Tree.add tree [ "b" ] "2" inlet* tree = Store.Tree.add tree [ "c" ] "3" inlet* () = Store.set_tree_exn t [ "batch" ] tree ~info in
A small worked example: a config service
A tiny module wrapping a GitStore for versioned config, with get/set/history all in terms of the Store API above.
module Config = structlet info msg () = Irmin.Info.Default.v ~author:"config-service" ~message:msg 0Llet get t key =Store.find t [ "config"; key ]let set t ~key ~value =Store.set_exn t ~info:(info ("set " ^ key)) [ "config"; key ] valuelet history t key =Store.last_modified t [ "config"; key ]end
Two independent writers on separate branches, merged, with the counter content type resolving any concurrent conflict rather than one writer silently losing.
let* writer_a = CounterStore.of_branch repo "writer-a" inlet* writer_b = CounterStore.of_branch repo "writer-b" inlet* () = CounterStore.set_exn writer_a ~info [ "config"; "replicas" ] 3 inlet* () = CounterStore.set_exn writer_b ~info [ "config"; "replicas" ] 5 inlet* main = CounterStore.main repo inlet* _ = CounterStore.merge_with_branch main "writer-a" ~info inlet* _ = CounterStore.merge_with_branch main "writer-b" ~info in
The merge function is the whole trick: Irmin never resolves conflicting writes for you, it just runs the content type's own merge function and stores whatever that returns. A plain reference cell has no sensible merge; a counter, a CRDT, or a last-write-wins record does. Getting the library itself onto a project is the same opam install step as any other dependency, covered in Introduction to opam.