Persistent store with Git semantics: lazy reads, delayed writes, content-addressing
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(irmin): hide Git_interop, expose everything via Irmin.Git

Git_interop was leaking as a top-level module with _git-suffixed names.
All callers now use Irmin.Git.{init,open_,import,read_object,...}.
Low-level object/ref ops folded into Git module alongside store builders.

+154 -57
+1 -1
bin/cmd_init.ml
··· 9 9 let path' = Fpath.v path in 10 10 match backend with 11 11 | `Git -> 12 - let _store = Git_interop.init_git ~sw ~fs ~path:path' in 12 + let _store = Git.init ~sw ~fs ~path:path' in 13 13 Common.success "Initialised Git repository at %a" Common.styled_bold path 14 14 | `Pds -> 15 15 let eio_path = Eio.Path.(fs / path) in
+1 -1
bin/common.ml
··· 64 64 type hash = S.hash 65 65 66 66 let open_store ~sw ~fs ~config = 67 - Git_interop.open_git ~sw ~fs ~path:(Fpath.v config.Config.store_path) 67 + Irmin.Git.open_ ~sw ~fs ~path:(Fpath.v config.Config.store_path) 68 68 69 69 let checkout s ~branch = S.checkout s ~branch 70 70 let empty_tree _s = S.Tree.empty ()
+3 -5
lib/git_interop.ml
··· 77 77 close = (fun () -> ()); 78 78 } 79 79 80 - (* Public API *) 81 - 82 - let import_git ~sw:_ ~fs ~git_dir = 80 + let import ~sw:_ ~fs ~git_dir = 83 81 let repo = Git.Repository.open_bare ~fs git_dir in 84 82 let backend = git_backend repo in 85 83 Store.Git.create ~backend 86 84 87 - let open_git ~sw:_ ~fs ~path = 85 + let open_ ~sw:_ ~fs ~path = 88 86 let repo = Git.Repository.open_repo ~fs path in 89 87 let backend = git_backend repo in 90 88 Store.Git.create ~backend 91 89 92 - let init_git ~sw:_ ~fs ~path = 90 + let init ~sw:_ ~fs ~path = 93 91 let repo = Git.Repository.init ~fs path in 94 92 let backend = git_backend repo in 95 93 Store.Git.create ~backend
+5 -28
lib/git_interop.mli
··· 1 - (** Git interoperability. 1 + (** Git interoperability — internal implementation module. 2 2 3 - Bidirectional support for reading and writing Git repositories. This allows 4 - Irmin to work with existing .git directories and interoperate with the Git 5 - ecosystem. Pack files are handled transparently. *) 3 + Exposed through [Irmin.Git]; do not use directly. *) 6 4 7 - (** {1 Git Repository Operations} *) 8 - 9 - val import_git : 5 + val import : 10 6 sw:Eio.Switch.t -> 11 7 fs:Eio.Fs.dir_ty Eio.Path.t -> 12 8 git_dir:Fpath.t -> 13 9 Store.Git.t 14 - (** [import_git ~sw ~fs ~git_dir] opens a bare .git directory as an Irmin store. 15 - Supports both loose objects and pack files. *) 16 10 17 - val open_git : 11 + val open_ : 18 12 sw:Eio.Switch.t -> fs:Eio.Fs.dir_ty Eio.Path.t -> path:Fpath.t -> Store.Git.t 19 - (** [open_git ~sw ~fs ~path] opens a Git repository (with .git subdirectory) as 20 - an Irmin store. Supports both loose objects and pack files. *) 21 13 22 - val init_git : 14 + val init : 23 15 sw:Eio.Switch.t -> fs:Eio.Fs.dir_ty Eio.Path.t -> path:Fpath.t -> Store.Git.t 24 - (** [init_git ~sw ~fs ~path] initializes a new Git repository at [path] and 25 - returns an Irmin store for it. *) 26 - 27 - (** {1 Object Operations} *) 28 16 29 17 val read_object : 30 18 sw:Eio.Switch.t -> ··· 32 20 git_dir:Fpath.t -> 33 21 Hash.sha1 -> 34 22 (string * string, [> `Msg of string ]) result 35 - (** [read_object ~sw ~fs ~git_dir hash] reads a Git object, returning 36 - [(type, data)] where type is "blob", "tree", "commit", or "tag". Checks 37 - loose objects first, then pack files. *) 38 23 39 24 val write_object : 40 25 sw:Eio.Switch.t -> ··· 43 28 typ:string -> 44 29 string -> 45 30 Hash.sha1 46 - (** [write_object ~sw ~fs ~git_dir ~typ data] writes a Git object as a 47 - zlib-compressed loose object. *) 48 - 49 - (** {1 Reference Operations} *) 50 31 51 32 val read_ref : 52 33 sw:Eio.Switch.t -> ··· 54 35 git_dir:Fpath.t -> 55 36 string -> 56 37 Hash.sha1 option 57 - (** [read_ref ~sw ~fs ~git_dir name] reads a Git reference. Follows symbolic 58 - refs. *) 59 38 60 39 val write_ref : 61 40 sw:Eio.Switch.t -> ··· 64 43 string -> 65 44 Hash.sha1 -> 66 45 unit 67 - (** [write_ref ~sw ~fs ~git_dir name hash] writes a Git reference. *) 68 46 69 47 val list_refs : 70 48 sw:Eio.Switch.t -> 71 49 fs:Eio.Fs.dir_ty Eio.Path.t -> 72 50 git_dir:Fpath.t -> 73 51 string list 74 - (** [list_refs ~sw ~fs ~git_dir] lists all references. *)
+8 -6
lib/irmin.ml
··· 71 71 72 72 (** {1 Interoperability} *) 73 73 74 - module Git_interop = Git_interop 75 - (** Git repository I/O. *) 76 - 77 74 module Pds_interop = Pds_interop 78 75 (** PDS store access via MST. *) 79 76 ··· 87 84 module Subtree = Subtree.Git 88 85 module Proof = Proof.Git 89 86 90 - let import = Git_interop.import_git 91 - let init = Git_interop.init_git 92 - let open_ = Git_interop.open_git 87 + let import = Git_interop.import 88 + let init = Git_interop.init 89 + let open_ = Git_interop.open_ 90 + let read_object = Git_interop.read_object 91 + let write_object = Git_interop.write_object 92 + let read_ref = Git_interop.read_ref 93 + let write_ref = Git_interop.write_ref 94 + let list_refs = Git_interop.list_refs 93 95 end 94 96 95 97 (** {1 Pre-instantiated: MST Format} *)
+47 -7
lib/irmin.mli
··· 71 71 72 72 (** {1 Interoperability} *) 73 73 74 - module Git_interop = Git_interop 75 - (** Git repository I/O. *) 76 - 77 74 module Pds_interop = Pds_interop 78 75 (** PDS store access via MST. *) 79 76 80 77 (** {1 Pre-instantiated: Git Format} *) 81 78 82 79 module Git : sig 83 - (** Git-compatible store (SHA-1, Git object format). 84 - 85 - All three builders return [Store.t = Store.Git.t], which embeds the 86 - backend. After creation use the flat [Store.Git.*] / [Tree.Git.*] API. *) 80 + (** Git-compatible store (SHA-1, Git object format). *) 87 81 88 82 module Tree = Tree.Git 89 83 module Store : module type of Store.Git 90 84 module Subtree = Subtree.Git 91 85 module Proof = Proof.Git 86 + 87 + (** {2 Store builders} *) 92 88 93 89 val import : 94 90 sw:Eio.Switch.t -> fs:Eio.Fs.dir_ty Eio.Path.t -> git_dir:Fpath.t -> Store.t ··· 101 97 val init : 102 98 sw:Eio.Switch.t -> fs:Eio.Fs.dir_ty Eio.Path.t -> path:Fpath.t -> Store.t 103 99 (** [init ~sw ~fs ~path] initializes a new Git repository at [path]. *) 100 + 101 + (** {2 Low-level object and ref access} *) 102 + 103 + val read_object : 104 + sw:Eio.Switch.t -> 105 + fs:Eio.Fs.dir_ty Eio.Path.t -> 106 + git_dir:Fpath.t -> 107 + Hash.sha1 -> 108 + (string * string, [> `Msg of string ]) result 109 + (** [read_object ~sw ~fs ~git_dir hash] returns [(kind, data)] for a loose 110 + object. [kind] is ["blob"], ["tree"], ["commit"], or ["tag"]. *) 111 + 112 + val write_object : 113 + sw:Eio.Switch.t -> 114 + fs:Eio.Fs.dir_ty Eio.Path.t -> 115 + git_dir:Fpath.t -> 116 + typ:string -> 117 + string -> 118 + Hash.sha1 119 + (** [write_object ~sw ~fs ~git_dir ~typ data] writes a loose object. *) 120 + 121 + val read_ref : 122 + sw:Eio.Switch.t -> 123 + fs:Eio.Fs.dir_ty Eio.Path.t -> 124 + git_dir:Fpath.t -> 125 + string -> 126 + Hash.sha1 option 127 + (** [read_ref ~sw ~fs ~git_dir name] reads a Git reference. *) 128 + 129 + val write_ref : 130 + sw:Eio.Switch.t -> 131 + fs:Eio.Fs.dir_ty Eio.Path.t -> 132 + git_dir:Fpath.t -> 133 + string -> 134 + Hash.sha1 -> 135 + unit 136 + (** [write_ref ~sw ~fs ~git_dir name hash] writes a Git reference. *) 137 + 138 + val list_refs : 139 + sw:Eio.Switch.t -> 140 + fs:Eio.Fs.dir_ty Eio.Path.t -> 141 + git_dir:Fpath.t -> 142 + string list 143 + (** [list_refs ~sw ~fs ~git_dir] lists all references. *) 104 144 end 105 145 106 146 (** {1 Pre-instantiated: MST Format} *)
+62 -9
test/test_git_interop.ml
··· 22 22 let test_init_git () = 23 23 with_temp_dir @@ fun ~sw ~fs tmp_path -> 24 24 let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in 25 - let _store = Git_interop.init_git ~sw ~fs ~path:fpath in 26 - (* Verify .git directory was created *) 25 + let _store = Git.init ~sw ~fs ~path:fpath in 27 26 let git_dir = Eio.Path.(tmp_path / ".git") in 28 27 Alcotest.(check bool) "git dir exists" true (Eio.Path.is_directory git_dir) 29 28 30 29 let test_write_read_object () = 31 30 with_temp_dir @@ fun ~sw ~fs tmp_path -> 32 31 let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in 33 - let _store = Git_interop.init_git ~sw ~fs ~path:fpath in 32 + let _store = Git.init ~sw ~fs ~path:fpath in 34 33 let git_dir = Fpath.(fpath / ".git") in 35 34 let data = "hello world" in 36 - let hash = Git_interop.write_object ~sw ~fs ~git_dir ~typ:"blob" data in 37 - match Git_interop.read_object ~sw ~fs ~git_dir hash with 35 + let hash = Git.write_object ~sw ~fs ~git_dir ~typ:"blob" data in 36 + match Git.read_object ~sw ~fs ~git_dir hash with 38 37 | Ok (typ, content) -> 39 38 Alcotest.(check string) "type" "blob" typ; 40 39 Alcotest.(check string) "content" data content ··· 43 42 let test_write_read_ref () = 44 43 with_temp_dir @@ fun ~sw ~fs tmp_path -> 45 44 let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in 46 - let _store = Git_interop.init_git ~sw ~fs ~path:fpath in 45 + let _store = Git.init ~sw ~fs ~path:fpath in 47 46 let git_dir = Fpath.(fpath / ".git") in 48 - let hash = Git_interop.write_object ~sw ~fs ~git_dir ~typ:"blob" "content" in 49 - Git_interop.write_ref ~sw ~fs ~git_dir "refs/heads/test" hash; 50 - match Git_interop.read_ref ~sw ~fs ~git_dir "refs/heads/test" with 47 + let hash = Git.write_object ~sw ~fs ~git_dir ~typ:"blob" "content" in 48 + Git.write_ref ~sw ~fs ~git_dir "refs/heads/test" hash; 49 + match Git.read_ref ~sw ~fs ~git_dir "refs/heads/test" with 51 50 | Some h -> Alcotest.(check bool) "ref matches" true (Hash.equal hash h) 52 51 | None -> Alcotest.fail "ref not found" 53 52 53 + (* Regression: Repository.init used mkdir (non-recursive), failing when parent 54 + dirs don't exist. Fixed to mkdirs. *) 55 + let test_init_nested_path () = 56 + with_temp_dir @@ fun ~sw ~fs tmp_path -> 57 + let nested = Eio.Path.native_exn tmp_path ^ "/a/b/repo" in 58 + let fpath = Fpath.v nested in 59 + let _store = Git.init ~sw ~fs ~path:fpath in 60 + let git_dir = Eio.Path.(fs / (nested ^ "/.git")) in 61 + Alcotest.(check bool) 62 + "git dir in nested path" true 63 + (Eio.Path.is_directory git_dir) 64 + 65 + (* Regression: git_backend.write called Repository.write unconditionally, 66 + failing on duplicate objects. Fixed to skip if already exists. *) 67 + let test_write_duplicate_object () = 68 + with_temp_dir @@ fun ~sw ~fs tmp_path -> 69 + let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in 70 + let store = Git.init ~sw ~fs ~path:fpath in 71 + let backend = Store.Git.backend store in 72 + let data = "hello world" in 73 + let h = Codec.Git.hash_contents data in 74 + backend.Backend.write h data; 75 + (* Second write of the same hash must not raise *) 76 + backend.Backend.write h data; 77 + Alcotest.(check bool) 78 + "object exists after double write" true (backend.Backend.exists h) 79 + 80 + (* Integration: write commits to disk, reopen, read back. *) 81 + let test_store_git_roundtrip () = 82 + with_temp_dir @@ fun ~sw ~fs tmp_path -> 83 + let fpath = Fpath.v (Eio.Path.native_exn tmp_path) in 84 + let store = Git.init ~sw ~fs ~path:fpath in 85 + let tree = 86 + Store.Git.Tree.add (Store.Git.Tree.empty ()) [ "README.md" ] "# Hello" 87 + in 88 + let h = 89 + Store.Git.commit store ~tree ~parents:[] ~message:"init" ~author:"test" 90 + in 91 + Store.Git.set_head store ~branch:"main" h; 92 + let store2 = Git.open_ ~sw ~fs ~path:fpath in 93 + Alcotest.(check bool) 94 + "head survived reopen" true 95 + (Store.Git.head store2 ~branch:"main" = Some h); 96 + match Store.Git.checkout store2 ~branch:"main" with 97 + | None -> Alcotest.fail "checkout failed" 98 + | Some tree2 -> 99 + Alcotest.(check (option string)) 100 + "content survived reopen" (Some "# Hello") 101 + (Store.Git.Tree.find tree2 [ "README.md" ]) 102 + 54 103 let suite = 55 104 ( "git_interop", 56 105 [ 57 106 Alcotest.test_case "init git" `Quick test_init_git; 58 107 Alcotest.test_case "write/read object" `Quick test_write_read_object; 59 108 Alcotest.test_case "write/read ref" `Quick test_write_read_ref; 109 + Alcotest.test_case "init nested path" `Quick test_init_nested_path; 110 + Alcotest.test_case "write duplicate object" `Quick 111 + test_write_duplicate_object; 112 + Alcotest.test_case "store roundtrip" `Quick test_store_git_roundtrip; 60 113 ] )
+27
test/test_link.ml
··· 63 63 Alcotest.(check int) "right" 3 r.x 64 64 | _ -> Alcotest.fail "expected leaves") 65 65 66 + (* of_backend: links persist through the backend and can be fetched by address 67 + using a second store instance backed by the same backend. *) 68 + let test_link_of_backend_persist () = 69 + let backend = Backend.Memory.create_sha256 () in 70 + let s : int Link.store = Link.Mst.of_backend backend in 71 + let l = Link.v s 42 in 72 + let addr = Link.address l in 73 + (* Second store wired to the same backend *) 74 + let s2 : int Link.store = Link.Mst.of_backend backend in 75 + let l2 = Link.of_address s2 addr in 76 + Alcotest.(check int) "fetched from backend" 42 (Link.get l2) 77 + 78 + (* of_backend with Git codec *) 79 + let test_link_of_backend_git () = 80 + let backend = Backend.Memory.create_sha1 () in 81 + let s : string Link.store = Link.Git.of_backend backend in 82 + let l = Link.v s "hello" in 83 + let addr = Link.address l in 84 + let s2 : string Link.store = Link.Git.of_backend backend in 85 + Alcotest.(check string) 86 + "fetched via git backend" "hello" 87 + (Link.get (Link.of_address s2 addr)) 88 + 66 89 let suite = 67 90 ( "link", 68 91 [ ··· 74 97 Alcotest.test_case "read/write" `Quick test_link_read_write; 75 98 Alcotest.test_case "is_open" `Quick test_link_is_open; 76 99 Alcotest.test_case "tree" `Quick test_link_tree; 100 + Alcotest.test_case "of_backend persist (mst)" `Quick 101 + test_link_of_backend_persist; 102 + Alcotest.test_case "of_backend persist (git)" `Quick 103 + test_link_of_backend_git; 77 104 ] )