Persistent store with Git semantics: lazy reads, delayed writes, content-addressing
at main 234 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 Fmt.str "Add '%s' from external source" (String.concat "/" prefix) 107 in 108 109 let new_head = 110 Store.commit store ~tree:new_tree ~parents ~message 111 ~author:"irmin-subtree" 112 in 113 Store.set_head store ~branch:"main" new_head; 114 new_head) 115 116 (* Pull: Update subtree from external source *) 117 let pull store ~prefix ~source = 118 match Store.head source ~branch:"main" with 119 | None -> Error (`Conflict []) 120 | Some source_head -> ( 121 match Store.read_commit source source_head with 122 | None -> Error (`Conflict []) 123 | Some source_commit -> 124 let source_tree = 125 Store.read_tree source (Commit.tree source_commit) 126 in 127 128 let current_tree = 129 match Store.head store ~branch:"main" with 130 | None -> Tree.empty () 131 | Some h -> ( 132 match Store.read_commit store h with 133 | None -> Tree.empty () 134 | Some c -> Store.read_tree store (Commit.tree c)) 135 in 136 137 (* Replace subtree at prefix *) 138 let new_tree = 139 let without = Tree.remove current_tree prefix in 140 Tree.add_tree without prefix source_tree 141 in 142 143 let parents = 144 match Store.head store ~branch:"main" with 145 | None -> [] 146 | Some h -> [ h ] 147 in 148 149 let message = 150 Fmt.str "Pull updates into '%s'" (String.concat "/" prefix) 151 in 152 153 let new_head = 154 Store.commit store ~tree:new_tree ~parents ~message 155 ~author:"irmin-subtree" 156 in 157 Store.set_head store ~branch:"main" new_head; 158 Ok new_head) 159 160 (* Push: Push subtree changes to external repo *) 161 let push store ~prefix ~target = 162 match Store.head store ~branch:"main" with 163 | None -> failwith "Store has no main branch" 164 | Some head -> ( 165 match Store.read_commit store head with 166 | None -> failwith "Cannot read store commit" 167 | Some commit -> ( 168 let tree = Store.read_tree store (Commit.tree commit) in 169 match extract_subtree tree prefix with 170 | None -> failwith "No subtree at prefix" 171 | Some subtree -> 172 let parents = 173 match Store.head target ~branch:"main" with 174 | None -> [] 175 | Some h -> [ h ] 176 in 177 178 let message = 179 Fmt.str "Push from '%s'" (String.concat "/" prefix) 180 in 181 182 let new_head = 183 Store.commit target ~tree:subtree ~parents ~message 184 ~author:"irmin-subtree" 185 in 186 Store.set_head target ~branch:"main" new_head; 187 new_head)) 188 189 (* Status: Compare subtree with external repo *) 190 let status store ~prefix ~external_ = 191 let local_head = Store.head store ~branch:"main" in 192 let remote_head = Store.head external_ ~branch:"main" in 193 194 match (local_head, remote_head) with 195 | None, None -> `In_sync 196 | None, Some _ -> `Remote_ahead 1 197 | Some _, None -> `Local_ahead 1 198 | Some lh, Some rh -> ( 199 (* Get subtree hash from local *) 200 let local_tree_hash = 201 match Store.read_commit store lh with 202 | None -> None 203 | Some c -> ( 204 let tree = Store.read_tree store (Commit.tree c) in 205 match Tree.find_tree tree prefix with 206 | None -> None 207 | Some t -> Some (Tree.hash t ~backend:(Store.backend store))) 208 in 209 210 (* Get tree hash from remote *) 211 let remote_tree_hash = 212 match Store.read_commit external_ rh with 213 | None -> None 214 | Some c -> Some (Commit.tree c) 215 in 216 217 match (local_tree_hash, remote_tree_hash) with 218 | None, None -> `In_sync 219 | None, Some _ -> `Remote_ahead 1 220 | Some _, None -> `Local_ahead 1 221 | Some lt, Some rt -> 222 if F.hash_equal lt rt then `In_sync 223 else if 224 (* Check ancestry *) 225 Store.is_ancestor external_ ~ancestor:rt ~descendant:lt 226 then 227 `Local_ahead (Store.commits_between external_ ~base:rt ~head:lt) 228 else if Store.is_ancestor external_ ~ancestor:lt ~descendant:rt then 229 `Remote_ahead (Store.commits_between external_ ~base:lt ~head:rt) 230 else `Trees_differ) 231end 232 233module Git = Make (Codec.Git) 234module Mst = Make (Codec.Mst)