View raw

1 (** Parsing commit messages and Conventional Commits metadata. 2 3 Extracted from the view layer so that both handlers (for filtering) and 4 views (for display) can use it without a layer violation. *) 5 6 type t = { summary : string; body : string } 7 8 let parse = function 9 | None -> { summary = ""; body = "" } 10 | Some message -> ( 11 match String.split_on_char '\n' message with 12 | [] -> { summary = ""; body = "" } 13 | summary :: rest -> 14 { summary; body = String.concat "\n" rest |> String.trim }) 15 16 let conventional_commit_types = 17 [ 18 "feat"; 19 "fix"; 20 "docs"; 21 "style"; 22 "refactor"; 23 "perf"; 24 "test"; 25 "build"; 26 "ci"; 27 "chore"; 28 "revert"; 29 "remove"; 30 ] 31 32 (** Split a Conventional Commits subject into its type and the remaining title. 33 An unrecognised prefix is left in the title untouched, so non-conforming 34 histories still read correctly. *) 35 let parse_conventional summary = 36 match String.index_opt summary ':' with 37 | None -> (None, summary) 38 | Some colon_pos -> 39 let prefix = String.sub summary 0 colon_pos in 40 let type_name = 41 match String.index_opt prefix '(' with 42 | Some paren_pos -> String.sub prefix 0 paren_pos 43 | None -> prefix 44 in 45 let type_lower = String.lowercase_ascii type_name in 46 if List.mem type_lower conventional_commit_types then 47 let rest = 48 String.sub summary (colon_pos + 1) 49 (String.length summary - colon_pos - 1) 50 |> String.trim 51 in 52 (Some type_lower, rest) 53 else (None, summary) 54