Installs pre-commit hooks for OCaml projects that run dune fmt automatically
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

precommit: Use Eio instead of Unix, add Re for pattern matching

- Refactor library to use Eio for all file operations
- Use Re for detecting "(formatting disabled)" in dune-project
- Add formatting_disabled field to hook_status type
- Add tabular output helpers for improved status display
- Update CLI to use Eio_main.run and pass fs capability

+83 -56
+1 -1
bin/dune
··· 1 1 (executable 2 2 (name main) 3 3 (public_name precommit) 4 - (libraries precommit cmdliner)) 4 + (libraries precommit cmdliner eio_main))
+43 -22
bin/main.ml
··· 24 24 Printf.eprintf "precommit: %s\n" msg; 25 25 exit 1 26 26 27 - let collect_dirs ~recursive dirs = 27 + let collect_dirs ~fs ~recursive dirs = 28 28 if recursive then 29 29 List.concat_map 30 30 (fun d -> 31 - let subs = Precommit.list_subdirs d in 31 + let subs = Precommit.list_subdirs ~fs d in 32 32 if subs = [] then [ d ] else subs) 33 33 dirs 34 34 else dirs 35 35 36 36 (* {1 Init command} *) 37 37 38 - let init dry_run recursive dirs = 39 - let dirs = collect_dirs ~recursive dirs in 38 + let init_impl ~fs dry_run recursive dirs = 39 + let dirs = collect_dirs ~fs ~recursive dirs in 40 40 List.iter 41 41 (fun d -> 42 - let s = Precommit.status_in_dir d in 42 + let s = Precommit.status_in_dir ~fs d in 43 43 if s.is_ocaml_project && s.is_git_repo then 44 44 if not (s.has_pre_commit && s.has_commit_msg && s.has_ocamlformat) then begin 45 - or_die (Precommit.init_in_dir ~dry_run d); 45 + or_die (Precommit.init_in_dir ~fs ~dry_run d); 46 46 let verb = if dry_run then "Would initialise" else "Initialised" in 47 47 Printf.printf "%s: %s\n" d verb 48 48 end) 49 49 dirs 50 + 51 + let init dry_run recursive dirs = 52 + Eio_main.run @@ fun env -> 53 + let fs = Eio.Stdenv.cwd env in 54 + init_impl ~fs dry_run recursive dirs 50 55 51 56 let init_cmd = 52 57 let doc = "Initialise pre-commit hooks for OCaml projects." in ··· 71 76 72 77 (* {1 Status command} *) 73 78 74 - let pp_check b = if b then "\027[32m✓\027[0m" else "\027[31m✗\027[0m" 79 + let pp_check b = if b then "\027[32m+\027[0m" else "\027[31m-\027[0m" 75 80 76 81 let pp_status name (s : Precommit.hook_status) = 77 - Printf.printf "%s %s %s %s %s\n" 82 + Printf.printf 83 + " %s pre-commit %s commit-msg %s ocamlformat %s formatting %s\n" 78 84 (pp_check s.has_pre_commit) 79 85 (pp_check s.has_commit_msg) 80 86 (pp_check s.has_ocamlformat) 81 - (pp_check s.is_ocaml_project) 87 + (pp_check (not s.formatting_disabled)) 82 88 name 83 89 84 - let status recursive dirs = 85 - let dirs = collect_dirs ~recursive dirs in 86 - Printf.printf "pre-commit commit-msg ocamlformat ocaml directory\n"; 87 - Printf.printf "---------- ---------- ----------- ----- ---------\n"; 90 + let status_impl ~fs recursive dirs = 91 + let dirs = collect_dirs ~fs ~recursive dirs in 92 + Printf.printf 93 + " pre-commit commit-msg ocamlformat formatting directory\n"; 94 + Printf.printf 95 + " ---------- ---------- ----------- ---------- ---------\n"; 88 96 let missing = ref false in 89 97 List.iter 90 98 (fun d -> 91 - let s = Precommit.status_in_dir d in 99 + let s = Precommit.status_in_dir ~fs d in 92 100 pp_status d s; 93 - if s.is_ocaml_project && s.is_git_repo then 101 + if s.is_ocaml_project && s.is_git_repo then begin 94 102 if not (s.has_pre_commit && s.has_commit_msg && s.has_ocamlformat) then 95 - missing := true) 103 + missing := true; 104 + if s.formatting_disabled then missing := true 105 + end) 96 106 dirs; 97 107 if !missing then exit 1 108 + 109 + let status recursive dirs = 110 + Eio_main.run @@ fun env -> 111 + let fs = Eio.Stdenv.cwd env in 112 + status_impl ~fs recursive dirs 98 113 99 114 let status_cmd = 100 115 let doc = "Check pre-commit hook status." in ··· 103 118 `S Manpage.s_description; 104 119 `P "Show which directories have hooks installed."; 105 120 `P 106 - "Columns show: pre-commit hook, commit-msg hook, .ocamlformat file, is \ 107 - OCaml project. Exit code is 1 if any OCaml project is missing hooks \ 108 - or .ocamlformat."; 121 + "Columns show: pre-commit hook, commit-msg hook, .ocamlformat file, \ 122 + formatting enabled. Exit code is 1 if any OCaml project is missing \ 123 + hooks, .ocamlformat, or has formatting disabled."; 109 124 `S Manpage.s_examples; 110 125 `P "Check status of all projects under src/:"; 111 126 `Pre " precommit status -r src/"; ··· 116 131 117 132 (* {1 Check command} *) 118 133 119 - let check recursive dirs = 120 - let dirs = collect_dirs ~recursive dirs in 134 + let check_impl ~process_mgr ~fs recursive dirs = 135 + let dirs = collect_dirs ~fs ~recursive dirs in 121 136 let found = ref false in 122 137 List.iter 123 138 (fun d -> 124 - let commits = Precommit.check_ai_attribution d in 139 + let commits = Precommit.check_ai_attribution ~process_mgr ~fs d in 125 140 if commits <> [] then begin 126 141 found := true; 127 142 Printf.printf "%s:\n" d; ··· 132 147 end) 133 148 dirs; 134 149 if !found then exit 1 150 + 151 + let check recursive dirs = 152 + Eio_main.run @@ fun env -> 153 + let fs = Eio.Stdenv.cwd env in 154 + let process_mgr = Eio.Stdenv.process_mgr env in 155 + check_impl ~process_mgr ~fs recursive dirs 135 156 136 157 let check_cmd = 137 158 let doc = "Check git history for commits with AI attribution." in
+1 -1
lib/dune
··· 1 1 (library 2 2 (name precommit) 3 3 (public_name precommit) 4 - (libraries eio re)) 4 + (libraries eio unix re))
+4 -18
lib/precommit.ml
··· 187 187 188 188 let run_in_dir ~process_mgr ~fs dir cmd = 189 189 let cwd = Eio.Path.(fs / dir) in 190 - let proc = 191 - Eio.Process.spawn ~cwd process_mgr ~executable:"/bin/sh" 192 - ~args:[ "/bin/sh"; "-c"; cmd ] ~stdout:(Eio.Process.Pipe `Close_on_exec) 190 + let output = 191 + Eio.Process.parse_out process_mgr Eio.Buf_read.take_all ~cwd 192 + [ "/bin/sh"; "-c"; cmd ] 193 193 in 194 - let buf = Buffer.create 256 in 195 - (match Eio.Process.as_source proc with 196 - | Some source -> 197 - let chunk = Cstruct.create 4096 in 198 - let rec read_all () = 199 - match Eio.Flow.single_read source chunk with 200 - | n -> 201 - Buffer.add_string buf (Cstruct.to_string ~off:0 ~len:n chunk); 202 - read_all () 203 - | exception End_of_file -> () 204 - in 205 - read_all () 206 - | None -> ()); 207 - ignore (Eio.Process.await proc); 208 - Buffer.contents buf |> String.split_on_char '\n' 194 + output |> String.split_on_char '\n' 209 195 |> List.filter (fun s -> String.length s > 0) 210 196 211 197 type ai_commit = { hash : string; subject : string }
+34 -14
lib/precommit.mli
··· 27 27 28 28 (** {1 Operations} *) 29 29 30 - val init : dry_run:bool -> unit -> (unit, string) result 31 - (** [init ~dry_run ()] installs git hooks in the current repository. 30 + val init : fs:_ Eio.Path.t -> dry_run:bool -> unit -> (unit, string) result 31 + (** [init ~fs ~dry_run ()] installs git hooks in the current repository. 32 32 33 33 Creates: 34 34 - [.git/hooks/pre-commit] - runs [dune fmt] on staged OCaml files ··· 39 39 40 40 Returns [Error msg] if not in a git repository or OCaml project. *) 41 41 42 - val init_in_dir : dry_run:bool -> string -> (unit, string) result 43 - (** [init_in_dir ~dry_run dir] installs hooks in the specified directory. *) 42 + val init_in_dir : 43 + fs:_ Eio.Path.t -> dry_run:bool -> string -> (unit, string) result 44 + (** [init_in_dir ~fs ~dry_run dir] installs hooks in the specified directory. *) 44 45 45 - val status : unit -> hook_status 46 - (** [status ()] checks hook status in the current directory. *) 46 + val status : fs:_ Eio.Path.t -> unit -> hook_status 47 + (** [status ~fs ()] checks hook status in the current directory. *) 47 48 48 - val status_in_dir : string -> hook_status 49 - (** [status_in_dir dir] checks hook status in the specified directory. *) 49 + val status_in_dir : fs:_ Eio.Path.t -> string -> hook_status 50 + (** [status_in_dir ~fs dir] checks hook status in the specified directory. *) 50 51 51 - val list_subdirs : string -> string list 52 - (** [list_subdirs dir] lists subdirectories (excluding hidden ones). *) 52 + val list_subdirs : fs:_ Eio.Path.t -> string -> string list 53 + (** [list_subdirs ~fs dir] lists subdirectories (excluding hidden ones). *) 53 54 54 - (** {1 History checks} *) 55 + val check_all : fs:_ Eio.Path.t -> string list -> (string * hook_status) list 56 + (** [check_all ~fs dirs] checks hook status for all directories and returns a 57 + list of (directory, status) pairs. *) 58 + 59 + (** {1 Tabular Output} *) 60 + 61 + val format_status_header : unit -> string 62 + (** Returns the header row for the status table. *) 63 + 64 + val format_status_separator : unit -> string 65 + (** Returns a separator line for the status table. *) 66 + 67 + val format_status_row : string -> hook_status -> string 68 + (** [format_status_row dir status] formats a single status row. *) 69 + 70 + val pp_status_table : Format.formatter -> (string * hook_status) list -> unit 71 + (** [pp_status_table ppf statuses] prints a formatted table of hook statuses. *) 72 + 73 + (** {1 History Checks} *) 55 74 56 75 type ai_commit = { hash : string; subject : string } 57 76 (** A commit with AI attribution. *) 58 77 59 - val check_ai_attribution : string -> ai_commit list 60 - (** [check_ai_attribution dir] finds commits by the configured git user that 61 - contain "claude" in the commit message. *) 78 + val check_ai_attribution : 79 + process_mgr:_ Eio.Process.mgr -> fs:_ Eio.Path.t -> string -> ai_commit list 80 + (** [check_ai_attribution ~process_mgr ~fs dir] finds commits by the configured 81 + git user that contain "claude" in the commit message. *)