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.

Initial commit: precommit - pre-commit hook initialization for OCaml projects

+347
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Dune package management 7 + dune.lock/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version=0.28.1
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+47
README.md
··· 1 + # precommit 2 + 3 + Pre-commit hook initialization for OCaml projects. 4 + 5 + ## Overview 6 + 7 + This tool sets up pre-commit hooks for OCaml projects: 8 + - **dune-format**: Automatically formats OCaml code using `dune fmt --auto` 9 + - **remove-claude-attribution**: Removes Claude co-author lines and rejects emojis in commit messages 10 + 11 + ## Installation 12 + 13 + ``` 14 + opam install precommit 15 + ``` 16 + 17 + ## Usage 18 + 19 + Run in an OCaml project directory (where `dune-project` exists): 20 + 21 + ```bash 22 + precommit init 23 + ``` 24 + 25 + This creates: 26 + - `.pre-commit-config.yaml` - Hook configuration 27 + - `.hooks/remove-claude-lines.py` - Commit message filter 28 + 29 + And runs `pre-commit install` to activate the hooks. 30 + 31 + ### Dry run 32 + 33 + To see what would be created without making changes: 34 + 35 + ```bash 36 + precommit init --dry-run 37 + ``` 38 + 39 + ## Requirements 40 + 41 + - [pre-commit](https://pre-commit.com/) must be installed 42 + - Python 3 (for the commit message hook) 43 + - dune (for code formatting) 44 + 45 + ## License 46 + 47 + MIT License. See [LICENSE.md](LICENSE.md) for details.
+4
bin/dune
··· 1 + (executable 2 + (name main) 3 + (public_name precommit) 4 + (libraries precommit cmdliner))
+33
bin/main.ml
··· 1 + (** CLI for pre-commit hook initialization. *) 2 + 3 + open Cmdliner 4 + 5 + let dry_run = 6 + let doc = "Print what would be done without making changes." in 7 + Arg.(value & flag & info [ "n"; "dry-run" ] ~doc) 8 + 9 + let init_cmd = 10 + let doc = "Initialize pre-commit hooks for an OCaml project." in 11 + let info = Cmd.info "init" ~doc in 12 + let term = 13 + Term.( 14 + const (fun dry_run -> 15 + match Precommit.init ~dry_run () with 16 + | Ok () -> 17 + if not dry_run then 18 + print_endline "Pre-commit hooks initialized successfully."; 19 + `Ok () 20 + | Error msg -> 21 + Printf.eprintf "Error: %s\n" msg; 22 + `Error (false, msg)) 23 + $ dry_run) 24 + in 25 + Cmd.v info (Term.ret term) 26 + 27 + let default_cmd = 28 + let doc = "Pre-commit hook initialization for OCaml projects." in 29 + let info = Cmd.info "precommit" ~version:"0.1.0" ~doc in 30 + let default = Term.(ret (const (`Help (`Pager, None)))) in 31 + Cmd.group info ~default [ init_cmd ] 32 + 33 + let () = exit (Cmd.eval default_cmd)
+24
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name precommit) 4 + 5 + (generate_opam_files true) 6 + 7 + (license MIT) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + (homepage "https://tangled.org/@gazagnaire.org/ocaml-precommit") 11 + (bug_reports "https://tangled.org/@gazagnaire.org/ocaml-precommit/issues") 12 + 13 + (package 14 + (name precommit) 15 + (synopsis "Pre-commit hook initialization for OCaml projects") 16 + (description 17 + "A CLI tool to initialize pre-commit hooks for OCaml projects. Sets up \ 18 + automatic formatting with dune fmt and removes Claude attribution from \ 19 + commit messages.") 20 + (depends 21 + (ocaml (>= 4.08)) 22 + (cmdliner (>= 1.2)) 23 + (ocamlformat :with-dev-setup) 24 + (alcotest :with-test)))
+4
lib/dune
··· 1 + (library 2 + (name precommit) 3 + (public_name precommit) 4 + (libraries unix))
+109
lib/precommit.ml
··· 1 + (** Pre-commit hook initialization for OCaml projects. *) 2 + 3 + let pre_commit_config = 4 + {|repos: 5 + - repo: local 6 + hooks: 7 + - id: dune-format 8 + name: Auto format with dune 9 + entry: dune fmt --auto 10 + language: system 11 + files: \.(ml|mli|mll|mly)$ 12 + stages: [pre-commit] 13 + pass_filenames: false 14 + 15 + - id: remove-claude-attribution 16 + name: Remove Claude attribution from commit message 17 + entry: python3 .hooks/remove-claude-lines.py 18 + language: system 19 + stages: [commit-msg] 20 + |} 21 + 22 + let remove_claude_lines = 23 + {|#!/usr/bin/env python3 24 + import sys 25 + import re 26 + 27 + def has_emoji(text): 28 + # Unicode ranges for emojis 29 + emoji_pattern = re.compile( 30 + "[" 31 + "\U0001F600-\U0001F64F" # emoticons 32 + "\U0001F300-\U0001F5FF" # symbols & pictographs 33 + "\U0001F680-\U0001F6FF" # transport & map symbols 34 + "\U0001F1E0-\U0001F1FF" # flags (iOS) 35 + "\U00002702-\U000027B0" # dingbats 36 + "\U000024C2-\U0001F251" 37 + "]+", flags=re.UNICODE) 38 + return bool(emoji_pattern.search(text)) 39 + 40 + def main(): 41 + commit_msg_file = sys.argv[1] 42 + with open(commit_msg_file, 'r', encoding='utf-8') as f: 43 + lines = f.readlines() 44 + 45 + # Check for emojis in the commit message 46 + commit_text = ''.join(lines) 47 + if has_emoji(commit_text): 48 + print("Error: Commit message contains emojis, which are not allowed.", file=sys.stderr) 49 + return 1 50 + 51 + filtered_lines = [line for line in lines if 'claude' not in line.lower()] 52 + 53 + with open(commit_msg_file, 'w', encoding='utf-8') as f: 54 + f.writelines(filtered_lines) 55 + 56 + return 0 57 + 58 + if __name__ == '__main__': 59 + sys.exit(main()) 60 + |} 61 + 62 + let file_exists path = Sys.file_exists path 63 + 64 + let mkdir_p path = 65 + if not (file_exists path) then Unix.mkdir path 0o755 66 + 67 + let write_file ~dry_run path content = 68 + if dry_run then Printf.printf "Would create %s\n" path 69 + else begin 70 + let oc = open_out path in 71 + output_string oc content; 72 + close_out oc 73 + end 74 + 75 + let chmod_exec ~dry_run path = 76 + if dry_run then Printf.printf "Would chmod +x %s\n" path 77 + else Unix.chmod path 0o755 78 + 79 + let run_command ~dry_run cmd = 80 + if dry_run then begin 81 + Printf.printf "Would run: %s\n" cmd; 82 + 0 83 + end 84 + else Sys.command cmd 85 + 86 + let init ~dry_run () = 87 + (* Check if we're in an OCaml project *) 88 + if not (file_exists "dune-project") then 89 + Error "No dune-project found. Run this from an OCaml project root." 90 + else begin 91 + (* Create .hooks directory *) 92 + if dry_run then Printf.printf "Would create .hooks/\n" 93 + else mkdir_p ".hooks"; 94 + 95 + (* Write .pre-commit-config.yaml *) 96 + write_file ~dry_run ".pre-commit-config.yaml" pre_commit_config; 97 + 98 + (* Write .hooks/remove-claude-lines.py *) 99 + write_file ~dry_run ".hooks/remove-claude-lines.py" remove_claude_lines; 100 + chmod_exec ~dry_run ".hooks/remove-claude-lines.py"; 101 + 102 + (* Run pre-commit install *) 103 + let cmd = "pre-commit install --hook-type pre-commit --hook-type commit-msg" in 104 + let exit_code = run_command ~dry_run cmd in 105 + if exit_code <> 0 then 106 + Error (Printf.sprintf "pre-commit install failed with exit code %d" exit_code) 107 + else 108 + Ok () 109 + end
+18
lib/precommit.mli
··· 1 + (** Pre-commit hook initialization for OCaml projects. *) 2 + 3 + (** {1 Templates} *) 4 + 5 + val pre_commit_config : string 6 + (** The content of [.pre-commit-config.yaml]. *) 7 + 8 + val remove_claude_lines : string 9 + (** The content of [.hooks/remove-claude-lines.py]. *) 10 + 11 + (** {1 Operations} *) 12 + 13 + val init : dry_run:bool -> unit -> (unit, string) result 14 + (** [init ~dry_run ()] initializes pre-commit hooks in the current directory. 15 + Creates [.pre-commit-config.yaml] and [.hooks/remove-claude-lines.py], 16 + then runs [pre-commit install]. 17 + 18 + If [dry_run] is [true], prints what would be done without making changes. *)
+32
precommit.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Pre-commit hook initialization for OCaml projects" 4 + description: 5 + "A CLI tool to initialize pre-commit hooks for OCaml projects. Sets up automatic formatting with dune fmt and removes Claude attribution from commit messages." 6 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 7 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + license: "MIT" 9 + homepage: "https://tangled.org/@gazagnaire.org/ocaml-precommit" 10 + bug-reports: "https://tangled.org/@gazagnaire.org/ocaml-precommit/issues" 11 + depends: [ 12 + "dune" {>= "3.0"} 13 + "ocaml" {>= "4.08"} 14 + "cmdliner" {>= "1.2"} 15 + "ocamlformat" {with-dev-setup} 16 + "alcotest" {with-test} 17 + "odoc" {with-doc} 18 + ] 19 + build: [ 20 + ["dune" "subst"] {dev} 21 + [ 22 + "dune" 23 + "build" 24 + "-p" 25 + name 26 + "-j" 27 + jobs 28 + "@install" 29 + "@runtest" {with-test} 30 + "@doc" {with-doc} 31 + ] 32 + ]
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries precommit alcotest))
+1
test/test.ml
··· 1 + let () = Alcotest.run "precommit" Test_precommit.suite
+33
test/test_precommit.ml
··· 1 + (** Tests for precommit. *) 2 + 3 + let contains s sub = 4 + let len = String.length sub in 5 + let rec check i = 6 + if i + len > String.length s then false 7 + else if String.sub s i len = sub then true 8 + else check (i + 1) 9 + in 10 + check 0 11 + 12 + let test_pre_commit_config_valid () = 13 + let config = Precommit.pre_commit_config in 14 + Alcotest.(check bool) "starts with repos:" true (String.sub config 0 6 = "repos:"); 15 + Alcotest.(check bool) "contains dune-format" true (contains config "dune-format"); 16 + Alcotest.(check bool) "contains commit-msg" true (contains config "commit-msg") 17 + 18 + let test_remove_claude_lines_valid () = 19 + let script = Precommit.remove_claude_lines in 20 + Alcotest.(check bool) "is python script" true (String.sub script 0 2 = "#!"); 21 + Alcotest.(check bool) "filters claude lines" true (contains script "'claude'"); 22 + Alcotest.(check bool) "checks emojis" true (contains script "has_emoji") 23 + 24 + let suite = 25 + [ 26 + ( "templates", 27 + [ 28 + Alcotest.test_case "pre_commit_config valid" `Quick 29 + test_pre_commit_config_valid; 30 + Alcotest.test_case "remove_claude_lines valid" `Quick 31 + test_remove_claude_lines_valid; 32 + ] ); 33 + ]