My aggregated monorepo of OCaml code, automaintained
1(*---------------------------------------------------------------------------
2 Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** High-level query interface for changes.
7
8 This module provides convenient functions for querying changes since a
9 specific timestamp and formatting them for broadcast. *)
10
11let changes_since ~fs ~changes_dir ~since =
12 (* Get the date part of since for filtering *)
13 let since_date =
14 let (y, m, d), _ = Ptime.to_date_time since in
15 Printf.sprintf "%04d-%02d-%02d" y m d
16 in
17 (* Get current date for range end *)
18 let now = Ptime_clock.now () in
19 let now_date =
20 let (y, m, d), _ = Ptime.to_date_time now in
21 Printf.sprintf "%04d-%02d-%02d" y m d
22 in
23 match Changes_aggregated.load_range ~fs ~changes_dir ~from_date:since_date ~to_date:now_date with
24 | Error e -> Error e
25 | Ok aggregated_files ->
26 (* Filter to files generated after 'since' and collect entries *)
27 let entries = List.concat_map (fun (agg : Changes_aggregated.t) ->
28 if Ptime.compare agg.generated_at since > 0 then
29 agg.entries
30 else
31 []) aggregated_files
32 in
33 Ok entries
34
35let has_new_changes ~fs ~changes_dir ~since =
36 match changes_since ~fs ~changes_dir ~since with
37 | Ok entries -> entries <> []
38 | Error _ -> false
39
40let format_repo_link repo url_opt =
41 match url_opt with
42 | Some url -> Printf.sprintf "[%s](%s)" repo url
43 | None -> Printf.sprintf "[%s](https://tangled.org/@anil.recoil.org/%s.git)" repo repo
44
45let format_for_zulip ~entries ~include_date ~date =
46 if entries = [] then
47 "No changes to report."
48 else begin
49 let buf = Buffer.create 1024 in
50 if include_date then begin
51 match date with
52 | Some d -> Buffer.add_string buf (Printf.sprintf "Updates for %s:\n\n" d)
53 | None -> Buffer.add_string buf "Recent updates:\n\n"
54 end;
55 (* Group by change type *)
56 let by_type = [
57 (Changes_aggregated.New_library, "New Libraries", []);
58 (Changes_aggregated.Feature, "Features", []);
59 (Changes_aggregated.Bugfix, "Bug Fixes", []);
60 (Changes_aggregated.Documentation, "Documentation", []);
61 (Changes_aggregated.Refactor, "Improvements", []);
62 (Changes_aggregated.Unknown, "Other Changes", []);
63 ] in
64 let grouped = List.map (fun (ct, title, _) ->
65 let matching = List.filter (fun (e : Changes_aggregated.entry) -> e.change_type = ct) entries in
66 (ct, title, matching)) by_type
67 in
68 List.iter (fun (_ct, title, entries) ->
69 if entries <> [] then begin
70 Buffer.add_string buf (Printf.sprintf "### %s\n\n" title);
71 List.iter (fun (entry : Changes_aggregated.entry) ->
72 let repo_link = format_repo_link entry.repository entry.repo_url in
73 Buffer.add_string buf (Printf.sprintf "**%s**: %s\n" repo_link entry.summary);
74 List.iter (fun change ->
75 Buffer.add_string buf (Printf.sprintf "- %s\n" change)) entry.changes;
76 if entry.contributors <> [] then
77 Buffer.add_string buf (Printf.sprintf "*Contributors: %s*\n"
78 (String.concat ", " entry.contributors));
79 Buffer.add_string buf "\n") entries
80 end) grouped;
81 Buffer.contents buf
82 end
83
84let format_summary ~entries =
85 if entries = [] then
86 "No new changes."
87 else
88 let count = List.length entries in
89 let repos = List.sort_uniq String.compare
90 (List.map (fun (e : Changes_aggregated.entry) -> e.repository) entries) in
91 Printf.sprintf "%d change%s across %d repositor%s: %s"
92 count (if count = 1 then "" else "s")
93 (List.length repos) (if List.length repos = 1 then "y" else "ies")
94 (String.concat ", " repos)
95
96(** {1 Daily Changes (Real-time)} *)
97
98let daily_changes_since ~fs ~changes_dir ~since =
99 Changes_daily.entries_since ~fs ~changes_dir ~since
100
101let has_new_daily_changes ~fs ~changes_dir ~since =
102 daily_changes_since ~fs ~changes_dir ~since <> []
103
104let format_daily_for_zulip ~entries ~include_date ~date =
105 if entries = [] then
106 "No changes to report."
107 else begin
108 let buf = Buffer.create 1024 in
109 if include_date then begin
110 match date with
111 | Some d -> Buffer.add_string buf (Printf.sprintf "## Changes for %s\n\n" d)
112 | None -> Buffer.add_string buf "## Recent Changes\n\n"
113 end;
114 (* Group by repository *)
115 let repos = List.sort_uniq String.compare
116 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries) in
117 List.iter (fun repo ->
118 let repo_entries = List.filter (fun (e : Changes_daily.entry) -> e.repository = repo) entries in
119 if repo_entries <> [] then begin
120 let first_entry = List.hd repo_entries in
121 let repo_link = format_repo_link repo first_entry.repo_url in
122 Buffer.add_string buf (Printf.sprintf "### %s\n\n" repo_link);
123 List.iter (fun (entry : Changes_daily.entry) ->
124 Buffer.add_string buf (Printf.sprintf "**%s**\n" entry.summary);
125 List.iter (fun change ->
126 Buffer.add_string buf (Printf.sprintf "- %s\n" change)) entry.changes;
127 if entry.contributors <> [] then
128 Buffer.add_string buf (Printf.sprintf "*Contributors: %s*\n"
129 (String.concat ", " entry.contributors));
130 Buffer.add_string buf "\n") repo_entries
131 end) repos;
132 Buffer.contents buf
133 end
134
135let format_daily_summary ~entries =
136 if entries = [] then
137 "No new changes."
138 else
139 let count = List.length entries in
140 let repos = List.sort_uniq String.compare
141 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries) in
142 Printf.sprintf "%d change%s across %d repositor%s: %s"
143 count (if count = 1 then "" else "s")
144 (List.length repos) (if List.length repos = 1 then "y" else "ies")
145 (String.concat ", " repos)