My aggregated monorepo of OCaml code, automaintained
at http2 256 lines 8.8 kB view raw
1type cmd_result = { exit_code : int; stdout : string; stderr : string } 2 3type error = 4 | Command_failed of string * cmd_result 5 | Not_a_repo of Fpath.t 6 | Dirty_worktree of Fpath.t 7 | Remote_not_found of string 8 | Branch_not_found of string 9 | Subtree_prefix_exists of string 10 | Subtree_prefix_missing of string 11 | Io_error of string 12 13let pp_error ppf = function 14 | Command_failed (cmd, r) -> 15 Fmt.pf ppf "Command failed: %s (exit %d)@.stdout: %s@.stderr: %s" cmd 16 r.exit_code r.stdout r.stderr 17 | Not_a_repo path -> Fmt.pf ppf "Not a git repository: %a" Fpath.pp path 18 | Dirty_worktree path -> 19 Fmt.pf ppf "Repository has uncommitted changes: %a" Fpath.pp path 20 | Remote_not_found name -> Fmt.pf ppf "Remote not found: %s" name 21 | Branch_not_found name -> Fmt.pf ppf "Branch not found: %s" name 22 | Subtree_prefix_exists prefix -> 23 Fmt.pf ppf "Subtree prefix already exists: %s" prefix 24 | Subtree_prefix_missing prefix -> 25 Fmt.pf ppf "Subtree prefix does not exist: %s" prefix 26 | Io_error msg -> Fmt.pf ppf "I/O error: %s" msg 27 28let run_git ~proc ~cwd args = 29 let cmd = "git" :: args in 30 let buf_stdout = Buffer.create 256 in 31 let buf_stderr = Buffer.create 256 in 32 Eio.Switch.run @@ fun sw -> 33 let child = 34 Eio.Process.spawn proc ~sw ~cwd 35 ~stdout:(Eio.Flow.buffer_sink buf_stdout) 36 ~stderr:(Eio.Flow.buffer_sink buf_stderr) 37 cmd 38 in 39 let exit_status = Eio.Process.await child in 40 let exit_code = 41 match exit_status with `Exited n -> n | `Signaled n -> 128 + n 42 in 43 { 44 exit_code; 45 stdout = Buffer.contents buf_stdout |> String.trim; 46 stderr = Buffer.contents buf_stderr |> String.trim; 47 } 48 49let run_git_ok ~proc ~cwd args = 50 let result = run_git ~proc ~cwd args in 51 if result.exit_code = 0 then Ok result.stdout 52 else Error (Command_failed (String.concat " " ("git" :: args), result)) 53 54let path_to_eio ~(fs : Eio.Fs.dir_ty Eio.Path.t) path = 55 let dir, _ = fs in 56 (dir, Fpath.to_string path) 57 58let is_repo ~proc ~fs path = 59 let cwd = path_to_eio ~fs path in 60 let result = run_git ~proc ~cwd [ "rev-parse"; "--git-dir" ] in 61 result.exit_code = 0 62 63let is_dirty ~proc ~fs path = 64 let cwd = path_to_eio ~fs path in 65 let result = run_git ~proc ~cwd [ "status"; "--porcelain" ] in 66 result.exit_code = 0 && result.stdout <> "" 67 68let current_branch ~proc ~fs path = 69 let cwd = path_to_eio ~fs path in 70 let result = run_git ~proc ~cwd [ "symbolic-ref"; "--short"; "HEAD" ] in 71 if result.exit_code = 0 then Some result.stdout else None 72 73let head_commit ~proc ~fs path = 74 let cwd = path_to_eio ~fs path in 75 run_git_ok ~proc ~cwd [ "rev-parse"; "HEAD" ] 76 77let rev_parse ~proc ~fs ~rev path = 78 let cwd = path_to_eio ~fs path in 79 run_git_ok ~proc ~cwd [ "rev-parse"; rev ] 80 81let clone ~proc ~fs ~url ~branch target = 82 let parent = Fpath.parent target in 83 let cwd = Eio.Path.(fs / Fpath.to_string parent) in 84 let target_name = Fpath.basename target in 85 let url_str = Uri.to_string url in 86 run_git_ok ~proc ~cwd [ "clone"; "--branch"; branch; url_str; target_name ] 87 |> Result.map ignore 88 89let fetch ~proc ~fs ?(remote = "origin") path = 90 let cwd = path_to_eio ~fs path in 91 run_git_ok ~proc ~cwd [ "fetch"; remote ] |> Result.map ignore 92 93let merge_ff ~proc ~fs ?(remote = "origin") ?branch path = 94 let cwd = path_to_eio ~fs path in 95 let branch = 96 match branch with 97 | Some b -> b 98 | None -> Option.value ~default:"main" (current_branch ~proc ~fs path) 99 in 100 let upstream = remote ^ "/" ^ branch in 101 run_git_ok ~proc ~cwd [ "merge"; "--ff-only"; upstream ] |> Result.map ignore 102 103let pull ~proc ~fs ?(remote = "origin") ?branch path = 104 let cwd = path_to_eio ~fs path in 105 let args = 106 match branch with 107 | Some b -> [ "pull"; remote; b ] 108 | None -> [ "pull"; remote ] 109 in 110 run_git_ok ~proc ~cwd args |> Result.map ignore 111 112let checkout ~proc ~fs ~branch path = 113 let cwd = path_to_eio ~fs path in 114 run_git_ok ~proc ~cwd [ "checkout"; branch ] |> Result.map ignore 115 116type ahead_behind = { ahead : int; behind : int } 117 118let ahead_behind ~proc ~fs ?(remote = "origin") ?branch path = 119 let cwd = path_to_eio ~fs path in 120 let branch = 121 match branch with 122 | Some b -> b 123 | None -> Option.value ~default:"HEAD" (current_branch ~proc ~fs path) 124 in 125 let upstream = remote ^ "/" ^ branch in 126 match 127 run_git_ok ~proc ~cwd 128 [ "rev-list"; "--left-right"; "--count"; branch ^ "..." ^ upstream ] 129 with 130 | Error e -> Error e 131 | Ok output -> ( 132 match String.split_on_char '\t' output with 133 | [ ahead; behind ] -> 134 Ok { ahead = int_of_string ahead; behind = int_of_string behind } 135 | _ -> Ok { ahead = 0; behind = 0 }) 136 137module Subtree = struct 138 let exists ~fs ~repo ~prefix = 139 let path = Eio.Path.(fs / Fpath.to_string repo / prefix) in 140 match Eio.Path.kind ~follow:true path with 141 | `Directory -> true 142 | _ -> false 143 | exception _ -> false 144 145 let add ~proc ~fs ~repo ~prefix ~url ~branch () = 146 if exists ~fs ~repo ~prefix then Error (Subtree_prefix_exists prefix) 147 else 148 let cwd = path_to_eio ~fs repo in 149 let url_str = Uri.to_string url in 150 run_git_ok ~proc ~cwd 151 [ "subtree"; "add"; "--prefix"; prefix; url_str; branch; "--squash" ] 152 |> Result.map ignore 153 154 let pull ~proc ~fs ~repo ~prefix ~url ~branch () = 155 if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix) 156 else 157 let cwd = path_to_eio ~fs repo in 158 let url_str = Uri.to_string url in 159 run_git_ok ~proc ~cwd 160 [ "subtree"; "pull"; "--prefix"; prefix; url_str; branch; "--squash" ] 161 |> Result.map ignore 162 163 let push ~proc ~fs ~repo ~prefix ~url ~branch () = 164 if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix) 165 else 166 let cwd = path_to_eio ~fs repo in 167 let url_str = Uri.to_string url in 168 run_git_ok ~proc ~cwd 169 [ "subtree"; "push"; "--prefix"; prefix; url_str; branch ] 170 |> Result.map ignore 171 172 let split ~proc ~fs ~repo ~prefix () = 173 if not (exists ~fs ~repo ~prefix) then Error (Subtree_prefix_missing prefix) 174 else 175 let cwd = path_to_eio ~fs repo in 176 run_git_ok ~proc ~cwd [ "subtree"; "split"; "--prefix"; prefix ] 177end 178 179let init ~proc ~fs path = 180 let cwd = path_to_eio ~fs (Fpath.parent path) in 181 let name = Fpath.basename path in 182 run_git_ok ~proc ~cwd [ "init"; name ] |> Result.map ignore 183 184let commit_allow_empty ~proc ~fs ~message path = 185 let cwd = path_to_eio ~fs path in 186 run_git_ok ~proc ~cwd [ "commit"; "--allow-empty"; "-m"; message ] 187 |> Result.map ignore 188 189let push_remote ~proc ~fs ?(remote = "origin") ?branch path = 190 let cwd = path_to_eio ~fs path in 191 let branch = 192 match branch with 193 | Some b -> b 194 | None -> Option.value ~default:"main" (current_branch ~proc ~fs path) 195 in 196 run_git_ok ~proc ~cwd [ "push"; remote; branch ] |> Result.map ignore 197 198let set_push_url ~proc ~fs ?(remote = "origin") ~url path = 199 let cwd = path_to_eio ~fs path in 200 run_git_ok ~proc ~cwd [ "remote"; "set-url"; "--push"; remote; url ] 201 |> Result.map ignore 202 203let get_push_url ~proc ~fs ?(remote = "origin") path = 204 let cwd = path_to_eio ~fs path in 205 match run_git_ok ~proc ~cwd [ "remote"; "get-url"; "--push"; remote ] with 206 | Ok url -> Some url 207 | Error _ -> None 208 209type log_entry = { 210 hash : string; 211 author : string; 212 date : string; 213 subject : string; 214 body : string; 215} 216 217let parse_log_entries output = 218 if String.trim output = "" then [] 219 else 220 (* Split by the record separator (NUL at end of each record) *) 221 let records = String.split_on_char '\x00' output in 222 (* Filter empty strings and parse each record *) 223 List.filter_map 224 (fun record -> 225 let record = String.trim record in 226 if record = "" then None 227 else 228 (* Each record is: hash\nauthor\ndate\nsubject\nbody *) 229 match String.split_on_char '\n' record with 230 | hash :: author :: date :: subject :: body_lines -> 231 Some 232 { 233 hash; 234 author; 235 date; 236 subject; 237 body = String.concat "\n" body_lines; 238 } 239 | _ -> None) 240 records 241 242let log ~proc ~fs ?since ?until ?path:(filter_path : string option) repo_path = 243 let cwd = path_to_eio ~fs repo_path in 244 (* Build args: use format with NUL separator between records *) 245 let format_arg = "--format=%H%n%an%n%aI%n%s%n%b%x00" in 246 let args = [ "log"; format_arg ] in 247 let args = 248 match since with Some s -> args @ [ "--since=" ^ s ] | None -> args 249 in 250 let args = 251 match until with Some u -> args @ [ "--until=" ^ u ] | None -> args 252 in 253 let args = match filter_path with Some p -> args @ [ "--"; p ] | None -> args in 254 match run_git_ok ~proc ~cwd args with 255 | Ok output -> Ok (parse_log_entries output) 256 | Error e -> Error e