Git object storage and pack files for Eio
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 user information (author/committer). *)
18
19type tz_offset = { sign : [ `Plus | `Minus ]; hours : int; minutes : int }
20type t = { name : string; email : string; date : int64 * tz_offset option }
21
22let v ~name ~email ~date ?tz_offset () =
23 { name; email; date = (date, tz_offset) }
24
25let name t = t.name
26let email t = t.email
27let date t = fst t.date
28let tz_offset t = snd t.date
29
30let pp_tz_offset ppf { sign; hours; minutes } =
31 let sign_char = match sign with `Plus -> '+' | `Minus -> '-' in
32 Fmt.pf ppf "%c%02d%02d" sign_char hours minutes
33
34let pp ppf t =
35 Fmt.pf ppf "%s <%s> %Ld" t.name t.email (fst t.date);
36 match snd t.date with
37 | Some tz -> Fmt.pf ppf " %a" pp_tz_offset tz
38 | None -> Fmt.pf ppf " +0000"
39
40let equal a b =
41 String.equal a.name b.name
42 && String.equal a.email b.email
43 && Int64.equal (fst a.date) (fst b.date)
44
45let compare a b =
46 match String.compare a.name b.name with
47 | 0 -> (
48 match String.compare a.email b.email with
49 | 0 -> Int64.compare (fst a.date) (fst b.date)
50 | n -> n)
51 | n -> n
52
53(** Encode user to git format: "Name <email> timestamp timezone" *)
54let to_string t =
55 let tz_str =
56 match snd t.date with
57 | Some { sign; hours; minutes } ->
58 let sign_char = match sign with `Plus -> '+' | `Minus -> '-' in
59 Fmt.str "%c%02d%02d" sign_char hours minutes
60 | None -> "+0000"
61 in
62 Fmt.str "%s <%s> %Ld %s" t.name t.email (fst t.date) tz_str
63
64(** Parse user from git format. *)
65let of_string s =
66 (* Format: "Name <email> timestamp timezone" *)
67 match String.rindex_opt s '<' with
68 | None -> Error (`Msg ("Invalid user format: " ^ s))
69 | Some lt_pos -> (
70 match String.index_from_opt s lt_pos '>' with
71 | None -> Error (`Msg ("Invalid user format: " ^ s))
72 | Some gt_pos -> (
73 let name = String.trim (String.sub s 0 lt_pos) in
74 let email = String.sub s (lt_pos + 1) (gt_pos - lt_pos - 1) in
75 let rest = String.sub s (gt_pos + 2) (String.length s - gt_pos - 2) in
76 let parts = String.split_on_char ' ' rest in
77 match parts with
78 | [ timestamp; tz ] -> (
79 match Int64.of_string_opt timestamp with
80 | None -> Error (`Msg ("Invalid timestamp: " ^ timestamp))
81 | Some ts ->
82 let tz_offset =
83 if String.length tz >= 5 then
84 let sign =
85 match tz.[0] with
86 | '+' -> `Plus
87 | '-' -> `Minus
88 | _ -> `Plus
89 in
90 let hours =
91 int_of_string_opt (String.sub tz 1 2)
92 |> Option.value ~default:0
93 in
94 let minutes =
95 int_of_string_opt (String.sub tz 3 2)
96 |> Option.value ~default:0
97 in
98 Some { sign; hours; minutes }
99 else None
100 in
101 Ok { name; email; date = (ts, tz_offset) })
102 | _ -> Error (`Msg ("Invalid user date format: " ^ rest))))