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.

Update precommit to install hooks directly, fix build issues

precommit:
- Remove pre-commit tool dependency
- Install git hooks directly to .git/hooks/
- pre-commit hook runs dune fmt on staged OCaml files

spake2:
- Remove hkdf/pbkdf2 from interface (use separate packages)

matter:
- Use hkdf and digestif packages directly instead of via spake2
- Add hkdf pin for Tangled

hkdf:
- Add SHA-384 and SHA-512 variants

+121 -104
+87 -74
lib/precommit.ml
··· 1 - (** Pre-commit hook initialization for OCaml projects. *) 1 + (** Pre-commit hook initialization for OCaml projects. 2 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 3 + Installs git hooks directly without requiring the pre-commit tool. *) 4 + 5 + let pre_commit_hook = 6 + {|#!/bin/sh 7 + # Auto-format OCaml files with dune before commit 14 8 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 - |} 9 + # Check if any OCaml files are staged 10 + STAGED_ML=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ml|mli|mll|mly)$' || true) 21 11 22 - let remove_claude_lines = 23 - {|#!/usr/bin/env python3 24 - import sys 25 - import re 12 + if [ -n "$STAGED_ML" ]; then 13 + # Run dune fmt 14 + if ! dune fmt 2>/dev/null; then 15 + echo "Error: dune fmt failed" >&2 16 + exit 1 17 + fi 26 18 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)) 19 + # Check if formatting changed any staged files 20 + CHANGED=$(git diff --name-only $STAGED_ML 2>/dev/null || true) 21 + if [ -n "$CHANGED" ]; then 22 + echo "Files were reformatted by dune fmt:" 23 + echo "$CHANGED" 24 + echo "" 25 + echo "Please review and stage the changes, then commit again." 26 + exit 1 27 + fi 28 + fi 39 29 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() 30 + exit 0 31 + |} 44 32 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 33 + let commit_msg_hook = 34 + {|#!/bin/sh 35 + # Check commit message for emojis and remove Claude attribution lines 50 36 51 - filtered_lines = [line for line in lines if 'claude' not in line.lower()] 37 + COMMIT_MSG_FILE="$1" 52 38 53 - with open(commit_msg_file, 'w', encoding='utf-8') as f: 54 - f.writelines(filtered_lines) 39 + # Check for emojis using grep with Unicode ranges 40 + if grep -qP '[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{2702}-\x{27B0}\x{24C2}-\x{1F251}]' "$COMMIT_MSG_FILE" 2>/dev/null; then 41 + echo "Error: Commit message contains emojis, which are not allowed." >&2 42 + exit 1 43 + fi 55 44 56 - return 0 45 + # Remove lines containing 'claude' (case-insensitive) 46 + if grep -qi 'claude' "$COMMIT_MSG_FILE"; then 47 + # Use sed to filter out claude lines in-place 48 + if [ "$(uname)" = "Darwin" ]; then 49 + sed -i '' '/[Cc][Ll][Aa][Uu][Dd][Ee]/d' "$COMMIT_MSG_FILE" 50 + else 51 + sed -i '/[Cc][Ll][Aa][Uu][Dd][Ee]/d' "$COMMIT_MSG_FILE" 52 + fi 53 + fi 57 54 58 - if __name__ == '__main__': 59 - sys.exit(main()) 55 + exit 0 60 56 |} 61 57 62 58 let file_exists path = Sys.file_exists path 63 - let mkdir_p path = if not (file_exists path) then Unix.mkdir path 0o755 59 + 60 + let mkdir_p path = 61 + let rec aux path = 62 + if not (file_exists path) then begin 63 + let parent = Filename.dirname path in 64 + if parent <> path then aux parent; 65 + Unix.mkdir path 0o755 66 + end 67 + in 68 + aux path 64 69 65 70 let write_file ~dry_run path content = 66 71 if dry_run then Printf.printf "Would create %s\n" path ··· 74 79 if dry_run then Printf.printf "Would chmod +x %s\n" path 75 80 else Unix.chmod path 0o755 76 81 77 - let run_command ~dry_run cmd = 78 - if dry_run then begin 79 - Printf.printf "Would run: %s\n" cmd; 80 - 0 81 - end 82 - else Sys.command cmd 82 + let find_git_dir () = 83 + (* Find .git directory, handling worktrees *) 84 + if file_exists ".git" then 85 + if Sys.is_directory ".git" then Some ".git" 86 + else begin 87 + (* .git is a file pointing to the real git dir (worktree) *) 88 + let ic = open_in ".git" in 89 + let line = input_line ic in 90 + close_in ic; 91 + if String.length line > 8 && String.sub line 0 8 = "gitdir: " then 92 + Some (String.sub line 8 (String.length line - 8)) 93 + else None 94 + end 95 + else None 83 96 84 97 let init ~dry_run () = 85 98 (* Check if we're in an OCaml project *) 86 99 if not (file_exists "dune-project") then 87 100 Error "No dune-project found. Run this from an OCaml project root." 88 - else begin 89 - (* Create .hooks directory *) 90 - if dry_run then Printf.printf "Would create .hooks/\n" else mkdir_p ".hooks"; 101 + else 102 + match find_git_dir () with 103 + | None -> Error "No .git directory found. Run this from a git repository." 104 + | Some git_dir -> 105 + let hooks_dir = Filename.concat git_dir "hooks" in 91 106 92 - (* Write .pre-commit-config.yaml *) 93 - write_file ~dry_run ".pre-commit-config.yaml" pre_commit_config; 107 + (* Create hooks directory if needed *) 108 + if dry_run then Printf.printf "Would create %s/\n" hooks_dir 109 + else mkdir_p hooks_dir; 94 110 95 - (* Write .hooks/remove-claude-lines.py *) 96 - write_file ~dry_run ".hooks/remove-claude-lines.py" remove_claude_lines; 97 - chmod_exec ~dry_run ".hooks/remove-claude-lines.py"; 111 + (* Install pre-commit hook *) 112 + let pre_commit_path = Filename.concat hooks_dir "pre-commit" in 113 + write_file ~dry_run pre_commit_path pre_commit_hook; 114 + chmod_exec ~dry_run pre_commit_path; 98 115 99 - (* Run pre-commit install *) 100 - let cmd = 101 - "pre-commit install --hook-type pre-commit --hook-type commit-msg" 102 - in 103 - let exit_code = run_command ~dry_run cmd in 104 - if exit_code <> 0 then 105 - Error 106 - (Printf.sprintf "pre-commit install failed with exit code %d" exit_code) 107 - else Ok () 108 - end 116 + (* Install commit-msg hook *) 117 + let commit_msg_path = Filename.concat hooks_dir "commit-msg" in 118 + write_file ~dry_run commit_msg_path commit_msg_hook; 119 + chmod_exec ~dry_run commit_msg_path; 120 + 121 + Ok ()
+18 -10
lib/precommit.mli
··· 1 - (** Pre-commit hook initialization for OCaml projects. *) 1 + (** Pre-commit hook initialization for OCaml projects. 2 2 3 - (** {1 Templates} *) 3 + Installs git hooks directly without requiring the pre-commit tool. *) 4 4 5 - val pre_commit_config : string 6 - (** The content of [.pre-commit-config.yaml]. *) 5 + (** {1 Hook Templates} *) 7 6 8 - val remove_claude_lines : string 9 - (** The content of [.hooks/remove-claude-lines.py]. *) 7 + val pre_commit_hook : string 8 + (** Shell script for the pre-commit hook. Runs [dune fmt] on staged OCaml files 9 + and fails if formatting changes are needed. *) 10 + 11 + val commit_msg_hook : string 12 + (** Shell script for the commit-msg hook. Checks for emojis (rejected) and 13 + removes lines containing "claude" (case-insensitive). *) 10 14 11 15 (** {1 Operations} *) 12 16 13 17 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], then 16 - runs [pre-commit install]. 18 + (** [init ~dry_run ()] installs git hooks in the current repository. 17 19 18 - If [dry_run] is [true], prints what would be done without making changes. *) 20 + Creates: 21 + - [.git/hooks/pre-commit] - runs [dune fmt] on staged OCaml files 22 + - [.git/hooks/commit-msg] - checks for emojis and removes Claude attribution 23 + 24 + If [dry_run] is [true], prints what would be done without making changes. 25 + 26 + Returns [Error msg] if not in a git repository or OCaml project. *)
+16 -20
test/test_precommit.ml
··· 9 9 in 10 10 check 0 11 11 12 - let test_pre_commit_config_valid () = 13 - let config = Precommit.pre_commit_config in 14 - Alcotest.(check bool) 15 - "starts with repos:" true 16 - (String.sub config 0 6 = "repos:"); 17 - Alcotest.(check bool) 18 - "contains dune-format" true 19 - (contains config "dune-format"); 12 + let test_pre_commit_hook_valid () = 13 + let hook = Precommit.pre_commit_hook in 14 + Alcotest.(check bool) "is shell script" true (String.sub hook 0 2 = "#!"); 15 + Alcotest.(check bool) "runs dune fmt" true (contains hook "dune fmt"); 20 16 Alcotest.(check bool) 21 - "contains commit-msg" true 22 - (contains config "commit-msg") 17 + "checks staged files" true 18 + (contains hook "git diff --cached") 23 19 24 - let test_remove_claude_lines_valid () = 25 - let script = Precommit.remove_claude_lines in 26 - Alcotest.(check bool) "is python script" true (String.sub script 0 2 = "#!"); 27 - Alcotest.(check bool) "filters claude lines" true (contains script "'claude'"); 28 - Alcotest.(check bool) "checks emojis" true (contains script "has_emoji") 20 + let test_commit_msg_hook_valid () = 21 + let hook = Precommit.commit_msg_hook in 22 + Alcotest.(check bool) "is shell script" true (String.sub hook 0 2 = "#!"); 23 + Alcotest.(check bool) "filters claude lines" true (contains hook "claude"); 24 + Alcotest.(check bool) "checks emojis" true (contains hook "emoji") 29 25 30 26 let suite = 31 27 [ 32 - ( "templates", 28 + ( "hooks", 33 29 [ 34 - Alcotest.test_case "pre_commit_config valid" `Quick 35 - test_pre_commit_config_valid; 36 - Alcotest.test_case "remove_claude_lines valid" `Quick 37 - test_remove_claude_lines_valid; 30 + Alcotest.test_case "pre_commit_hook valid" `Quick 31 + test_pre_commit_hook_valid; 32 + Alcotest.test_case "commit_msg_hook valid" `Quick 33 + test_commit_msg_hook_valid; 38 34 ] ); 39 35 ]