(** Pre-commit hook initialization for OCaml projects. Installs git hooks directly without requiring the pre-commit tool. *) (** {1 Context} *) type ctx (** Filesystem context for operations. Contains the working directory for relative paths and the full filesystem for walking up to parent directories. *) val ctx : cwd:_ Eio.Path.t -> fs:_ Eio.Path.t -> ctx (** [ctx ~cwd ~fs] creates a context from the working directory and full filesystem paths. *) (** {1 Hook Templates} *) val pre_commit_hook : string (** Shell script for the pre-commit hook. Runs [dune fmt] on staged OCaml files and fails if formatting changes are needed. *) val commit_msg_hook : string (** Shell script for the commit-msg hook. Checks for emojis (rejected) and removes lines containing "claude" (case-insensitive). *) (** {1 Types} *) type hook_status = { has_pre_commit : bool; has_commit_msg : bool; has_ocamlformat : bool; formatting_disabled : bool; is_ocaml_project : bool; is_git_repo : bool; } (** Status of hooks in a directory. [formatting_disabled] is [true] if dune-project contains "(formatting disabled)". *) (** {1 Operations} *) type hooks = { fmt : bool; ai : bool } (** Which hooks to install. *) val all_hooks : hooks (** Both fmt and ai hooks. *) val init : ctx -> dry_run:bool -> force:bool -> hooks:hooks -> unit -> (unit, string) result (** [init ctx ~dry_run ~force ~hooks ()] installs git hooks in the current repository. Creates (depending on [hooks]): - [.git/hooks/pre-commit] - runs [dune fmt] on staged OCaml files - [.git/hooks/commit-msg] - checks for emojis and removes Claude attribution - [.ocamlformat] - if missing, creates with default version (unless [force]) If [dry_run] is [true], prints what would be done without making changes. If [force] is [true], skips dune-project check and .ocamlformat creation. Returns [Error msg] if not in a git repository. *) val init_in_dir : ctx -> dry_run:bool -> force:bool -> hooks:hooks -> string -> (unit, string) result (** [init_in_dir ctx ~dry_run ~force ~hooks dir] installs hooks in the specified directory. *) val status : ctx -> unit -> hook_status (** [status ctx ()] checks hook status in the current directory. *) val status_in_dir : ctx -> string -> hook_status (** [status_in_dir ctx dir] checks hook status in the specified directory. *) val list_subdirs : fs:_ Eio.Path.t -> string -> string list (** [list_subdirs ~fs dir] lists subdirectories (excluding hidden ones). *) val git_projects : ctx -> string -> string list (** [git_projects ctx dir] recursively scans [dir] for directories containing a [.git] entry. Stops recursing into a directory once a [.git] is found. Hidden directories are skipped. If [dir] is inside a git repository (by checking parent directories), it is included even if it doesn't contain [.git] directly. *) val check_all : ctx -> string list -> (string * hook_status) list (** [check_all ctx dirs] checks hook status for all directories and returns a list of (directory, status) pairs. *) (** {1 Tabular Output} *) val format_status_header : unit -> string (** Returns the header row for the status table. *) val format_status_separator : unit -> string (** Returns a separator line for the status table. *) val format_status_row : string -> hook_status -> string (** [format_status_row dir status] formats a single status row. *) val pp_status_table : Format.formatter -> (string * hook_status) list -> unit (** [pp_status_table ppf statuses] prints a formatted table of hook statuses. *) (** {1 History Checks} *) type ai_commit = { hash : string; subject : string } (** A commit with AI attribution. *) val check_ai_attribution : ctx -> string -> ai_commit list (** [check_ai_attribution ctx dir] uses ocaml-git to find commits that contain AI attribution patterns in the commit message. Only checks commits authored by the current user (determined from git config). *) (** {1 History Rewriting} *) val current_branch : ctx -> string -> string option (** [current_branch ctx dir] returns the current branch name, or [None] if HEAD is detached or no git root is found. Uses ocaml-git. *) val backup_branch : ctx -> string -> string (** [backup_branch ctx dir] creates a backup branch named [backup/-before-fix-] and returns the backup name. Uses ocaml-git. Raises [Failure] if no git root is found. *) val rewrite_ai_attribution : ctx -> string -> (int, string) result (** [rewrite_ai_attribution ctx dir] uses ocaml-git to rewrite commits and remove [Co-Authored-By:.*claude] lines from commit messages. Only rewrites commits authored by the current user. Returns [Ok n] where [n] is the number of commits rewritten, or [Error msg] on failure. *)