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 Fmt.str "%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 Fmt.str "%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 -> Fmt.str "[%s](%s)" repo url
45 | None -> repo (* No URL available, just use repo name *)
46
47let format_entry_zulip buf (entry : Changes_aggregated.entry) =
48 let repo_link = format_repo_link entry.repository entry.repo_url in
49 Buffer.add_string buf (Fmt.str "**%s**: %s\n" repo_link entry.summary);
50 List.iter
51 (fun change -> Buffer.add_string buf (Fmt.str "- %s\n" change))
52 entry.changes;
53 if entry.contributors <> [] then
54 Buffer.add_string buf
55 (Fmt.str "*Contributors: %s*\n" (String.concat ", " entry.contributors));
56 Buffer.add_string buf "\n"
57
58let format_type_section buf title entries =
59 if entries <> [] then begin
60 Buffer.add_string buf (Fmt.str "### %s\n\n" title);
61 List.iter (format_entry_zulip buf) entries
62 end
63
64let format_for_zulip ~entries ~include_date ~date =
65 if entries = [] then "No changes to report."
66 else begin
67 let buf = Buffer.create 1024 in
68 if include_date then
69 Buffer.add_string buf
70 (match date with
71 | Some d -> Fmt.str "Updates for %s:\n\n" d
72 | None -> "Recent updates:\n\n");
73 (* Group by change type *)
74 let by_type =
75 [
76 (Changes_aggregated.New_library, "New Libraries");
77 (Changes_aggregated.Feature, "Features");
78 (Changes_aggregated.Bugfix, "Bug Fixes");
79 (Changes_aggregated.Documentation, "Documentation");
80 (Changes_aggregated.Refactor, "Improvements");
81 (Changes_aggregated.Unknown, "Other Changes");
82 ]
83 in
84 List.iter
85 (fun (ct, title) ->
86 let matching =
87 List.filter
88 (fun (e : Changes_aggregated.entry) -> e.change_type = ct)
89 entries
90 in
91 format_type_section buf title matching)
92 by_type;
93 Buffer.contents buf
94 end
95
96let format_summary ~entries =
97 if entries = [] then "No new changes."
98 else
99 let count = List.length entries in
100 let repos =
101 List.sort_uniq String.compare
102 (List.map (fun (e : Changes_aggregated.entry) -> e.repository) entries)
103 in
104 Fmt.str "%d change%s across %d repositor%s: %s" count
105 (if count = 1 then "" else "s")
106 (List.length repos)
107 (if List.length repos = 1 then "y" else "ies")
108 (String.concat ", " repos)
109
110(** {1 Daily Changes (Real-time)} *)
111
112let daily_changes_since ~fs ~changes_dir ~since =
113 Changes_daily.entries_since ~fs ~changes_dir ~since
114
115let has_new_daily_changes ~fs ~changes_dir ~since =
116 daily_changes_since ~fs ~changes_dir ~since <> []
117
118let format_daily_entry buf (entry : Changes_daily.entry) =
119 Buffer.add_string buf (Fmt.str "**%s**\n" entry.summary);
120 List.iter
121 (fun change -> Buffer.add_string buf (Fmt.str "- %s\n" change))
122 entry.changes;
123 if entry.contributors <> [] then
124 Buffer.add_string buf
125 (Fmt.str "*Contributors: %s*\n" (String.concat ", " entry.contributors));
126 Buffer.add_string buf "\n"
127
128let format_repo_section buf repo repo_entries =
129 if repo_entries <> [] then begin
130 let first_entry = List.hd repo_entries in
131 let repo_link = format_repo_link repo first_entry.Changes_daily.repo_url in
132 Buffer.add_string buf (Fmt.str "### %s\n\n" repo_link);
133 List.iter (format_daily_entry buf) repo_entries
134 end
135
136let format_daily_for_zulip ~entries ~include_date ~date =
137 if entries = [] then "No changes to report."
138 else begin
139 let buf = Buffer.create 1024 in
140 if include_date then
141 Buffer.add_string buf
142 (match date with
143 | Some d -> Fmt.str "## Changes for %s\n\n" d
144 | None -> "## Recent Changes\n\n");
145 (* Group by repository *)
146 let repos =
147 List.sort_uniq String.compare
148 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries)
149 in
150 List.iter
151 (fun repo ->
152 let repo_entries =
153 List.filter
154 (fun (e : Changes_daily.entry) -> e.repository = repo)
155 entries
156 in
157 format_repo_section buf repo repo_entries)
158 repos;
159 Buffer.contents buf
160 end
161
162let format_daily_summary ~entries =
163 if entries = [] then "No new changes."
164 else
165 let count = List.length entries in
166 let repos =
167 List.sort_uniq String.compare
168 (List.map (fun (e : Changes_daily.entry) -> e.repository) entries)
169 in
170 Fmt.str "%d change%s across %d repositor%s: %s" count
171 (if count = 1 then "" else "s")
172 (List.length repos)
173 (if List.length repos = 1 then "y" else "ies")
174 (String.concat ", " repos)