(* Copyright (c) 2013-2017 Thomas Gazagnaire Copyright (c) 2017-2024 Romain Calascibetta Copyright (c) 2024-2026 Thomas Gazagnaire Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) (** Git user information (author/committer). *) type tz_offset = { sign : [ `Plus | `Minus ]; hours : int; minutes : int } type t = { name : string; email : string; date : int64 * tz_offset option } let v ~name ~email ~date ?tz_offset () = { name; email; date = (date, tz_offset) } let name t = t.name let email t = t.email let date t = fst t.date let tz_offset t = snd t.date let pp_tz_offset ppf { sign; hours; minutes } = let sign_char = match sign with `Plus -> '+' | `Minus -> '-' in Fmt.pf ppf "%c%02d%02d" sign_char hours minutes let pp ppf t = Fmt.pf ppf "%s <%s> %Ld" t.name t.email (fst t.date); match snd t.date with | Some tz -> Fmt.pf ppf " %a" pp_tz_offset tz | None -> Fmt.pf ppf " +0000" let equal a b = String.equal a.name b.name && String.equal a.email b.email && Int64.equal (fst a.date) (fst b.date) let compare a b = match String.compare a.name b.name with | 0 -> ( match String.compare a.email b.email with | 0 -> Int64.compare (fst a.date) (fst b.date) | n -> n) | n -> n (** Encode user to git format: "Name timestamp timezone" *) let to_string t = let tz_str = match snd t.date with | Some { sign; hours; minutes } -> let sign_char = match sign with `Plus -> '+' | `Minus -> '-' in Fmt.str "%c%02d%02d" sign_char hours minutes | None -> "+0000" in Fmt.str "%s <%s> %Ld %s" t.name t.email (fst t.date) tz_str (** Parse user from git format. *) let of_string s = (* Format: "Name timestamp timezone" *) match String.rindex_opt s '<' with | None -> Error (`Msg ("Invalid user format: " ^ s)) | Some lt_pos -> ( match String.index_from_opt s lt_pos '>' with | None -> Error (`Msg ("Invalid user format: " ^ s)) | Some gt_pos -> ( let name = String.trim (String.sub s 0 lt_pos) in let email = String.sub s (lt_pos + 1) (gt_pos - lt_pos - 1) in let rest = String.sub s (gt_pos + 2) (String.length s - gt_pos - 2) in let parts = String.split_on_char ' ' rest in match parts with | [ timestamp; tz ] -> ( match Int64.of_string_opt timestamp with | None -> Error (`Msg ("Invalid timestamp: " ^ timestamp)) | Some ts -> let tz_offset = if String.length tz >= 5 then let sign = match tz.[0] with | '+' -> `Plus | '-' -> `Minus | _ -> `Plus in let hours = int_of_string_opt (String.sub tz 1 2) |> Option.value ~default:0 in let minutes = int_of_string_opt (String.sub tz 3 2) |> Option.value ~default:0 in Some { sign; hours; minutes } else None in Ok { name; email; date = (ts, tz_offset) }) | _ -> Error (`Msg ("Invalid user date format: " ^ rest))))