[OCaml] Mobile-friendly clone of cgit.
feat link commit identities to filtered history
Show authors and committers as recognizable links to commit history filtered by exact email identity. Combine author, committer, and conventional commit type filters across links, pagination, and removable toolbar controls. Collect enough matching first-parent commits before paginating so selective filters can reach older matches.
Changed files
lib/handlers.ml
@@ -80,28 +80,41 @@
80
80
in
81
81
Views.Repo.summary context branches commits
82
82
83
Removed:
let filter_commits filter_type commits =
84
Removed:
match filter_type with
85
Removed:
| None -> commits
86
Removed:
| Some ct ->
87
Removed:
List.filter
88
Removed:
(fun (commit : Resolvers.Commit.t) ->
83
Added:
let commit_matches ?filter_type ?author ?committer
84
Added:
(commit : Resolvers.Commit.t) =
85
Added:
let type_matches =
86
Added:
match filter_type with
87
Added:
| None -> true
88
Added:
| Some expected ->
89
Added:
let summary =
89
90
match commit.message with
90
Removed:
| None -> false
91
Removed:
| Some message ->
92
Removed:
let summary =
93
Removed:
match String.split_on_char '\n' message with
94
Removed:
| [] -> ""
95
Removed:
| s :: _ -> s
96
Removed:
in
97
Removed:
let commit_type, _ = Views.Repo.parse_conventional summary in
98
Removed:
commit_type = Some ct)
99
Removed:
commits
91
Added:
| None -> ""
92
Added:
| Some message -> (
93
Added:
match String.split_on_char '\n' message with
94
Added:
| [] -> ""
95
Added:
| summary :: _ -> summary)
96
Added:
in
97
Added:
let commit_type, _ = Views.Repo.parse_conventional summary in
98
Added:
commit_type = Some expected
99
Added:
in
100
Added:
let author_matches =
101
Added:
match author with
102
Added:
| None -> true
103
Added:
| Some email -> String.equal commit.author.email email
104
Added:
in
105
Added:
let committer_matches =
106
Added:
match committer with
107
Added:
| None -> true
108
Added:
| Some email -> String.equal commit.committer.email email
109
Added:
in
110
Added:
type_matches && author_matches && committer_matches
100
111
101
112
let page_size = 20
102
113
103
114
let commits _config request repository context =
104
115
let filter_type = Dream.query request "type" in
116
Added:
let author = Dream.query request "author" in
117
Added:
let committer = Dream.query request "committer" in
105
118
let page =
106
119
match Dream.query request "page" with
107
120
| None -> 1
@@ -111,8 +124,10 @@
111
124
let offset = (page - 1) * page_size in
112
125
(* Fetch one beyond orphan threshold to detect whether more exist *)
113
126
let fetch_count = offset + page_size + 11 in
114
Removed:
let* all_commits = Resolvers.Commit.recent repository fetch_count in
115
Removed:
let all_commits = filter_commits filter_type all_commits in
127
Added:
let predicate = commit_matches ?filter_type ?author ?committer in
128
Added:
let* all_commits =
129
Added:
Resolvers.Commit.recent_matching repository fetch_count predicate
130
Added:
in
116
131
let total = List.length all_commits in
117
132
let after_offset =
118
133
if offset >= total then []
@@ -129,8 +144,8 @@
129
144
in
130
145
let has_next = remaining > effective_size in
131
146
let has_prev = page > 1 in
132
Removed:
Views.Repo.commits ?filter_type ~page ~has_prev ~has_next context
133
Removed:
page_commits
147
Added:
Views.Repo.commits ?filter_type ?author ?committer ~page ~has_prev ~has_next
148
Added:
context page_commits
134
149
135
150
let commits_branch _config repository context branch =
136
151
let* reference = Resolvers.Reference.of_id repository branch in
lib/resolvers.ml
@@ -233,6 +233,7 @@
233
233
tree : string;
234
234
parents : string list;
235
235
author : user;
236
Added:
committer : user;
236
237
message : string option;
237
238
}
238
239
@@ -243,6 +244,7 @@
243
244
tree = Value.Commit.tree commit |> Hash.to_hex;
244
245
parents = Value.Commit.parents commit |> List.map Hash.to_hex;
245
246
author = Value.Commit.author commit;
247
Added:
committer = Value.Commit.committer commit;
246
248
message = Value.Commit.message commit;
247
249
}
248
250
@@ -261,21 +263,34 @@
261
263
let* hash = resolve_head_hash repository in
262
264
of_hash repository hash
263
265
264
Removed:
let recent_from repository hash count =
266
Added:
let recent_matching_from repository hash count predicate =
265
267
let rec walk commits hash remaining =
266
268
if remaining <= 0 then Lwt_result.return (List.rev commits)
267
269
else
268
270
let* commit = of_id repository hash in
269
Removed:
match commit.parents with
270
Removed:
| parent_hash :: _ ->
271
Removed:
walk (commit :: commits) parent_hash (remaining - 1)
272
Removed:
| [] -> Lwt_result.return (List.rev (commit :: commits))
271
Added:
let commits, remaining =
272
Added:
if predicate commit then (commit :: commits, remaining - 1)
273
Added:
else (commits, remaining)
274
Added:
in
275
Added:
if remaining <= 0 then Lwt_result.return (List.rev commits)
276
Added:
else
277
Added:
match commit.parents with
278
Added:
| parent_hash :: _ -> walk commits parent_hash remaining
279
Added:
| [] -> Lwt_result.return (List.rev commits)
273
280
in
274
281
walk [] hash count
275
282
276
Removed:
let recent repository count =
283
Added:
let recent_from repository hash count =
284
Added:
recent_matching_from repository hash count (Fun.const true)
285
Added:
286
Added:
let recent_matching repository count predicate =
277
287
let* head_hash = resolve_head_hash repository in
278
Removed:
recent_from repository (Store.Hash.to_hex head_hash) count
288
Added:
recent_matching_from repository
289
Added:
(Store.Hash.to_hex head_hash)
290
Added:
count predicate
291
Added:
292
Added:
let recent repository count =
293
Added:
recent_matching repository count (Fun.const true)
279
294
end
280
295
281
296
module Reference = struct
lib/resolvers.mli
@@ -50,12 +50,17 @@
50
50
tree : string;
51
51
parents : string list;
52
52
author : user;
53
Added:
committer : user;
53
54
message : string option;
54
55
}
55
56
56
57
val of_id : repository -> string -> (t, error) Lwt_result.t
57
58
val head : repository -> (t, error) Lwt_result.t
58
59
val recent_from : repository -> string -> int -> (t list, error) Lwt_result.t
60
Added:
61
Added:
val recent_matching :
62
Added:
repository -> int -> (t -> bool) -> (t list, error) Lwt_result.t
63
Added:
59
64
val recent : repository -> int -> (t list, error) Lwt_result.t
60
65
end
61
66
lib/static/styles.css
@@ -258,6 +258,27 @@
258
258
white-space: nowrap;
259
259
}
260
260
261
Added:
.commit-identity {
262
Added:
color: skyblue;
263
Added:
text-decoration: underline;
264
Added:
text-underline-offset: 0.15em;
265
Added:
border-radius: 0.2rem;
266
Added:
}
267
Added:
268
Added:
.commit-identity:visited {
269
Added:
color: skyblue;
270
Added:
}
271
Added:
272
Added:
.commit-identity:hover {
273
Added:
color: black;
274
Added:
background-color: white;
275
Added:
}
276
Added:
277
Added:
.commit-meta .commit-identity,
278
Added:
.commit-meta time {
279
Added:
font-family: monospace;
280
Added:
}
281
Added:
261
282
.commit-pill {
262
283
display: inline-flex;
263
284
align-items: center;
@@ -377,6 +398,17 @@
377
398
align-items: center;
378
399
gap: 0.35em;
379
400
flex-shrink: 0;
401
Added:
}
402
Added:
403
Added:
.toolbar-filter-value {
404
Added:
display: inline-flex;
405
Added:
align-items: center;
406
Added:
padding: 0.4em 0.9em;
407
Added:
border-radius: 9999px;
408
Added:
font-family: monospace;
409
Added:
font-size: 0.8em;
410
Added:
color: white;
411
Added:
background-color: #2d2d2d;
380
412
}
381
413
382
414
.toolbar-dismiss {
lib/views/repo.ml
@@ -324,7 +324,67 @@
324
324
Printf.sprintf "%04d-%02d-%02d %02d:%02d" (tm.tm_year + 1900) (tm.tm_mon + 1)
325
325
tm.tm_mday tm.tm_hour tm.tm_min
326
326
327
Removed:
let li_of_commit ?(hide_pill = false) repo (commit : Resolvers.Commit.t) =
327
Added:
let detailed_timestamp (date, timezone) =
328
Added:
let offset_seconds, suffix =
329
Added:
match timezone with
330
Added:
| None -> (0, "Z")
331
Added:
| Some (offset : Git.User.tz_offset) ->
332
Added:
let direction = match offset.sign with `Plus -> 1 | `Minus -> -1 in
333
Added:
let seconds = direction * ((offset.hours * 60) + offset.minutes) * 60 in
334
Added:
let sign = match offset.sign with `Plus -> "+" | `Minus -> "-" in
335
Added:
(seconds, Printf.sprintf "%s%02d:%02d" sign offset.hours offset.minutes)
336
Added:
in
337
Added:
let adjusted = Int64.add date (Int64.of_int offset_seconds) in
338
Added:
let tm = adjusted |> Int64.to_float |> Unix.gmtime in
339
Added:
let date =
340
Added:
Printf.sprintf "%04d-%02d-%02d" (tm.tm_year + 1900) (tm.tm_mon + 1)
341
Added:
tm.tm_mday
342
Added:
in
343
Added:
let clock = Printf.sprintf "%02d:%02d:%02d" tm.tm_hour tm.tm_min tm.tm_sec in
344
Added:
( Printf.sprintf "%sT%s%s" date clock suffix,
345
Added:
Printf.sprintf "%s %s %s" date clock suffix )
346
Added:
347
Added:
let time_node date =
348
Added:
let machine_time, display_time = detailed_timestamp date in
349
Added:
HTML.(time [ datetime "%s" machine_time ] [ txt "%s" display_time ])
350
Added:
351
Added:
let commits_url ?filter_type ?author ?committer ?(page = 1) repo =
352
Added:
let params =
353
Added:
(if page > 1 then [ ("page", string_of_int page) ] else [])
354
Added:
@ (match filter_type with Some value -> [ ("type", value) ] | None -> [])
355
Added:
@ (match author with Some value -> [ ("author", value) ] | None -> [])
356
Added:
@ match committer with Some value -> [ ("committer", value) ] | None -> []
357
Added:
in
358
Added:
let base = Printf.sprintf "/%s/commits/" repo in
359
Added:
match params with
360
Added:
| [] -> base
361
Added:
| _ -> base ^ "?" ^ Dream.to_form_urlencoded params
362
Added:
363
Added:
let identity_link ?filter_type ?author ?committer ?(show_email = false) ~role
364
Added:
repo (user : Resolvers.Commit.user) =
365
Added:
let url, role_label =
366
Added:
match role with
367
Added:
| `Author ->
368
Added:
(commits_url ?filter_type ~author:user.email ?committer repo, "author")
369
Added:
| `Committer ->
370
Added:
( commits_url ?filter_type ?author ~committer:user.email repo,
371
Added:
"committer" )
372
Added:
in
373
Added:
let link_text =
374
Added:
if show_email then Printf.sprintf "%s <%s>" user.name user.email
375
Added:
else user.name
376
Added:
in
377
Added:
HTML.(
378
Added:
a
379
Added:
[
380
Added:
href "%s" url;
381
Added:
class_ "commit-identity";
382
Added:
Aria.label "Filter commits by %s %s" role_label user.name;
383
Added:
]
384
Added:
[ txt "%s" link_text ])
385
Added:
386
Added:
let li_of_commit ?filter_type ?author ?committer ?(hide_pill = false) repo
387
Added:
(commit : Resolvers.Commit.t) =
328
388
let message = parse_commit_message commit.message in
329
389
let commit_type, commit_title = parse_conventional message.summary in
330
390
let commit_route = Routes.Commit (repo, commit.hash) in
@@ -340,7 +400,14 @@
340
400
HTML.(
341
401
span
342
402
[ class_ "commit-pill commit-pill-%s" ct ]
343
Removed:
[ a [ href "/%s/commits/?type=%s" repo ct ] [ txt "%s" ct ] ])
403
Added:
[
404
Added:
a
405
Added:
[
406
Added:
href "%s"
407
Added:
(commits_url ~filter_type:ct ?author ?committer repo);
408
Added:
]
409
Added:
[ txt "%s" ct ];
410
Added:
])
344
411
in
345
412
let title_span =
346
413
HTML.(
@@ -355,7 +422,13 @@
355
422
[ txt "%s" (Time_fmt.relative_time commit.author.date) ])
356
423
in
357
424
let author_span =
358
Removed:
HTML.(span [ class_ "commit-author" ] [ txt "%s" commit.author.name ])
425
Added:
HTML.(
426
Added:
span
427
Added:
[ class_ "commit-author" ]
428
Added:
[
429
Added:
identity_link ?filter_type ?author ?committer ~role:`Author repo
430
Added:
commit.author;
431
Added:
])
359
432
in
360
433
HTML.(
361
434
li []
@@ -438,52 +511,63 @@
438
511
ul [] (List.map (li_of_commit context.repo) commits);
439
512
]
440
513
441
Removed:
let toolbar ?(filter : (string * string) option) content =
442
Removed:
let filter_el =
443
Removed:
match filter with
444
Removed:
| None -> HTML.null []
445
Removed:
| Some (commit_type, dismiss_href) ->
446
Removed:
HTML.(
447
Removed:
span
448
Removed:
[ class_ "toolbar-filter" ]
514
Added:
let toolbar ?(filters = []) content =
515
Added:
let filter_el (filter_name, display, dismiss_href, value_class) =
516
Added:
HTML.(
517
Added:
span
518
Added:
[ class_ "toolbar-filter" ]
519
Added:
[
520
Added:
span [ class_ "%s" value_class ] [ txt "%s" display ];
521
Added:
a
449
522
[
450
Removed:
span
451
Removed:
[ class_ "commit-pill commit-pill-%s" commit_type ]
452
Removed:
[ txt "%s" commit_type ];
453
Removed:
a
454
Removed:
[
455
Removed:
href "%s" dismiss_href;
456
Removed:
class_ "toolbar-dismiss";
457
Removed:
Aria.label "Remove filter";
458
Removed:
]
459
Removed:
[ txt "\xc3\x97" ];
460
Removed:
])
523
Added:
href "%s" dismiss_href;
524
Added:
class_ "toolbar-dismiss";
525
Added:
Aria.label "Remove %s filter" filter_name;
526
Added:
]
527
Added:
[ txt "\xc3\x97" ];
528
Added:
])
461
529
in
462
Removed:
let children = content @ [ filter_el ] in
530
Added:
let children = content @ List.map filter_el filters in
463
531
HTML.(
464
532
div [ class_ "toolbar"; role `toolbar; Aria.label "View toolbar" ] children)
465
533
466
Removed:
let commits ?filter_type ~page ~has_prev ~has_next context commits =
467
Removed:
let filter =
468
Removed:
match filter_type with
469
Removed:
| None -> None
470
Removed:
| Some ct -> Some (ct, Printf.sprintf "/%s/commits/" context.repo)
471
Removed:
in
534
Added:
let commits ?filter_type ?author ?committer ~page ~has_prev ~has_next context
535
Added:
commits =
472
536
let hide_pill = Option.is_some filter_type in
473
Removed:
let tb =
474
Removed:
match filter with None -> HTML.null [] | Some _ -> toolbar ?filter []
475
Removed:
in
476
Removed:
let page_url p =
477
Removed:
let base = Printf.sprintf "/%s/commits/" context.repo in
478
Removed:
let params =
479
Removed:
(if p > 1 then [ Printf.sprintf "page=%d" p ] else [])
480
Removed:
@
481
Removed:
match filter_type with
482
Removed:
| Some ct -> [ Printf.sprintf "type=%s" ct ]
537
Added:
let filters =
538
Added:
(match filter_type with
483
539
| None -> []
484
Removed:
in
485
Removed:
match params with [] -> base | _ -> base ^ "?" ^ String.concat "&" params
540
Added:
| Some commit_type ->
541
Added:
[
542
Added:
( "commit type",
543
Added:
commit_type,
544
Added:
commits_url ?author ?committer context.repo,
545
Added:
"commit-pill commit-pill-" ^ commit_type );
546
Added:
])
547
Added:
@ (match author with
548
Added:
| None -> []
549
Added:
| Some email ->
550
Added:
[
551
Added:
( "author",
552
Added:
"Author: " ^ email,
553
Added:
commits_url ?filter_type ?committer context.repo,
554
Added:
"toolbar-filter-value" );
555
Added:
])
556
Added:
@
557
Added:
match committer with
558
Added:
| None -> []
559
Added:
| Some email ->
560
Added:
[
561
Added:
( "committer",
562
Added:
"Committer: " ^ email,
563
Added:
commits_url ?filter_type ?author context.repo,
564
Added:
"toolbar-filter-value" );
565
Added:
]
486
566
in
567
Added:
let tb = match filters with [] -> HTML.null [] | _ -> toolbar ~filters [] in
568
Added:
let page_url page =
569
Added:
commits_url ?filter_type ?author ?committer ~page context.repo
570
Added:
in
487
571
let show_pagination = has_prev || has_next in
488
572
let pagination =
489
573
if not show_pagination then HTML.null []
@@ -530,7 +614,11 @@
530
614
[
531
615
tb;
532
616
pagination;
533
Removed:
ul [] (List.map (li_of_commit ~hide_pill context.repo) commits);
617
Added:
ul []
618
Added:
(List.map
619
Added:
(li_of_commit ~hide_pill ?filter_type ?author ?committer
620
Added:
context.repo)
621
Added:
commits);
534
622
pagination;
535
623
]
536
624
@@ -716,7 +804,9 @@
716
804
span
717
805
[ class_ "commit-pill commit-pill-%s" ct ]
718
806
[
719
Removed:
a [ href "/%s/commits/?type=%s" context.repo ct ] [ txt "%s" ct ];
807
Added:
a
808
Added:
[ href "%s" (commits_url ~filter_type:ct context.repo) ]
809
Added:
[ txt "%s" ct ];
720
810
])
721
811
in
722
812
let content =
@@ -731,7 +821,21 @@
731
821
dt [] [ txt "Commit" ];
732
822
dd [] [ txt "%s" commit.hash ];
733
823
dt [] [ txt "Author" ];
734
Removed:
dd [] [ txt "%s <%s>" commit.author.name commit.author.email ];
824
Added:
dd []
825
Added:
[
826
Added:
identity_link ~show_email:true ~role:`Author context.repo
827
Added:
commit.author;
828
Added:
];
829
Added:
dt [] [ txt "Author date" ];
830
Added:
dd [] [ time_node commit.author.date ];
831
Added:
dt [] [ txt "Committer" ];
832
Added:
dd []
833
Added:
[
834
Added:
identity_link ~show_email:true ~role:`Committer context.repo
835
Added:
commit.committer;
836
Added:
];
837
Added:
dt [] [ txt "Committer date" ];
838
Added:
dd [] [ time_node commit.committer.date ];
735
839
];
736
840
]
737
841
@ diff_content)