forked from
anil.recoil.org/monopam
Monorepo management for opam overlays
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 ~now =
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_date =
19 let (y, m, d), _ = Ptime.to_date_time now in
20 Printf.sprintf "%04d-%02d-%02d" y m d
21 in
22 match
23 Changes_aggregated.load_range ~fs ~changes_dir ~from_date:since_date
24 ~to_date:now_date
25 with
26 | Error e -> Error e
27 | Ok aggregated_files ->
28 (* Filter to files generated after 'since' and collect entries *)
29 let entries =
30 List.concat_map
31 (fun (agg : Changes_aggregated.t) ->
32 if Ptime.compare agg.generated_at since > 0 then agg.entries else [])
33 aggregated_files
34 in
35 Ok entries
36
37let has_new_changes ~fs ~changes_dir ~since ~now =
38 match changes_since ~fs ~changes_dir ~since ~now with
39 | Ok entries -> entries <> []
40 | Error _ -> false
41
42let format_repo_link repo url_opt =
43 match url_opt with
44 | Some url -> Printf.sprintf "[%s](%s)" repo url
45 | None -> repo (* No URL available, just use repo name *)
46
47let format_for_zulip ~entries ~include_date ~date =
48 if entries = [] then "No changes to report."
49 else begin
50 let buf = Buffer.create 1024 in
51 if include_date then begin
52 match date with
53 | Some d -> Buffer.add_string buf (Printf.sprintf "Updates for %s:\n\n" d)
54 | None -> Buffer.add_string buf "Recent updates:\n\n"
55 end;
56 (* Group by change type *)
57 let by_type =
58 [
59 (Changes_aggregated.New_library, "New Libraries", []);
60 (Changes_aggregated.Feature, "Features", []);
61 (Changes_aggregated.Bugfix, "Bug Fixes", []);
62 (Changes_aggregated.Documentation, "Documentation", []);
63 (Changes_aggregated.Refactor, "Improvements", []);
64 (Changes_aggregated.Unknown, "Other Changes", []);
65 ]
66 in
67 let grouped =
68 List.map
69 (fun (ct, title, _) ->
70 let matching =
71 List.filter
72 (fun (e : Changes_aggregated.entry) -> e.change_type = ct)
73 entries
74 in
75 (ct, title, matching))
76 by_type
77 in
78 List.iter
79 (fun (_ct, title, entries) ->
80 if entries <> [] then begin
81 Buffer.add_string buf (Printf.sprintf "### %s\n\n" title);
82 List.iter
83 (fun (entry : Changes_aggregated.entry) ->
84 let repo_link =
85 format_repo_link entry.repository entry.repo_url
86 in
87 Buffer.add_string buf
88 (Printf.sprintf "**%s**: %s\n" repo_link entry.summary);
89 List.iter
90 (fun change ->
91 Buffer.add_string buf (Printf.sprintf "- %s\n" change))
92 entry.changes;
93 if entry.contributors <> [] then
94 Buffer.add_string buf
95 (Printf.sprintf "*Contributors: %s*\n"
96 (String.concat ", " entry.contributors));
97 Buffer.add_string buf "\n")
98 entries
99 end)
100 grouped;
101 Buffer.contents buf
102 end
103
104let format_summary ~entries =
105 if entries = [] then "No new changes."
106 else
107 let count = List.length entries in
108 let repos =
109 List.sort_uniq String.compare
110 (List.map (fun (e : Changes_aggregated.entry) -> e.repository) entries)
111 in
112 Printf.sprintf "%d change%s across %d repositor%s: %s" count
113 (if count = 1 then "" else "s")
114 (List.length repos)
115 (if List.length repos = 1 then "y" else "ies")
116 (String.concat ", " repos)
117
118(** {1 Daily Changes (Real-time)} *)
119
120let daily_changes_since ~fs ~changes_dir ~since =
121 Changes_daily.entries_since ~fs ~changes_dir ~since
122
123let has_new_daily_changes ~fs ~changes_dir ~since =
124 daily_changes_since ~fs ~changes_dir ~since <> []
125
126let format_daily_for_zulip ~entries ~include_date ~date =
127 if entries = [] then "No changes to report."
128 else begin
129 let buf = Buffer.create 1024 in
130 if include_date then begin
131 match date with
132 | Some d ->
133 Buffer.add_string buf (Printf.sprintf "## Changes for %s\n\n" d)
134 | None -> Buffer.add_string buf "## Recent Changes\n\n"
135 end;
136 (* Group by repository *)
137 let repos =
138 List.sort_uniq String.compare
139 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries)
140 in
141 List.iter
142 (fun repo ->
143 let repo_entries =
144 List.filter
145 (fun (e : Changes_daily.entry) -> e.repository = repo)
146 entries
147 in
148 if repo_entries <> [] then begin
149 let first_entry = List.hd repo_entries in
150 let repo_link = format_repo_link repo first_entry.repo_url in
151 Buffer.add_string buf (Printf.sprintf "### %s\n\n" repo_link);
152 List.iter
153 (fun (entry : Changes_daily.entry) ->
154 Buffer.add_string buf (Printf.sprintf "**%s**\n" entry.summary);
155 List.iter
156 (fun change ->
157 Buffer.add_string buf (Printf.sprintf "- %s\n" change))
158 entry.changes;
159 if entry.contributors <> [] then
160 Buffer.add_string buf
161 (Printf.sprintf "*Contributors: %s*\n"
162 (String.concat ", " entry.contributors));
163 Buffer.add_string buf "\n")
164 repo_entries
165 end)
166 repos;
167 Buffer.contents buf
168 end
169
170let format_daily_summary ~entries =
171 if entries = [] then "No new changes."
172 else
173 let count = List.length entries in
174 let repos =
175 List.sort_uniq String.compare
176 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries)
177 in
178 Printf.sprintf "%d change%s across %d repositor%s: %s" count
179 (if count = 1 then "" else "s")
180 (List.length repos)
181 (if List.length repos = 1 then "y" else "ies")
182 (String.concat ", " repos)