feat rank repositories by recency on root page

Sort repositories by most recent HEAD commit date. Display a right-aligned relative time span (identical style to the commits page) next to each repository name. Repos without a resolvable HEAD appear last.

Commit
cf6caab6c2944529497e0ce8819f37da29fb9fb1
Author
Claude Sonnet 4 <agent@anthropic.com>
Author date
Committer
Claude Sonnet 4 <agent@anthropic.com>
Committer date
Changed files
lib/handlers.ml
index 394f11c8..d3238051 100644..100644
@@ -18,8 +18,29 @@
18 18 let root config _request =
19 19 match Resolvers.repositories config with
20 20 | Ok repositories ->
21 Removed: Views.root ~user:config.Config.user ~root_title:(root_title config)
22 Removed: repositories
21 Added: let open Lwt.Syntax in
22 Added: let* repos_with_dates =
23 Added: Lwt_list.map_p
24 Added: (fun name ->
25 Added: Lwt.bind (Resolvers.open_repository config name) @@ function
26 Added: | Error _ -> Lwt.return (name, None)
27 Added: | Ok repository ->
28 Added: let* date = Resolvers.head_commit_date repository in
29 Added: let* () = Resolvers.close_repository repository in
30 Added: Lwt.return (name, date))
31 Added: repositories
32 Added: in
33 Added: let sorted =
34 Added: List.sort
35 Added: (fun (_, a) (_, b) ->
36 Added: match (a, b) with
37 Added: | None, None -> 0
38 Added: | None, Some _ -> 1
39 Added: | Some _, None -> -1
40 Added: | Some (ta, _), Some (tb, _) -> Int64.compare tb ta)
41 Added: repos_with_dates
42 Added: in
43 Added: Views.root ~user:config.Config.user ~root_title:(root_title config) sorted
23 44 | Error error -> error_response error
24 45
25 46 module Repo = struct
lib/resolvers.ml
index b03e1f77..6bfbc9b7 100644..100644
@@ -210,6 +210,21 @@
210 210 | Error error -> Lwt_result.fail (Store_error error)
211 211 | Ok value -> Lwt_result.return value
212 212
213 Added: let head_commit_date repository =
214 Added: let open Lwt.Syntax in
215 Added: let* result =
216 Added: Lwt_result.bind (resolve_head_hash repository) @@ fun hash ->
217 Added: Lwt.bind (Store.read repository.store hash) @@ function
218 Added: | Error _ -> Lwt_result.return None
219 Added: | Ok value -> (
220 Added: match value with
221 Added: | Git.Value.Commit commit ->
222 Added: let author = Store.Value.Commit.author commit in
223 Added: Lwt_result.return (Some author.Git.User.date)
224 Added: | _ -> Lwt_result.return None)
225 Added: in
226 Added: Lwt.return (Result.value result ~default:None)
227 Added:
213 228 module Commit = struct
214 229 type user = Git.User.t
215 230
lib/resolvers.mli
index 601b7daf..49900d30 100644..100644
@@ -30,6 +30,9 @@
30 30 val repository_description : repository -> string
31 31 val close_repository : repository -> unit Lwt.t
32 32
33 Added: val head_commit_date :
34 Added: repository -> (int64 * Git.User.tz_offset option) option Lwt.t
35 Added:
33 36 (** {1 Repository metadata} *)
34 37
35 38 val default_repo_description : string
lib/views/root.ml
index fa939698..eac9a70f 100644..100644
@@ -2,9 +2,40 @@
2 2
3 3 open Dream_html
4 4
5 Added: let relative_time (date, _) =
6 Added: let seconds = Unix.time () -. Int64.to_float date |> int_of_float in
7 Added: let minutes = seconds / 60 in
8 Added: let hours = minutes / 60 in
9 Added: let days = hours / 24 in
10 Added: let months = days / 30 in
11 Added: let years = months / 12 in
12 Added: let quantity value singular =
13 Added: Printf.sprintf "%d %s%s ago" value singular (if value = 1 then "" else "s")
14 Added: in
15 Added: match seconds with
16 Added: | s when s < 60 -> "just now"
17 Added: | _ when minutes < 60 -> quantity minutes "minute"
18 Added: | _ when hours < 24 -> quantity hours "hour"
19 Added: | _ when days < 30 -> quantity days "day"
20 Added: | _ when months < 12 -> quantity months "month"
21 Added: | _ -> quantity years "year"
22 Added:
5 23 let render ~user ~root_title repositories =
6 Removed: let li_of_repo repo =
7 Removed: HTML.li [] [ Routes.link_to (Routes.Repo repo) (txt "%s" repo) ]
24 Added: let li_of_repo (repo_name, date) =
25 Added: let ago_span =
26 Added: match date with
27 Added: | None -> HTML.null []
28 Added: | Some d ->
29 Added: HTML.(span [ class_ "commit-ago" ] [ txt "%s" (relative_time d) ])
30 Added: in
31 Added: HTML.(
32 Added: li []
33 Added: [
34 Added: span
35 Added: [ class_ "commit-left" ]
36 Added: [ Routes.link_to (Routes.Repo repo_name) (txt "%s" repo_name) ];
37 Added: ago_span;
38 Added: ])
8 39 in
9 40 let all_repositories =
10 41 HTML.(