(*--------------------------------------------------------------------------- Copyright (c) 2026 Anil Madhavapeddy . All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*) (** High-level query interface for changes. This module provides convenient functions for querying changes since a specific timestamp and formatting them for broadcast. *) let changes_since ~fs ~changes_dir ~since ~now = (* Get the date part of since for filtering *) let since_date = let (y, m, d), _ = Ptime.to_date_time since in Printf.sprintf "%04d-%02d-%02d" y m d in (* Get current date for range end *) let now_date = let (y, m, d), _ = Ptime.to_date_time now in Printf.sprintf "%04d-%02d-%02d" y m d in match Changes_aggregated.load_range ~fs ~changes_dir ~from_date:since_date ~to_date:now_date with | Error e -> Error e | Ok aggregated_files -> (* Filter to files generated after 'since' and collect entries *) let entries = List.concat_map (fun (agg : Changes_aggregated.t) -> if Ptime.compare agg.generated_at since > 0 then agg.entries else []) aggregated_files in Ok entries let has_new_changes ~fs ~changes_dir ~since ~now = match changes_since ~fs ~changes_dir ~since ~now with | Ok entries -> entries <> [] | Error _ -> false let format_repo_link repo url_opt = match url_opt with | Some url -> Printf.sprintf "[%s](%s)" repo url | None -> repo (* No URL available, just use repo name *) let format_for_zulip ~entries ~include_date ~date = if entries = [] then "No changes to report." else begin let buf = Buffer.create 1024 in if include_date then begin match date with | Some d -> Buffer.add_string buf (Printf.sprintf "Updates for %s:\n\n" d) | None -> Buffer.add_string buf "Recent updates:\n\n" end; (* Group by change type *) let by_type = [ (Changes_aggregated.New_library, "New Libraries", []); (Changes_aggregated.Feature, "Features", []); (Changes_aggregated.Bugfix, "Bug Fixes", []); (Changes_aggregated.Documentation, "Documentation", []); (Changes_aggregated.Refactor, "Improvements", []); (Changes_aggregated.Unknown, "Other Changes", []); ] in let grouped = List.map (fun (ct, title, _) -> let matching = List.filter (fun (e : Changes_aggregated.entry) -> e.change_type = ct) entries in (ct, title, matching)) by_type in List.iter (fun (_ct, title, entries) -> if entries <> [] then begin Buffer.add_string buf (Printf.sprintf "### %s\n\n" title); List.iter (fun (entry : Changes_aggregated.entry) -> let repo_link = format_repo_link entry.repository entry.repo_url in Buffer.add_string buf (Printf.sprintf "**%s**: %s\n" repo_link entry.summary); List.iter (fun change -> Buffer.add_string buf (Printf.sprintf "- %s\n" change)) entry.changes; if entry.contributors <> [] then Buffer.add_string buf (Printf.sprintf "*Contributors: %s*\n" (String.concat ", " entry.contributors)); Buffer.add_string buf "\n") entries end) grouped; Buffer.contents buf end let format_summary ~entries = if entries = [] then "No new changes." else let count = List.length entries in let repos = List.sort_uniq String.compare (List.map (fun (e : Changes_aggregated.entry) -> e.repository) entries) in Printf.sprintf "%d change%s across %d repositor%s: %s" count (if count = 1 then "" else "s") (List.length repos) (if List.length repos = 1 then "y" else "ies") (String.concat ", " repos) (** {1 Daily Changes (Real-time)} *) let daily_changes_since ~fs ~changes_dir ~since = Changes_daily.entries_since ~fs ~changes_dir ~since let has_new_daily_changes ~fs ~changes_dir ~since = daily_changes_since ~fs ~changes_dir ~since <> [] let format_daily_for_zulip ~entries ~include_date ~date = if entries = [] then "No changes to report." else begin let buf = Buffer.create 1024 in if include_date then begin match date with | Some d -> Buffer.add_string buf (Printf.sprintf "## Changes for %s\n\n" d) | None -> Buffer.add_string buf "## Recent Changes\n\n" end; (* Group by repository *) let repos = List.sort_uniq String.compare (List.map (fun (e : Changes_daily.entry) -> e.repository) entries) in List.iter (fun repo -> let repo_entries = List.filter (fun (e : Changes_daily.entry) -> e.repository = repo) entries in if repo_entries <> [] then begin let first_entry = List.hd repo_entries in let repo_link = format_repo_link repo first_entry.repo_url in Buffer.add_string buf (Printf.sprintf "### %s\n\n" repo_link); List.iter (fun (entry : Changes_daily.entry) -> Buffer.add_string buf (Printf.sprintf "**%s**\n" entry.summary); List.iter (fun change -> Buffer.add_string buf (Printf.sprintf "- %s\n" change)) entry.changes; if entry.contributors <> [] then Buffer.add_string buf (Printf.sprintf "*Contributors: %s*\n" (String.concat ", " entry.contributors)); Buffer.add_string buf "\n") repo_entries end) repos; Buffer.contents buf end let format_daily_summary ~entries = if entries = [] then "No new changes." else let count = List.length entries in let repos = List.sort_uniq String.compare (List.map (fun (e : Changes_daily.entry) -> e.repository) entries) in Printf.sprintf "%d change%s across %d repositor%s: %s" count (if count = 1 then "" else "s") (List.length repos) (if List.length repos = 1 then "y" else "ies") (String.concat ", " repos)