Git object storage and pack files for Eio
1(* Copyright (c) 2015 Daniel C. Bünzli
2 Copyright (c) 2020-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 references (branches, tags, HEAD). *)
18
19type t = string
20
21let head = "HEAD"
22let master = "refs/heads/master"
23let main = "refs/heads/main"
24
25let of_string s =
26 if String.length s = 0 then Error (`Msg "Empty reference")
27 else if String.contains s '\x00' then
28 Error (`Msg "Reference contains null byte")
29 else if s.[0] = '/' then Error (`Msg "Absolute reference not allowed")
30 else
31 (* Collapse consecutive slashes *)
32 let buf = Buffer.create (String.length s) in
33 let last_was_slash = ref false in
34 String.iter
35 (fun c ->
36 if c = '/' then (
37 if not !last_was_slash then Buffer.add_char buf c;
38 last_was_slash := true)
39 else (
40 Buffer.add_char buf c;
41 last_was_slash := false))
42 s;
43 Ok (Buffer.contents buf)
44
45let of_string_exn s =
46 match of_string s with Ok r -> r | Error (`Msg m) -> invalid_arg m
47
48let v = of_string_exn
49let to_string t = t
50let pp ppf t = Fmt.string ppf t
51let equal = String.equal
52let compare = String.compare
53let hash = Hashtbl.hash
54
55let segs t =
56 String.split_on_char '/' t |> List.filter (fun s -> String.length s > 0)
57
58let ( / ) t seg =
59 if String.contains seg '\x00' || String.contains seg '/' then
60 invalid_arg "Invalid segment";
61 if t.[String.length t - 1] = '/' then t ^ seg else t ^ "/" ^ seg
62
63let ( // ) t1 t2 =
64 if t2.[0] = '/' then t2
65 else if t1.[String.length t1 - 1] = '/' then t1 ^ t2
66 else t1 ^ "/" ^ t2
67
68(** Reference contents: either a direct hash or a symbolic reference. *)
69type contents = Hash of Hash.t | Ref of t
70
71let contents_equal a b =
72 match (a, b) with
73 | Hash a, Hash b -> Hash.equal a b
74 | Ref a, Ref b -> equal a b
75 | _ -> false
76
77let contents_compare a b =
78 match (a, b) with
79 | Hash a, Hash b -> Hash.compare a b
80 | Ref a, Ref b -> compare a b
81 | Hash _, Ref _ -> -1
82 | Ref _, Hash _ -> 1
83
84let pp_contents ppf = function
85 | Hash h -> Hash.pp ppf h
86 | Ref r -> Fmt.pf ppf "ref: %s" r
87
88(** Parse reference file contents. *)
89let contents_of_string s =
90 let s = String.trim s in
91 if String.length s = 0 then Error (`Msg "Empty reference contents")
92 else if String.length s >= 5 && String.sub s 0 5 = "ref: " then
93 let ref_path = String.sub s 5 (String.length s - 5) in
94 Result.map (fun r -> Ref r) (of_string ref_path)
95 else
96 (* Try to parse as a hex hash *)
97 try Ok (Hash (Hash.of_hex s))
98 with Invalid_argument _ ->
99 Error (`Msg ("Invalid reference contents: " ^ s))
100
101let contents_of_string_exn s =
102 match contents_of_string s with Ok c -> c | Error (`Msg m) -> failwith m
103
104(** Encode reference contents. *)
105let contents_to_string = function
106 | Hash h -> Hash.to_hex h ^ "\n"
107 | Ref r -> "ref: " ^ r ^ "\n"
108
109module Set = Set.Make (String)
110module Map = Map.Make (String)