Git object storage and pack files for Eio
at main 227 lines 8.5 kB view raw
1(** Tree and commit diff operations. *) 2 3type change = 4 | Added of { path : string; hash : Hash.t; perm : Tree.perm } 5 | Removed of { path : string; hash : Hash.t; perm : Tree.perm } 6 | Modified of { 7 path : string; 8 old_hash : Hash.t; 9 new_hash : Hash.t; 10 old_perm : Tree.perm; 11 new_perm : Tree.perm; 12 } 13 14type t = change list 15type stats = { additions : int; deletions : int; modifications : int } 16 17let change_path = function 18 | Added { path; _ } | Removed { path; _ } | Modified { path; _ } -> path 19 20let compare_by_path c1 c2 = String.compare (change_path c1) (change_path c2) 21let path prefix name = if prefix = "" then name else prefix ^ "/" ^ name 22 23(** Read a tree from the repository, returning empty list if not found. *) 24let read_tree repo hash = 25 match Repository.read repo hash with 26 | Ok (Value.Tree tree) -> Ok (Tree.to_list tree) 27 | Ok _ -> Error (`Msg "expected tree object") 28 | Error e -> Error e 29 30(** Check if an entry is a directory (tree). *) 31let is_dir (entry : Tree.entry) = entry.perm = `Dir 32 33(** Recursively collect all entries in a tree as Added changes. *) 34let rec collect_added repo prefix entries acc = 35 List.fold_left 36 (fun acc (entry : Tree.entry) -> 37 match acc with 38 | Error _ -> acc 39 | Ok changes -> 40 let path = path prefix entry.name in 41 if is_dir entry then 42 (* Recurse into subdirectory *) 43 match read_tree repo entry.hash with 44 | Error e -> Error e 45 | Ok sub_entries -> collect_added repo path sub_entries (Ok changes) 46 else 47 Ok (Added { path; hash = entry.hash; perm = entry.perm } :: changes)) 48 acc entries 49 50(** Recursively collect all entries in a tree as Removed changes. *) 51let rec collect_removed repo prefix entries acc = 52 List.fold_left 53 (fun acc (entry : Tree.entry) -> 54 match acc with 55 | Error _ -> acc 56 | Ok changes -> 57 let path = path prefix entry.name in 58 if is_dir entry then 59 match read_tree repo entry.hash with 60 | Error e -> Error e 61 | Ok sub_entries -> 62 collect_removed repo path sub_entries (Ok changes) 63 else 64 Ok 65 (Removed { path; hash = entry.hash; perm = entry.perm } :: changes)) 66 acc entries 67 68(** Process a single added entry (file or directory). *) 69let process_added_entry repo prefix (entry : Tree.entry) changes = 70 let path = path prefix entry.name in 71 if is_dir entry then 72 match read_tree repo entry.hash with 73 | Error e -> Error e 74 | Ok sub -> collect_added repo path sub (Ok changes) 75 else Ok (Added { path; hash = entry.hash; perm = entry.perm } :: changes) 76 77(** Process a single removed entry (file or directory). *) 78let process_removed_entry repo prefix (entry : Tree.entry) changes = 79 let path = path prefix entry.name in 80 if is_dir entry then 81 match read_tree repo entry.hash with 82 | Error e -> Error e 83 | Ok sub -> collect_removed repo path sub (Ok changes) 84 else Ok (Removed { path; hash = entry.hash; perm = entry.perm } :: changes) 85 86(** Handle entries with same name but different content/type. *) 87let rec process_matched_entries repo prefix old_e new_e changes walk_fn = 88 let path = path prefix old_e.Tree.name in 89 match (is_dir old_e, is_dir new_e) with 90 | true, true -> ( 91 (* Both directories, recurse *) 92 match (read_tree repo old_e.hash, read_tree repo new_e.hash) with 93 | Ok old_entries', Ok new_entries' -> 94 walk_fn repo path old_entries' new_entries' (Ok changes) 95 | Error e, _ | _, Error e -> Error e) 96 | false, false -> 97 (* Both files, content changed *) 98 Ok 99 (Modified 100 { 101 path; 102 old_hash = old_e.hash; 103 new_hash = new_e.hash; 104 old_perm = old_e.perm; 105 new_perm = new_e.perm; 106 } 107 :: changes) 108 | true, false -> ( 109 (* Dir became file: remove dir contents, add file *) 110 match read_tree repo old_e.hash with 111 | Error e -> Error e 112 | Ok sub -> ( 113 match collect_removed repo path sub (Ok changes) with 114 | Error e -> Error e 115 | Ok changes' -> 116 Ok 117 (Added { path; hash = new_e.hash; perm = new_e.perm } 118 :: changes'))) 119 | false, true -> ( 120 (* File became dir: remove file, add dir contents *) 121 let change = Removed { path; hash = old_e.hash; perm = old_e.perm } in 122 match read_tree repo new_e.hash with 123 | Error e -> Error e 124 | Ok sub -> collect_added repo path sub (Ok (change :: changes))) 125 126(** Merge-walk two sorted entry lists and compute differences. *) 127and walk_trees repo prefix old_entries new_entries acc = 128 match (old_entries, new_entries, acc) with 129 | _, _, Error _ -> acc 130 | [], [], Ok changes -> Ok changes 131 | [], new_e :: rest, Ok changes -> ( 132 match process_added_entry repo prefix new_e changes with 133 | Error e -> Error e 134 | Ok changes' -> walk_trees repo prefix [] rest (Ok changes')) 135 | old_e :: rest, [], Ok changes -> ( 136 match process_removed_entry repo prefix old_e changes with 137 | Error e -> Error e 138 | Ok changes' -> walk_trees repo prefix rest [] (Ok changes')) 139 | old_e :: old_rest, new_e :: new_rest, Ok changes -> ( 140 let cmp = String.compare old_e.Tree.name new_e.Tree.name in 141 if cmp < 0 then 142 match process_removed_entry repo prefix old_e changes with 143 | Error e -> Error e 144 | Ok changes' -> 145 walk_trees repo prefix old_rest new_entries (Ok changes') 146 else if cmp > 0 then 147 match process_added_entry repo prefix new_e changes with 148 | Error e -> Error e 149 | Ok changes' -> 150 walk_trees repo prefix old_entries new_rest (Ok changes') 151 else if Hash.equal old_e.hash new_e.hash && old_e.perm = new_e.perm then 152 walk_trees repo prefix old_rest new_rest acc 153 else 154 match 155 process_matched_entries repo prefix old_e new_e changes walk_trees 156 with 157 | Error e -> Error e 158 | Ok changes' -> walk_trees repo prefix old_rest new_rest (Ok changes')) 159 160let trees repo ~old_tree ~new_tree = 161 let old_entries = read_tree repo old_tree in 162 let new_entries = read_tree repo new_tree in 163 match (old_entries, new_entries) with 164 | Error e, _ | _, Error e -> Error e 165 | Ok old_e, Ok new_e -> ( 166 match walk_trees repo "" old_e new_e (Ok []) with 167 | Error e -> Error e 168 | Ok changes -> Ok (List.sort compare_by_path changes)) 169 170let commits repo ~old_commit ~new_commit = 171 let read_commit_tree hash = 172 match Repository.read repo hash with 173 | Ok (Value.Commit commit) -> Ok (Commit.tree commit) 174 | Ok _ -> Error (`Msg "expected commit object") 175 | Error e -> Error e 176 in 177 match (read_commit_tree old_commit, read_commit_tree new_commit) with 178 | Error e, _ | _, Error e -> Error e 179 | Ok old_tree, Ok new_tree -> trees repo ~old_tree ~new_tree 180 181let tree_to_empty repo tree = 182 match read_tree repo tree with 183 | Error e -> Error e 184 | Ok entries -> ( 185 match collect_added repo "" entries (Ok []) with 186 | Error e -> Error e 187 | Ok changes -> Ok (List.sort compare_by_path changes)) 188 189let empty_to_tree repo tree = 190 match read_tree repo tree with 191 | Error e -> Error e 192 | Ok entries -> ( 193 match collect_removed repo "" entries (Ok []) with 194 | Error e -> Error e 195 | Ok changes -> Ok (List.sort compare_by_path changes)) 196 197let filter_by_path ~prefix diff = 198 let prefix_len = String.length prefix in 199 List.filter 200 (fun change -> 201 let path = change_path change in 202 String.length path >= prefix_len 203 && String.sub path 0 prefix_len = prefix 204 && (String.length path = prefix_len || path.[prefix_len] = '/')) 205 diff 206 207let stats diff = 208 List.fold_left 209 (fun acc change -> 210 match change with 211 | Added _ -> { acc with additions = acc.additions + 1 } 212 | Removed _ -> { acc with deletions = acc.deletions + 1 } 213 | Modified _ -> { acc with modifications = acc.modifications + 1 }) 214 { additions = 0; deletions = 0; modifications = 0 } 215 diff 216 217let pp_change ppf change = 218 match change with 219 | Added { path; _ } -> Fmt.pf ppf "A %s" path 220 | Removed { path; _ } -> Fmt.pf ppf "D %s" path 221 | Modified { path; _ } -> Fmt.pf ppf "M %s" path 222 223let pp ppf diff = Fmt.pf ppf "@[<v>%a@]" Fmt.(list ~sep:cut pp_change) diff 224 225let pp_stats ppf stats = 226 Fmt.pf ppf "%d additions, %d deletions, %d modifications" stats.additions 227 stats.deletions stats.modifications