Git object storage and pack files for Eio
at main 282 lines 9.9 kB view raw
1(* Copyright (c) 2013-2017 Thomas Gazagnaire <thomas@gazagnaire.org> 2 Copyright (c) 2017-2024 Romain Calascibetta <romain.calascibetta@gmail.com> 3 Copyright (c) 2024-2026 Thomas Gazagnaire <thomas@gazagnaire.org> 4 5 Permission to use, copy, modify, and distribute this software for any 6 purpose with or without fee is hereby granted, provided that the above 7 copyright notice and this permission notice appear in all copies. 8 9 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) 16 17(** Git commit objects. *) 18 19type t = { 20 tree : Hash.t; 21 parents : Hash.t list; 22 author : User.t; 23 committer : User.t; 24 extra : (string * string list) list; 25 message : string option; 26} 27 28let v ~tree ~author ~committer ?(parents = []) ?(extra = []) message = 29 { tree; parents; author; committer; extra; message } 30 31let tree t = t.tree 32let parents t = t.parents 33let author t = t.author 34let committer t = t.committer 35let message t = t.message 36let extra t = t.extra 37 38let pp ppf t = 39 Fmt.pf ppf "@[<v>tree %a@," Hash.pp t.tree; 40 List.iter (fun p -> Fmt.pf ppf "parent %a@," Hash.pp p) t.parents; 41 Fmt.pf ppf "author %a@," User.pp t.author; 42 Fmt.pf ppf "committer %a@," User.pp t.committer; 43 List.iter 44 (fun (k, vs) -> List.iter (fun v -> Fmt.pf ppf "%s %s@," k v) vs) 45 t.extra; 46 (match t.message with Some msg -> Fmt.pf ppf "@,%s" msg | None -> ()); 47 Fmt.pf ppf "@]" 48 49let equal a b = 50 Hash.equal a.tree b.tree 51 && List.equal Hash.equal a.parents b.parents 52 && User.equal a.author b.author 53 && User.equal a.committer b.committer 54 55let compare a b = 56 match Hash.compare a.tree b.tree with 57 | 0 -> ( 58 match List.compare Hash.compare a.parents b.parents with 59 | 0 -> User.compare a.author b.author 60 | n -> n) 61 | n -> n 62 63let hash t = Hashtbl.hash t 64 65let compare_by_date a b = 66 Int64.compare (User.date a.author) (User.date b.author) 67 68(** Encode commit to git format. *) 69let to_string t = 70 let buf = Buffer.create 512 in 71 Buffer.add_string buf "tree "; 72 Buffer.add_string buf (Hash.to_hex t.tree); 73 Buffer.add_char buf '\n'; 74 List.iter 75 (fun p -> 76 Buffer.add_string buf "parent "; 77 Buffer.add_string buf (Hash.to_hex p); 78 Buffer.add_char buf '\n') 79 t.parents; 80 Buffer.add_string buf "author "; 81 Buffer.add_string buf (User.to_string t.author); 82 Buffer.add_char buf '\n'; 83 Buffer.add_string buf "committer "; 84 Buffer.add_string buf (User.to_string t.committer); 85 Buffer.add_char buf '\n'; 86 List.iter 87 (fun (key, values) -> 88 List.iter 89 (fun value -> 90 Buffer.add_string buf key; 91 Buffer.add_char buf ' '; 92 Buffer.add_string buf value; 93 Buffer.add_char buf '\n') 94 values) 95 t.extra; 96 (match t.message with 97 | Some msg -> 98 Buffer.add_char buf '\n'; 99 Buffer.add_string buf msg 100 | None -> ()); 101 Buffer.contents buf 102 103(** Parse a single header line. *) 104let parse_header line = 105 match String.index_opt line ' ' with 106 | None -> None 107 | Some pos -> 108 let key = String.sub line 0 pos in 109 let value = String.sub line (pos + 1) (String.length line - pos - 1) in 110 Some (key, value) 111 112(** Parse commit from git format. *) 113let of_string s = 114 let lines = String.split_on_char '\n' s in 115 let rec parse_headers acc tree parents author committer extra = function 116 | [] -> Error (`Msg "Unexpected end of commit") 117 | "" :: rest -> ( 118 (* Empty line marks start of message *) 119 let message = 120 match rest with [] -> None | _ -> Some (String.concat "\n" rest) 121 in 122 match (tree, author, committer) with 123 | Some tree, Some author, Some committer -> 124 Ok 125 { 126 tree; 127 parents = List.rev parents; 128 author; 129 committer; 130 extra = List.rev extra; 131 message; 132 } 133 | None, _, _ -> Error (`Msg "Missing tree in commit") 134 | _, None, _ -> Error (`Msg "Missing author in commit") 135 | _, _, None -> Error (`Msg "Missing committer in commit")) 136 | line :: rest -> ( 137 match parse_header line with 138 | None -> Error (`Msg ("Invalid header line: " ^ line)) 139 | Some ("tree", hex) -> 140 let tree = Hash.of_hex hex in 141 parse_headers acc (Some tree) parents author committer extra rest 142 | Some ("parent", hex) -> 143 let parent = Hash.of_hex hex in 144 parse_headers acc tree (parent :: parents) author committer extra 145 rest 146 | Some ("author", user_str) -> ( 147 match User.of_string user_str with 148 | Ok user -> 149 parse_headers acc tree parents (Some user) committer extra rest 150 | Error _ as e -> e) 151 | Some ("committer", user_str) -> ( 152 match User.of_string user_str with 153 | Ok user -> 154 parse_headers acc tree parents author (Some user) extra rest 155 | Error _ as e -> e) 156 | Some (key, value) -> 157 (* Handle extra headers like gpgsig *) 158 let extra = 159 match List.assoc_opt key extra with 160 | Some values -> 161 (key, values @ [ value ]) :: List.remove_assoc key extra 162 | None -> (key, [ value ]) :: extra 163 in 164 parse_headers acc tree parents author committer extra rest) 165 in 166 parse_headers [] None [] None None [] lines 167 168let of_string_exn s = 169 match of_string s with Ok t -> t | Error (`Msg m) -> failwith m 170 171(** {1 Reader-based parsing} 172 173 Parse directly from a {!Bytesrw.Bytes.Reader.t} without materialising the 174 full object into a string. The reader must be positioned at the start of the 175 commit body (after the loose-object header, if any). *) 176 177module Reader = Bytesrw.Bytes.Reader 178module Slice = Bytesrw.Bytes.Slice 179 180(** Read until [delim] from a reader. Returns the bytes before the delimiter; 181 the delimiter is consumed. *) 182let read_until reader delim = 183 let buf = Buffer.create 128 in 184 let rec loop () = 185 match Reader.read reader with 186 | slice when Slice.is_eod slice -> 187 if Buffer.length buf = 0 then None else Some (Buffer.contents buf) 188 | slice -> 189 let bytes = Slice.bytes slice in 190 let first = Slice.first slice in 191 let len = Slice.length slice in 192 let rec scan i = 193 if i >= first + len then begin 194 Buffer.add_subbytes buf bytes first len; 195 loop () 196 end 197 else if Char.code delim = Bytes.get_uint8 bytes i then begin 198 Buffer.add_subbytes buf bytes first (i - first); 199 (* Push back the remaining bytes after the delimiter *) 200 let rest_off = i + 1 in 201 let rest_len = first + len - rest_off in 202 if rest_len > 0 then 203 Reader.push_back reader 204 (Slice.make bytes ~first:rest_off ~length:rest_len); 205 Some (Buffer.contents buf) 206 end 207 else scan (i + 1) 208 in 209 scan first 210 in 211 loop () 212 213(** Read all remaining bytes from a reader. *) 214let read_rest reader = 215 let buf = Buffer.create 256 in 216 Reader.add_to_buffer buf reader; 217 if Buffer.length buf = 0 then None else Some (Buffer.contents buf) 218 219let of_reader reader = 220 let rec parse_headers tree parents author committer extra = 221 match read_until reader '\n' with 222 | None -> Error (`Msg "Unexpected end of commit") 223 | Some "" -> ( 224 (* Empty line marks start of message *) 225 let message = read_rest reader in 226 match (tree, author, committer) with 227 | Some tree, Some author, Some committer -> 228 Ok 229 { 230 tree; 231 parents = List.rev parents; 232 author; 233 committer; 234 extra = List.rev extra; 235 message; 236 } 237 | None, _, _ -> Error (`Msg "Missing tree in commit") 238 | _, None, _ -> Error (`Msg "Missing author in commit") 239 | _, _, None -> Error (`Msg "Missing committer in commit")) 240 | Some line -> ( 241 match parse_header line with 242 | None -> Error (`Msg ("Invalid header line: " ^ line)) 243 | Some ("tree", hex) -> 244 let tree = Hash.of_hex hex in 245 parse_headers (Some tree) parents author committer extra 246 | Some ("parent", hex) -> 247 let parent = Hash.of_hex hex in 248 parse_headers tree (parent :: parents) author committer extra 249 | Some ("author", user_str) -> ( 250 match User.of_string user_str with 251 | Ok user -> parse_headers tree parents (Some user) committer extra 252 | Error _ as e -> e) 253 | Some ("committer", user_str) -> ( 254 match User.of_string user_str with 255 | Ok user -> parse_headers tree parents author (Some user) extra 256 | Error _ as e -> e) 257 | Some (key, value) -> 258 let extra = 259 match List.assoc_opt key extra with 260 | Some values -> 261 (key, values @ [ value ]) :: List.remove_assoc key extra 262 | None -> (key, [ value ]) :: extra 263 in 264 parse_headers tree parents author committer extra) 265 in 266 parse_headers None [] None None [] 267 268let digest t = 269 let s = to_string t in 270 Hash.digest_string ~kind:`Commit s 271 272module Set = Set.Make (struct 273 type nonrec t = t 274 275 let compare = compare 276end) 277 278module Map = Map.Make (struct 279 type nonrec t = t 280 281 let compare = compare 282end)