(*--------------------------------------------------------------------------- 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 Fmt.str "%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 Fmt.str "%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 -> Fmt.str "[%s](%s)" repo url | None -> repo (* No URL available, just use repo name *) let format_entry_zulip buf (entry : Changes_aggregated.entry) = let repo_link = format_repo_link entry.repository entry.repo_url in Buffer.add_string buf (Fmt.str "**%s**: %s\n" repo_link entry.summary); List.iter (fun change -> Buffer.add_string buf (Fmt.str "- %s\n" change)) entry.changes; if entry.contributors <> [] then Buffer.add_string buf (Fmt.str "*Contributors: %s*\n" (String.concat ", " entry.contributors)); Buffer.add_string buf "\n" let format_type_section buf title entries = if entries <> [] then begin Buffer.add_string buf (Fmt.str "### %s\n\n" title); List.iter (format_entry_zulip buf) entries end 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 Buffer.add_string buf (match date with | Some d -> Fmt.str "Updates for %s:\n\n" d | None -> "Recent updates:\n\n"); (* 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 List.iter (fun (ct, title) -> let matching = List.filter (fun (e : Changes_aggregated.entry) -> e.change_type = ct) entries in format_type_section buf title matching) by_type; 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 Fmt.str "%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_entry buf (entry : Changes_daily.entry) = Buffer.add_string buf (Fmt.str "**%s**\n" entry.summary); List.iter (fun change -> Buffer.add_string buf (Fmt.str "- %s\n" change)) entry.changes; if entry.contributors <> [] then Buffer.add_string buf (Fmt.str "*Contributors: %s*\n" (String.concat ", " entry.contributors)); Buffer.add_string buf "\n" let format_repo_section buf repo repo_entries = if repo_entries <> [] then begin let first_entry = List.hd repo_entries in let repo_link = format_repo_link repo first_entry.Changes_daily.repo_url in Buffer.add_string buf (Fmt.str "### %s\n\n" repo_link); List.iter (format_daily_entry buf) repo_entries end 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 Buffer.add_string buf (match date with | Some d -> Fmt.str "## Changes for %s\n\n" d | None -> "## Recent Changes\n\n"); (* 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 format_repo_section buf repo repo_entries) 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 Fmt.str "%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)