Git object storage and pack files for Eio
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