Installs pre-commit hooks for OCaml projects that run dune fmt automatically
1(** Pre-commit hook initialization for OCaml projects.
2
3 Installs git hooks directly without requiring the pre-commit tool. *)
4
5(** {1 Context} *)
6
7type ctx
8(** Filesystem context for operations. Contains the working directory for
9 relative paths and the full filesystem for walking up to parent directories.
10*)
11
12val ctx : cwd:_ Eio.Path.t -> fs:_ Eio.Path.t -> ctx
13(** [ctx ~cwd ~fs] creates a context from the working directory and full
14 filesystem paths. *)
15
16(** {1 Hook Templates} *)
17
18val pre_commit_hook : string
19(** Shell script for the pre-commit hook. Runs [dune fmt] on staged OCaml files
20 and fails if formatting changes are needed. *)
21
22val commit_msg_hook : string
23(** Shell script for the commit-msg hook. Checks for emojis (rejected) and
24 removes lines containing "claude" (case-insensitive). *)
25
26(** {1 Types} *)
27
28type hook_status = {
29 has_pre_commit : bool;
30 has_commit_msg : bool;
31 has_ocamlformat : bool;
32 formatting_disabled : bool;
33 is_ocaml_project : bool;
34 is_git_repo : bool;
35}
36(** Status of hooks in a directory. [formatting_disabled] is [true] if
37 dune-project contains "(formatting disabled)". *)
38
39(** {1 Operations} *)
40
41type hooks = { fmt : bool; ai : bool }
42(** Which hooks to install. *)
43
44val all_hooks : hooks
45(** Both fmt and ai hooks. *)
46
47val init :
48 ctx ->
49 dry_run:bool ->
50 force:bool ->
51 hooks:hooks ->
52 unit ->
53 (unit, string) result
54(** [init ctx ~dry_run ~force ~hooks ()] installs git hooks in the current
55 repository.
56
57 Creates (depending on [hooks]):
58 - [.git/hooks/pre-commit] - runs [dune fmt] on staged OCaml files
59 - [.git/hooks/commit-msg] - checks for emojis and removes Claude attribution
60 - [.ocamlformat] - if missing, creates with default version (unless [force])
61
62 If [dry_run] is [true], prints what would be done without making changes. If
63 [force] is [true], skips dune-project check and .ocamlformat creation.
64
65 Returns [Error msg] if not in a git repository. *)
66
67val init_in_dir :
68 ctx ->
69 dry_run:bool ->
70 force:bool ->
71 hooks:hooks ->
72 string ->
73 (unit, string) result
74(** [init_in_dir ctx ~dry_run ~force ~hooks dir] installs hooks in the specified
75 directory. *)
76
77val status : ctx -> unit -> hook_status
78(** [status ctx ()] checks hook status in the current directory. *)
79
80val status_in_dir : ctx -> string -> hook_status
81(** [status_in_dir ctx dir] checks hook status in the specified directory. *)
82
83val list_subdirs : fs:_ Eio.Path.t -> string -> string list
84(** [list_subdirs ~fs dir] lists subdirectories (excluding hidden ones). *)
85
86val git_projects : ctx -> string -> string list
87(** [git_projects ctx dir] recursively scans [dir] for directories containing a
88 [.git] entry. Stops recursing into a directory once a [.git] is found.
89 Hidden directories are skipped. If [dir] is inside a git repository (by
90 checking parent directories), it is included even if it doesn't contain
91 [.git] directly. *)
92
93val check_all : ctx -> string list -> (string * hook_status) list
94(** [check_all ctx dirs] checks hook status for all directories and returns a
95 list of (directory, status) pairs. *)
96
97(** {1 Tabular Output} *)
98
99val format_status_header : unit -> string
100(** Returns the header row for the status table. *)
101
102val format_status_separator : unit -> string
103(** Returns a separator line for the status table. *)
104
105val format_status_row : string -> hook_status -> string
106(** [format_status_row dir status] formats a single status row. *)
107
108val pp_status_table : Format.formatter -> (string * hook_status) list -> unit
109(** [pp_status_table ppf statuses] prints a formatted table of hook statuses. *)
110
111(** {1 History Checks} *)
112
113type ai_commit = { hash : string; subject : string }
114(** A commit with AI attribution. *)
115
116val check_ai_attribution : ctx -> string -> ai_commit list
117(** [check_ai_attribution ctx dir] uses ocaml-git to find commits that contain
118 AI attribution patterns in the commit message. Only checks commits authored
119 by the current user (determined from git config). *)
120
121(** {1 History Rewriting} *)
122
123val current_branch : ctx -> string -> string option
124(** [current_branch ctx dir] returns the current branch name, or [None] if HEAD
125 is detached or no git root is found. Uses ocaml-git. *)
126
127val backup_branch : ctx -> string -> string
128(** [backup_branch ctx dir] creates a backup branch named
129 [backup/<branch>-before-fix-<timestamp>] and returns the backup name. Uses
130 ocaml-git. Raises [Failure] if no git root is found. *)
131
132val rewrite_ai_attribution : ctx -> string -> (int, string) result
133(** [rewrite_ai_attribution ctx dir] uses ocaml-git to rewrite commits and
134 remove [Co-Authored-By:.*claude] lines from commit messages. Only rewrites
135 commits authored by the current user. Returns [Ok n] where [n] is the number
136 of commits rewritten, or [Error msg] on failure. *)