Persistent store with Git semantics: lazy reads, delayed writes, content-addressing
at benchs+cache 235 lines 8.4 kB view raw
1module Make (F : Codec.S) = struct 2 type hash = F.hash 3 4 module Store = Store.Make (F) 5 module Tree = Store.Tree 6 module Commit = Store.Commit 7 8 type status = 9 [ `In_sync 10 | `Local_ahead of int 11 | `Remote_ahead of int 12 | `Diverged of int * int (* local, remote *) 13 | `Trees_differ ] 14 15 (* Extract subtree at prefix from a tree *) 16 let extract_subtree tree prefix = Tree.find_tree tree prefix 17 18 (* Check if a commit touches the given prefix *) 19 let commit_touches_prefix store commit prefix = 20 let tree = Store.read_tree store (Commit.tree commit) in 21 Option.is_some (Tree.find_tree tree prefix) 22 23 (* Split: Extract subtree history into a new store *) 24 let split store ~prefix = 25 let backend = Backend.Memory.create_with_hash F.hash_to_hex F.hash_equal in 26 let new_store = Store.create ~backend () in 27 28 (* Walk commits and rewrite those touching prefix *) 29 let rec rewrite_commit old_hash rewritten = 30 if List.mem_assoc old_hash rewritten then 31 (List.assoc old_hash rewritten, rewritten) 32 else 33 match Store.read_commit store old_hash with 34 | None -> (old_hash, rewritten) 35 | Some commit -> ( 36 if not (commit_touches_prefix store commit prefix) then 37 (* Skip commits not touching prefix *) 38 match Commit.parents commit with 39 | [] -> (old_hash, rewritten) 40 | p :: _ -> rewrite_commit p rewritten 41 else 42 (* Rewrite parents first *) 43 let parents, rewritten = 44 List.fold_left 45 (fun (parents, rw) p -> 46 let new_p, rw = rewrite_commit p rw in 47 (new_p :: parents, rw)) 48 ([], rewritten) (Commit.parents commit) 49 in 50 let parents = List.rev parents in 51 52 (* Extract subtree *) 53 let tree = Store.read_tree store (Commit.tree commit) in 54 match extract_subtree tree prefix with 55 | None -> (old_hash, rewritten) 56 | Some subtree -> 57 let new_hash = 58 Store.commit new_store ~tree:subtree ~parents 59 ~message:(Commit.message commit) 60 ~author:(Commit.author commit) 61 in 62 (new_hash, (old_hash, new_hash) :: rewritten)) 63 in 64 65 (* Start from main branch head *) 66 (match Store.head store ~branch:"main" with 67 | Some head -> 68 let new_head, _ = rewrite_commit head [] in 69 Store.set_head new_store ~branch:"main" new_head 70 | None -> ()); 71 72 new_store 73 74 (* Add: Add external repo as subtree *) 75 let add store ~prefix ~source = 76 match Store.head source ~branch:"main" with 77 | None -> failwith "Source has no main branch" 78 | Some source_head -> ( 79 match Store.read_commit source source_head with 80 | None -> failwith "Cannot read source commit" 81 | Some source_commit -> 82 let source_tree = 83 Store.read_tree source (Commit.tree source_commit) 84 in 85 86 (* Get current tree or empty *) 87 let current_tree = 88 match Store.head store ~branch:"main" with 89 | None -> Tree.empty () 90 | Some h -> ( 91 match Store.read_commit store h with 92 | None -> Tree.empty () 93 | Some c -> Store.read_tree store (Commit.tree c)) 94 in 95 96 (* Add source tree at prefix *) 97 let new_tree = Tree.add_tree current_tree prefix source_tree in 98 99 let parents = 100 match Store.head store ~branch:"main" with 101 | None -> [] 102 | Some h -> [ h ] 103 in 104 105 let message = 106 Printf.sprintf "Add '%s' from external source" 107 (String.concat "/" prefix) 108 in 109 110 let new_head = 111 Store.commit store ~tree:new_tree ~parents ~message 112 ~author:"irmin-subtree" 113 in 114 Store.set_head store ~branch:"main" new_head; 115 new_head) 116 117 (* Pull: Update subtree from external source *) 118 let pull store ~prefix ~source = 119 match Store.head source ~branch:"main" with 120 | None -> Error (`Conflict []) 121 | Some source_head -> ( 122 match Store.read_commit source source_head with 123 | None -> Error (`Conflict []) 124 | Some source_commit -> 125 let source_tree = 126 Store.read_tree source (Commit.tree source_commit) 127 in 128 129 let current_tree = 130 match Store.head store ~branch:"main" with 131 | None -> Tree.empty () 132 | Some h -> ( 133 match Store.read_commit store h with 134 | None -> Tree.empty () 135 | Some c -> Store.read_tree store (Commit.tree c)) 136 in 137 138 (* Replace subtree at prefix *) 139 let new_tree = 140 let without = Tree.remove current_tree prefix in 141 Tree.add_tree without prefix source_tree 142 in 143 144 let parents = 145 match Store.head store ~branch:"main" with 146 | None -> [] 147 | Some h -> [ h ] 148 in 149 150 let message = 151 Printf.sprintf "Pull updates into '%s'" (String.concat "/" prefix) 152 in 153 154 let new_head = 155 Store.commit store ~tree:new_tree ~parents ~message 156 ~author:"irmin-subtree" 157 in 158 Store.set_head store ~branch:"main" new_head; 159 Ok new_head) 160 161 (* Push: Push subtree changes to external repo *) 162 let push store ~prefix ~target = 163 match Store.head store ~branch:"main" with 164 | None -> failwith "Store has no main branch" 165 | Some head -> ( 166 match Store.read_commit store head with 167 | None -> failwith "Cannot read store commit" 168 | Some commit -> ( 169 let tree = Store.read_tree store (Commit.tree commit) in 170 match extract_subtree tree prefix with 171 | None -> failwith "No subtree at prefix" 172 | Some subtree -> 173 let parents = 174 match Store.head target ~branch:"main" with 175 | None -> [] 176 | Some h -> [ h ] 177 in 178 179 let message = 180 Printf.sprintf "Push from '%s'" (String.concat "/" prefix) 181 in 182 183 let new_head = 184 Store.commit target ~tree:subtree ~parents ~message 185 ~author:"irmin-subtree" 186 in 187 Store.set_head target ~branch:"main" new_head; 188 new_head)) 189 190 (* Status: Compare subtree with external repo *) 191 let status store ~prefix ~external_ = 192 let local_head = Store.head store ~branch:"main" in 193 let remote_head = Store.head external_ ~branch:"main" in 194 195 match (local_head, remote_head) with 196 | None, None -> `In_sync 197 | None, Some _ -> `Remote_ahead 1 198 | Some _, None -> `Local_ahead 1 199 | Some lh, Some rh -> ( 200 (* Get subtree hash from local *) 201 let local_tree_hash = 202 match Store.read_commit store lh with 203 | None -> None 204 | Some c -> ( 205 let tree = Store.read_tree store (Commit.tree c) in 206 match Tree.find_tree tree prefix with 207 | None -> None 208 | Some t -> Some (Tree.hash t ~backend:(Store.backend store))) 209 in 210 211 (* Get tree hash from remote *) 212 let remote_tree_hash = 213 match Store.read_commit external_ rh with 214 | None -> None 215 | Some c -> Some (Commit.tree c) 216 in 217 218 match (local_tree_hash, remote_tree_hash) with 219 | None, None -> `In_sync 220 | None, Some _ -> `Remote_ahead 1 221 | Some _, None -> `Local_ahead 1 222 | Some lt, Some rt -> 223 if F.hash_equal lt rt then `In_sync 224 else if 225 (* Check ancestry *) 226 Store.is_ancestor external_ ~ancestor:rt ~descendant:lt 227 then 228 `Local_ahead (Store.commits_between external_ ~base:rt ~head:lt) 229 else if Store.is_ancestor external_ ~ancestor:lt ~descendant:rt then 230 `Remote_ahead (Store.commits_between external_ ~base:lt ~head:rt) 231 else `Trees_differ) 232end 233 234module Git = Make (Codec.Git) 235module Mst = Make (Codec.Mst)