refactor extract commit message parsing into its own module

Move parse_commit_message and parse_conventional out of the view layer (lib/views/repo.ml) into lib/commit_message.ml. Handlers now call Commit_message directly instead of reaching into Views.Repo for domain logic.

Commit
85c5351a7d350e4a162865370a98b8714934abba
Author
Claude Sonnet 4 <claude@anthropic.invalid>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/commit_message.ml
index 00000000..5a05f8b0 000000..100644
@@ -0,0 +1,53 @@
1 Added: (** Parsing commit messages and Conventional Commits metadata.
2 Added:
3 Added: Extracted from the view layer so that both handlers (for filtering) and
4 Added: views (for display) can use it without a layer violation. *)
5 Added:
6 Added: type t = { summary : string; body : string }
7 Added:
8 Added: let parse = function
9 Added: | None -> { summary = ""; body = "" }
10 Added: | Some message -> (
11 Added: match String.split_on_char '\n' message with
12 Added: | [] -> { summary = ""; body = "" }
13 Added: | summary :: rest ->
14 Added: { summary; body = String.concat "\n" rest |> String.trim })
15 Added:
16 Added: let conventional_commit_types =
17 Added: [
18 Added: "feat";
19 Added: "fix";
20 Added: "docs";
21 Added: "style";
22 Added: "refactor";
23 Added: "perf";
24 Added: "test";
25 Added: "build";
26 Added: "ci";
27 Added: "chore";
28 Added: "revert";
29 Added: "remove";
30 Added: ]
31 Added:
32 Added: (** Split a Conventional Commits subject into its type and the remaining title.
33 Added: An unrecognised prefix is left in the title untouched, so non-conforming
34 Added: histories still read correctly. *)
35 Added: let parse_conventional summary =
36 Added: match String.index_opt summary ':' with
37 Added: | None -> (None, summary)
38 Added: | Some colon_pos ->
39 Added: let prefix = String.sub summary 0 colon_pos in
40 Added: let type_name =
41 Added: match String.index_opt prefix '(' with
42 Added: | Some paren_pos -> String.sub prefix 0 paren_pos
43 Added: | None -> prefix
44 Added: in
45 Added: let type_lower = String.lowercase_ascii type_name in
46 Added: if List.mem type_lower conventional_commit_types then
47 Added: let rest =
48 Added: String.sub summary (colon_pos + 1)
49 Added: (String.length summary - colon_pos - 1)
50 Added: |> String.trim
51 Added: in
52 Added: (Some type_lower, rest)
53 Added: else (None, summary)
lib/commit_message.mli
index 00000000..c0756c2f 000000..100644
@@ -0,0 +1,10 @@
1 Added: (** Parsing commit messages and Conventional Commits metadata. *)
2 Added:
3 Added: type t = { summary : string; body : string }
4 Added:
5 Added: val parse : string option -> t
6 Added: (** Extract the summary line and body from a raw commit message. *)
7 Added:
8 Added: val parse_conventional : string -> string option * string
9 Added: (** Split a Conventional Commits subject into [(Some type, title)] or
10 Added: [(None, original)] when the prefix is unrecognised. *)
lib/handlers.ml
index 64eaa6c2..9e83140a 100644..100644
@@ -164,7 +164,7 @@
164 164 | [] -> ""
165 165 | summary :: _ -> summary)
166 166 in
167 Removed: let commit_type, _ = Views.Repo.parse_conventional summary in
167 Added: let commit_type, _ = Commit_message.parse_conventional summary in
168 168 commit_type = Some expected
169 169 in
170 170 let author_matches =
lib/views.ml
index 62d2e2a8..ee0f0fcc 100644..100644
@@ -10,8 +10,8 @@
10 10 }
11 11
12 12 let context = Repo.context
13 Removed: let parse_commit_message = Repo.parse_commit_message
14 Removed: let parse_conventional = Repo.parse_conventional
13 Added: let parse_commit_message = Commit_message.parse
14 Added: let parse_conventional = Commit_message.parse_conventional
15 15 let summary = Repo.summary
16 16 let commits = Repo.commits
17 17 let files = Repo.files
lib/views/repo.ml
index 44b6d0f7..3b9cc3d7 100644..100644
@@ -8,58 +8,14 @@
8 8 type context = { repo : string; description : string; site : Layout.site }
9 9 (** What every repository page needs to know about its subject. *)
10 10
11 Removed: type commit_message = { summary : string; body : string }
11 Added: type commit_message = Commit_message.t = { summary : string; body : string }
12 12
13 13 let context ~site ~repo ~description = { repo; description; site }
14 14
15 15 (** {1 Commit messages} *)
16 16
17 Removed: let parse_commit_message = function
18 Removed: | None -> { summary = ""; body = "" }
19 Removed: | Some message -> (
20 Removed: match String.split_on_char '\n' message with
21 Removed: | [] -> { summary = ""; body = "" }
22 Removed: | summary :: rest ->
23 Removed: { summary; body = String.concat "\n" rest |> String.trim })
24 Removed:
25 Removed: let conventional_commit_types =
26 Removed: [
27 Removed: "feat";
28 Removed: "fix";
29 Removed: "docs";
30 Removed: "style";
31 Removed: "refactor";
32 Removed: "perf";
33 Removed: "test";
34 Removed: "build";
35 Removed: "ci";
36 Removed: "chore";
37 Removed: "revert";
38 Removed: "remove";
39 Removed: ]
40 Removed:
41 Removed: (** Split a Conventional Commits subject into its type and the remaining title.
42 Removed: An unrecognised prefix is left in the title untouched, so non-conforming
43 Removed: histories still read correctly. *)
44 Removed: let parse_conventional summary =
45 Removed: match String.index_opt summary ':' with
46 Removed: | None -> (None, summary)
47 Removed: | Some colon_pos ->
48 Removed: let prefix = String.sub summary 0 colon_pos in
49 Removed: let type_name =
50 Removed: match String.index_opt prefix '(' with
51 Removed: | Some paren_pos -> String.sub prefix 0 paren_pos
52 Removed: | None -> prefix
53 Removed: in
54 Removed: let type_lower = String.lowercase_ascii type_name in
55 Removed: if List.mem type_lower conventional_commit_types then
56 Removed: let rest =
57 Removed: String.sub summary (colon_pos + 1)
58 Removed: (String.length summary - colon_pos - 1)
59 Removed: |> String.trim
60 Removed: in
61 Removed: (Some type_lower, rest)
62 Removed: else (None, summary)
17 Added: let parse_commit_message = Commit_message.parse
18 Added: let parse_conventional = Commit_message.parse_conventional
63 19
64 20 (** {1 Links into the commit list} *)
65 21