Persistent store with Git semantics: lazy reads, delayed writes, content-addressing
at inode 208 lines 6.7 kB view raw
1(** Common CLI helpers. *) 2 3open Irmin 4 5(* Output helpers using Tty *) 6let success fmt = 7 let style = Tty.Style.(fg Tty.Color.green) in 8 Fmt.pr ("%a " ^^ fmt ^^ "@.") (Tty.Style.styled style Fmt.string) "" 9 10let error fmt = 11 let style = Tty.Style.(fg Tty.Color.red) in 12 Fmt.epr ("%a " ^^ fmt ^^ "@.") (Tty.Style.styled style Fmt.string) "" 13 14let styled_bold = Tty.Style.(styled bold Fmt.string) 15let styled_cyan = Tty.Style.(styled (fg Tty.Color.cyan) Fmt.string) 16let styled_faint = Tty.Style.(styled faint Fmt.string) 17let styled_yellow = Tty.Style.(styled (fg Tty.Color.yellow) Fmt.string) 18let styled_blue = Tty.Style.(styled (fg Tty.Color.blue) Fmt.string) 19 20let path_of_string s = 21 String.split_on_char '/' s |> List.filter (fun s -> s <> "") 22 23type log_entry = { hash : string; author : string; message : string } 24(** Log entry for commit history. *) 25 26(** Backend module signature - first-class modules for extensibility. *) 27module type BACKEND = sig 28 type store 29 type tree 30 type hash 31 32 val open_store : 33 sw:Eio.Switch.t -> fs:Eio.Fs.dir_ty Eio.Path.t -> config:Config.t -> store 34 35 val checkout : store -> branch:string -> tree option 36 val empty_tree : store -> tree 37 val tree_find : tree -> string list -> string option 38 val tree_add : tree -> string list -> string -> tree 39 val tree_remove : tree -> string list -> tree 40 val tree_list : tree -> string list -> (string * [ `Contents | `Node ]) list 41 val head : store -> branch:string -> hash option 42 43 val commit : 44 store -> 45 tree:tree -> 46 parents:hash list -> 47 message:string -> 48 author:string -> 49 hash 50 51 val set_head : store -> branch:string -> hash -> unit 52 val branches : store -> string list 53 val log : store -> branch:string -> limit:int option -> log_entry list 54 val hash_to_hex : hash -> string 55 val hash_short : hash -> string 56end 57 58(** Git backend implementation. *) 59module Git : BACKEND = struct 60 type store = Store.Git.t 61 type tree = Tree.Git.t 62 type hash = Hash.sha1 63 64 let open_store ~sw ~fs ~config = 65 let git_dir = Fpath.(v config.Config.store_path / ".git") in 66 Git_interop.import_git ~sw ~fs ~git_dir 67 68 let checkout store ~branch = Store.Git.checkout store ~branch 69 let empty_tree _store = Tree.Git.empty () 70 let tree_find tree path = Tree.Git.find tree path 71 let tree_add tree path content = Tree.Git.add tree path content 72 let tree_remove tree path = Tree.Git.remove tree path 73 let tree_list tree path = Tree.Git.list tree path 74 let head store ~branch = Store.Git.head store ~branch 75 76 let commit store ~tree ~parents ~message ~author = 77 (* Disable inlining for git interop: inlined contents use a format 78 (version byte \x01) that is not valid git, so git tools cannot 79 read the tree objects. *) 80 Store.Git.commit ~inline_threshold:0 store ~tree ~parents ~message ~author 81 82 let set_head store ~branch hash = Store.Git.set_head store ~branch hash 83 let branches store = Store.Git.branches store 84 85 let log store ~branch ~limit = 86 let rec walk n hash acc = 87 if n = Some 0 then List.rev acc 88 else 89 match Store.Git.read_commit store hash with 90 | None -> List.rev acc 91 | Some commit -> ( 92 let entry = 93 { 94 hash = Hash.to_hex hash; 95 author = Commit.Git.author commit; 96 message = Commit.Git.message commit; 97 } 98 in 99 match Commit.Git.parents commit with 100 | [] -> List.rev (entry :: acc) 101 | p :: _ -> walk (Option.map pred n) p (entry :: acc)) 102 in 103 match Store.Git.head store ~branch with 104 | None -> [] 105 | Some h -> walk limit h [] 106 107 let hash_to_hex h = Hash.to_hex h 108 let hash_short h = String.sub (Hash.to_hex h) 0 7 109end 110 111(** MST backend implementation using ATProto blockstore. *) 112module Mst : BACKEND = struct 113 type store = Atp.Blockstore.writable 114 type tree = Atp.Mst.node * Atp.Blockstore.writable 115 type hash = Atp.Cid.t 116 117 let open_store ~sw:_ ~fs ~config = 118 match config.Config.backend with 119 | Config.Memory -> Atp.Blockstore.memory () 120 | Config.Mst | Config.Git -> 121 let irmin_dir = Filename.concat config.Config.store_path ".irmin" in 122 (try Unix.mkdir irmin_dir 0o755 with Unix.Unix_error _ -> ()); 123 let blocks_path = Eio.Path.(fs / irmin_dir / "blocks") in 124 Atp.Blockstore.filesystem blocks_path 125 126 let checkout _store ~branch = 127 (* TODO: Load root from refs *) 128 ignore branch; 129 None 130 131 let empty_tree store = (Atp.Mst.empty, store) 132 133 let tree_find (mst, bs) path = 134 let key = String.concat "/" path in 135 match Atp.Mst.get key mst ~store:(bs :> Atp.Blockstore.readable) with 136 | None -> None 137 | Some cid -> bs#get cid 138 139 let tree_add (mst, bs) path content = 140 let key = String.concat "/" path in 141 let cid = Atp.Cid.of_string content in 142 bs#put cid content; 143 let mst' = Atp.Mst.add key cid mst ~store:bs in 144 (mst', bs) 145 146 let tree_remove (mst, bs) path = 147 let key = String.concat "/" path in 148 let mst' = Atp.Mst.remove key mst ~store:bs in 149 (mst', bs) 150 151 let tree_list (mst, bs) path = 152 let prefix = 153 match path with [] -> "" | _ -> String.concat "/" path ^ "/" 154 in 155 Atp.Mst.leaves mst ~store:(bs :> Atp.Blockstore.readable) 156 |> Seq.filter_map (fun (k, _cid) -> 157 if 158 prefix = "" 159 || String.length k > String.length prefix 160 && String.sub k 0 (String.length prefix) = prefix 161 then 162 let suffix = 163 String.sub k (String.length prefix) 164 (String.length k - String.length prefix) 165 in 166 match String.index_opt suffix '/' with 167 | None -> Some (suffix, `Contents) 168 | Some i -> Some (String.sub suffix 0 i, `Node) 169 else None) 170 |> List.of_seq |> List.sort_uniq compare 171 172 let head _store ~branch = 173 (* TODO: Store refs in blockstore *) 174 ignore branch; 175 None 176 177 let commit bs ~tree:(mst, _) ~parents ~message ~author = 178 ignore parents; 179 ignore message; 180 ignore author; 181 let cid = Atp.Mst.to_cid mst ~store:bs in 182 bs#sync; 183 cid 184 185 let set_head _store ~branch _hash = 186 (* TODO: Store refs *) 187 ignore branch 188 189 let branches _store = [ "main" ] 190 191 let log _store ~branch ~limit = 192 (* MST stores don't have traditional commit history *) 193 ignore branch; 194 ignore limit; 195 [] 196 197 let hash_to_hex cid = Atp.Cid.to_string cid 198 199 let hash_short cid = 200 let s = Atp.Cid.to_string cid in 201 if String.length s > 7 then String.sub s 0 7 else s 202end 203 204(** Get the appropriate backend module for a configuration. *) 205let backend_of_config (config : Config.t) : (module BACKEND) = 206 match config.backend with 207 | Config.Git -> (module Git) 208 | Config.Mst | Config.Memory -> (module Mst)