1
1
(* -*- mode: tuareg; -*- *)
2
2
3
-
Removed:
open Dream_html
4
-
Removed:
5
-
Removed:
type context = { repo : string; description : string; site : Layout.site }
6
-
Removed:
type commit_message = { summary : string; body : string }
7
-
Removed:
8
-
Removed:
let context ~site ~repo ~description = { repo; description; site }
9
-
Removed:
10
-
Removed:
let language_of_filename name =
11
-
Removed:
match Filename.extension name |> String.lowercase_ascii with
12
-
Removed:
| ".ml" | ".mli" -> Some "ocaml"
13
-
Removed:
| ".c" | ".h" -> Some "c"
14
-
Removed:
| ".cpp" | ".cc" | ".cxx" | ".hpp" -> Some "cpp"
15
-
Removed:
| ".cs" -> Some "csharp"
16
-
Removed:
| ".css" -> Some "css"
17
-
Removed:
| ".diff" | ".patch" -> Some "diff"
18
-
Removed:
| ".el" | ".lisp" | ".cl" -> Some "lisp"
19
-
Removed:
| ".erl" -> Some "erlang"
20
-
Removed:
| ".ex" | ".exs" -> Some "elixir"
21
-
Removed:
| ".go" -> Some "go"
22
-
Removed:
| ".hs" -> Some "haskell"
23
-
Removed:
| ".html" | ".htm" -> Some "xml"
24
-
Removed:
| ".java" -> Some "java"
25
-
Removed:
| ".js" | ".mjs" | ".cjs" -> Some "javascript"
26
-
Removed:
| ".json" -> Some "json"
27
-
Removed:
| ".kt" -> Some "kotlin"
28
-
Removed:
| ".lua" -> Some "lua"
29
-
Removed:
| ".md" -> Some "markdown"
30
-
Removed:
| ".nix" -> Some "nix"
31
-
Removed:
| ".php" -> Some "php"
32
-
Removed:
| ".pl" | ".pm" | ".t" -> Some "perl"
33
-
Removed:
| ".py" -> Some "python"
34
-
Removed:
| ".r" -> Some "r"
35
-
Removed:
| ".rb" -> Some "ruby"
36
-
Removed:
| ".rs" -> Some "rust"
37
-
Removed:
| ".scala" -> Some "scala"
38
-
Removed:
| ".sh" | ".bash" | ".zsh" -> Some "bash"
39
-
Removed:
| ".sql" -> Some "sql"
40
-
Removed:
| ".swift" -> Some "swift"
41
-
Removed:
| ".toml" -> Some "ini"
42
-
Removed:
| ".ts" | ".tsx" -> Some "typescript"
43
-
Removed:
| ".xml" | ".svg" | ".xsl" -> Some "xml"
44
-
Removed:
| ".yaml" | ".yml" -> Some "yaml"
45
-
Removed:
| ".zig" -> Some "zig"
46
-
Removed:
| _ -> None
47
-
Removed:
48
-
Removed:
let language_of_shebang line =
49
-
Removed:
if not (String.starts_with ~prefix:"#!" line) then None
50
-
Removed:
else
51
-
Removed:
(* Extract the last path component, ignoring env and arguments *)
52
-
Removed:
let rest = String.sub line 2 (String.length line - 2) in
53
-
Removed:
let parts = String.split_on_char ' ' (String.trim rest) in
54
-
Removed:
let interpreter =
55
-
Removed:
match parts with
56
-
Removed:
| [] -> ""
57
-
Removed:
| cmd :: args ->
58
-
Removed:
let base = Filename.basename cmd in
59
-
Removed:
if base = "env" then
60
-
Removed:
(* /usr/bin/env python3 — take next non-flag argument *)
61
-
Removed:
List.find_opt (fun s -> s <> "" && s.[0] <> '-') args
62
-
Removed:
|> Option.value ~default:"" |> Filename.basename
63
-
Removed:
else base
64
-
Removed:
in
65
-
Removed:
(* Strip version suffixes: python3.11 -> python, ruby3.2 -> ruby *)
66
-
Removed:
let strip_trailing_digits s =
67
-
Removed:
let len = String.length s in
68
-
Removed:
let rec find_end i =
69
-
Removed:
if i < 0 then s
70
-
Removed:
else if s.[i] >= '0' && s.[i] <= '9' then find_end (i - 1)
71
-
Removed:
else String.sub s 0 (i + 1)
72
-
Removed:
in
73
-
Removed:
find_end (len - 1)
74
-
Removed:
in
75
-
Removed:
let interpreter =
76
-
Removed:
match String.split_on_char '.' interpreter with
77
-
Removed:
| [] -> ""
78
-
Removed:
| base :: _ -> strip_trailing_digits base
79
-
Removed:
in
80
-
Removed:
match String.lowercase_ascii interpreter with
81
-
Removed:
| "sh" | "bash" | "dash" | "ash" | "zsh" -> Some "bash"
82
-
Removed:
| "python" -> Some "python"
83
-
Removed:
| "ruby" -> Some "ruby"
84
-
Removed:
| "perl" -> Some "perl"
85
-
Removed:
| "node" | "deno" | "bun" -> Some "javascript"
86
-
Removed:
| "lua" -> Some "lua"
87
-
Removed:
| "php" -> Some "php"
88
-
Removed:
| "elixir" -> Some "elixir"
89
-
Removed:
| "awk" | "gawk" | "mawk" -> Some "awk"
90
-
Removed:
| "ocaml" -> Some "ocaml"
91
-
Removed:
| _ -> None
92
-
Removed:
93
-
Removed:
let language_of_emacs_prop line =
94
-
Removed:
let find_between s prefix suffix =
95
-
Removed:
let plen = String.length prefix in
96
-
Removed:
let slen = String.length suffix in
97
-
Removed:
let total = String.length s in
98
-
Removed:
let rec find_start i =
99
-
Removed:
if i > total - plen then None
100
-
Removed:
else if String.sub s i plen = prefix then
101
-
Removed:
let after = i + plen in
102
-
Removed:
let rec find_end j =
103
-
Removed:
if j > total - slen then None
104
-
Removed:
else if String.sub s j slen = suffix then
105
-
Removed:
Some (String.sub s after (j - after) |> String.trim)
106
-
Removed:
else find_end (j + 1)
107
-
Removed:
in
108
-
Removed:
find_end after
109
-
Removed:
else find_start (i + 1)
110
-
Removed:
in
111
-
Removed:
find_start 0
112
-
Removed:
in
113
-
Removed:
let extract_mode between =
114
-
Removed:
let props = String.split_on_char ';' between in
115
-
Removed:
let mode_prop =
116
-
Removed:
List.find_map
117
-
Removed:
(fun prop ->
118
-
Removed:
match String.split_on_char ':' (String.trim prop) with
119
-
Removed:
| [ key; value ]
120
-
Removed:
when String.trim (String.lowercase_ascii key) = "mode" ->
121
-
Removed:
Some (String.trim value)
122
-
Removed:
| _ -> None)
123
-
Removed:
props
124
-
Removed:
in
125
-
Removed:
match mode_prop with
126
-
Removed:
| Some _ -> mode_prop
127
-
Removed:
| None ->
128
-
Removed:
if
129
-
Removed:
(not (String.contains between ':'))
130
-
Removed:
&& not (String.contains between ';')
131
-
Removed:
then Some (String.trim between)
132
-
Removed:
else None
133
-
Removed:
in
134
-
Removed:
let normalize_mode mode =
135
-
Removed:
match String.lowercase_ascii mode with
136
-
Removed:
| "tuareg" | "caml" | "ocaml" -> Some "ocaml"
137
-
Removed:
| "emacs-lisp" | "lisp" | "elisp" -> Some "lisp"
138
-
Removed:
| "shell-script" | "sh" | "bash" -> Some "bash"
139
-
Removed:
| "python" -> Some "python"
140
-
Removed:
| "ruby" -> Some "ruby"
141
-
Removed:
| "perl" | "cperl" -> Some "perl"
142
-
Removed:
| "c" -> Some "c"
143
-
Removed:
| "c++" -> Some "cpp"
144
-
Removed:
| "javascript" | "js" -> Some "javascript"
145
-
Removed:
| "typescript" -> Some "typescript"
146
-
Removed:
| "rust" -> Some "rust"
147
-
Removed:
| "go" -> Some "go"
148
-
Removed:
| "haskell" -> Some "haskell"
149
-
Removed:
| "lua" -> Some "lua"
150
-
Removed:
| "sql" -> Some "sql"
151
-
Removed:
| "yaml" -> Some "yaml"
152
-
Removed:
| "nix" -> Some "nix"
153
-
Removed:
| "makefile" -> Some "makefile"
154
-
Removed:
| m -> Some m
155
-
Removed:
in
156
-
Removed:
let ( >>= ) = Option.bind in
157
-
Removed:
find_between line "-*-" "-*-" >>= extract_mode >>= normalize_mode
158
-
Removed:
159
-
Removed:
let language_of_vim_modeline line =
160
-
Removed:
let contains_substring s sub =
161
-
Removed:
let slen = String.length s in
162
-
Removed:
let sublen = String.length sub in
163
-
Removed:
let rec check i =
164
-
Removed:
if i > slen - sublen then false
165
-
Removed:
else if String.sub s i sublen = sub then true
166
-
Removed:
else check (i + 1)
167
-
Removed:
in
168
-
Removed:
sublen <= slen && check 0
169
-
Removed:
in
170
-
Removed:
let l = String.lowercase_ascii line in
171
-
Removed:
let has_vim_prefix =
172
-
Removed:
contains_substring l "vim:"
173
-
Removed:
|| contains_substring l "vi:" || contains_substring l "ex:"
174
-
Removed:
in
175
-
Removed:
if not has_vim_prefix then None
176
-
Removed:
else
177
-
Removed:
let find_value prefix s =
178
-
Removed:
let plen = String.length prefix in
179
-
Removed:
let slen = String.length s in
180
-
Removed:
let rec find_at i =
181
-
Removed:
if i > slen - plen then None
182
-
Removed:
else if String.sub s i plen = prefix then
183
-
Removed:
let vstart = i + plen in
184
-
Removed:
let rec scan_end j =
185
-
Removed:
if j >= slen || s.[j] = ' ' || s.[j] = ':' || s.[j] = '\t' then j
186
-
Removed:
else scan_end (j + 1)
187
-
Removed:
in
188
-
Removed:
let vend = scan_end vstart in
189
-
Removed:
Some (String.sub s vstart (vend - vstart))
190
-
Removed:
else find_at (i + 1)
191
-
Removed:
in
192
-
Removed:
find_at 0
193
-
Removed:
in
194
-
Removed:
let ft =
195
-
Removed:
match find_value "ft=" l with
196
-
Removed:
| Some _ as r -> r
197
-
Removed:
| None -> find_value "filetype=" l
198
-
Removed:
in
199
-
Removed:
match ft with
200
-
Removed:
| None -> None
201
-
Removed:
| Some ft -> (
202
-
Removed:
match ft with
203
-
Removed:
| "sh" | "bash" | "zsh" -> Some "bash"
204
-
Removed:
| "python" -> Some "python"
205
-
Removed:
| "ruby" -> Some "ruby"
206
-
Removed:
| "perl" -> Some "perl"
207
-
Removed:
| "javascript" | "js" -> Some "javascript"
208
-
Removed:
| "typescript" -> Some "typescript"
209
-
Removed:
| "ocaml" -> Some "ocaml"
210
-
Removed:
| "c" -> Some "c"
211
-
Removed:
| "cpp" -> Some "cpp"
212
-
Removed:
| "rust" -> Some "rust"
213
-
Removed:
| "go" -> Some "go"
214
-
Removed:
| "haskell" -> Some "haskell"
215
-
Removed:
| "lua" -> Some "lua"
216
-
Removed:
| "make" | "makefile" -> Some "makefile"
217
-
Removed:
| "yaml" -> Some "yaml"
218
-
Removed:
| "sql" -> Some "sql"
219
-
Removed:
| "nix" -> Some "nix"
220
-
Removed:
| other -> Some other)
221
-
Removed:
222
-
Removed:
let language_of_content content =
223
-
Removed:
let lines = String.split_on_char '\n' content in
224
-
Removed:
let len = List.length lines in
225
-
Removed:
let first_lines =
226
-
Removed:
let n = min 5 len in
227
-
Removed:
List_ext.take n lines
228
-
Removed:
in
229
-
Removed:
let last_lines =
230
-
Removed:
let start = max 0 (len - 5) in
231
-
Removed:
List_ext.drop start lines
232
-
Removed:
in
233
-
Removed:
let try_lines detector lines = List.find_map detector lines in
234
-
Removed:
let ( <|> ) a b = match a with Some _ -> a | None -> b () in
235
-
Removed:
match first_lines with
236
-
Removed:
| [] -> None
237
-
Removed:
| first :: _ ->
238
-
Removed:
( ( language_of_shebang first <|> fun () ->
239
-
Removed:
try_lines language_of_emacs_prop first_lines )
240
-
Removed:
<|> fun () -> try_lines language_of_vim_modeline first_lines )
241
-
Removed:
<|> fun () -> try_lines language_of_vim_modeline last_lines
242
-
Removed:
243
-
Removed:
let page_title context = context.repo ^ " — " ^ context.description
244
-
Removed:
245
-
Removed:
let render_page ?heading ?toolbar context ~active content =
246
-
Removed:
respond
247
-
Removed:
@@ Layout.render context.site ~page_title:(page_title context)
248
-
Removed:
{
249
-
Removed:
repo = Some context.repo;
250
-
Removed:
title = Option.value heading ~default:context.repo;
251
-
Removed:
subtitle = context.description;
252
-
Removed:
active;
253
-
Removed:
toolbar = Option.value toolbar ~default:[];
254
-
Removed:
home_href = None;
255
-
Removed:
content;
256
-
Removed:
}
257
-
Removed:
258
-
Removed:
let li_of_branch repo (branch : Resolvers.Reference.t) =
259
-
Removed:
HTML.(
260
-
Removed:
li []
261
-
Removed:
[
262
-
Removed:
Routes.link_to
263
-
Removed:
(Commits_branch (repo, branch.name))
264
-
Removed:
(txt "%s" branch.name);
265
-
Removed:
])
266
-
Removed:
267
-
Removed:
let li_of_tag repo (tag : Resolvers.Reference.t) =
268
-
Removed:
HTML.(li [] [ Routes.link_to (Tags repo) (txt "%s" tag.name) ])
269
-
Removed:
270
-
Removed:
let parse_commit_message = function
271
-
Removed:
| None -> { summary = ""; body = "" }
272
-
Removed:
| Some message -> (
273
-
Removed:
match String.split_on_char '\n' message with
274
-
Removed:
| [] -> { summary = ""; body = "" }
275
-
Removed:
| summary :: rest ->
276
-
Removed:
{ summary; body = String.concat "\n" rest |> String.trim })
277
-
Removed:
278
-
Removed:
let conventional_commit_types =
279
-
Removed:
[
280
-
Removed:
"feat";
281
-
Removed:
"fix";
282
-
Removed:
"docs";
283
-
Removed:
"style";
284
-
Removed:
"refactor";
285
-
Removed:
"perf";
286
-
Removed:
"test";
287
-
Removed:
"build";
288
-
Removed:
"ci";
289
-
Removed:
"chore";
290
-
Removed:
"revert";
291
-
Removed:
]
292
-
Removed:
293
-
Removed:
let parse_conventional summary =
294
-
Removed:
match String.index_opt summary ':' with
295
-
Removed:
| None -> (None, summary)
296
-
Removed:
| Some colon_pos ->
297
-
Removed:
let prefix = String.sub summary 0 colon_pos in
298
-
Removed:
let type_name =
299
-
Removed:
match String.index_opt prefix '(' with
300
-
Removed:
| Some paren_pos -> String.sub prefix 0 paren_pos
301
-
Removed:
| None -> prefix
302
-
Removed:
in
303
-
Removed:
let type_lower = String.lowercase_ascii type_name in
304
-
Removed:
if List.mem type_lower conventional_commit_types then
305
-
Removed:
let rest =
306
-
Removed:
String.sub summary (colon_pos + 1)
307
-
Removed:
(String.length summary - colon_pos - 1)
308
-
Removed:
|> String.trim
309
-
Removed:
in
310
-
Removed:
(Some type_lower, rest)
311
-
Removed:
else (None, summary)
312
-
Removed:
313
-
Removed:
let timestamp (date, _) =
314
-
Removed:
let tm = date |> Int64.to_float |> Unix.localtime in
315
-
Removed:
Printf.sprintf "%04d-%02d-%02d %02d:%02d" (tm.tm_year + 1900) (tm.tm_mon + 1)
316
-
Removed:
tm.tm_mday tm.tm_hour tm.tm_min
317
-
Removed:
318
-
Removed:
let detailed_timestamp (date, timezone) =
319
-
Removed:
let offset_seconds, suffix =
320
-
Removed:
match timezone with
321
-
Removed:
| None -> (0, "Z")
322
-
Removed:
| Some (offset : Git.User.tz_offset) ->
323
-
Removed:
let direction = match offset.sign with `Plus -> 1 | `Minus -> -1 in
324
-
Removed:
let seconds = direction * ((offset.hours * 60) + offset.minutes) * 60 in
325
-
Removed:
let sign = match offset.sign with `Plus -> "+" | `Minus -> "-" in
326
-
Removed:
(seconds, Printf.sprintf "%s%02d:%02d" sign offset.hours offset.minutes)
327
-
Removed:
in
328
-
Removed:
let adjusted = Int64.add date (Int64.of_int offset_seconds) in
329
-
Removed:
let tm = adjusted |> Int64.to_float |> Unix.gmtime in
330
-
Removed:
let date =
331
-
Removed:
Printf.sprintf "%04d-%02d-%02d" (tm.tm_year + 1900) (tm.tm_mon + 1)
332
-
Removed:
tm.tm_mday
333
-
Removed:
in
334
-
Removed:
let clock = Printf.sprintf "%02d:%02d:%02d" tm.tm_hour tm.tm_min tm.tm_sec in
335
-
Removed:
( Printf.sprintf "%sT%s%s" date clock suffix,
336
-
Removed:
Printf.sprintf "%s %s %s" date clock suffix )
337
-
Removed:
338
-
Removed:
let time_node date =
339
-
Removed:
let machine_time, display_time = detailed_timestamp date in
340
-
Removed:
HTML.(time [ datetime "%s" machine_time ] [ txt "%s" display_time ])
341
-
Removed:
342
-
Removed:
let commits_url ?filter_type ?author ?committer ?(page = 1) repo =
343
-
Removed:
let params =
344
-
Removed:
(if page > 1 then [ ("page", string_of_int page) ] else [])
345
-
Removed:
@ (match filter_type with Some value -> [ ("type", value) ] | None -> [])
346
-
Removed:
@ (match author with Some value -> [ ("author", value) ] | None -> [])
347
-
Removed:
@ match committer with Some value -> [ ("committer", value) ] | None -> []
348
-
Removed:
in
349
-
Removed:
let base = Printf.sprintf "/%s/commits/" repo in
350
-
Removed:
match params with
351
-
Removed:
| [] -> base
352
-
Removed:
| _ -> base ^ "?" ^ Dream.to_form_urlencoded params
353
-
Removed:
354
-
Removed:
let identity_link ?filter_type ?author ?committer ?(show_email = false) ~role
355
-
Removed:
repo (user : Resolvers.Commit.user) =
356
-
Removed:
let url, role_label =
357
-
Removed:
match role with
358
-
Removed:
| `Author ->
359
-
Removed:
(commits_url ?filter_type ~author:user.email ?committer repo, "author")
360
-
Removed:
| `Committer ->
361
-
Removed:
( commits_url ?filter_type ?author ~committer:user.email repo,
362
-
Removed:
"committer" )
363
-
Removed:
in
364
-
Removed:
let link_text =
365
-
Removed:
if show_email then Printf.sprintf "%s <%s>" user.name user.email
366
-
Removed:
else user.name
367
-
Removed:
in
368
-
Removed:
HTML.(
369
-
Removed:
a
370
-
Removed:
[
371
-
Removed:
href "%s" url;
372
-
Removed:
class_ "commit-identity";
373
-
Removed:
Aria.label "Filter commits by %s %s" role_label user.name;
374
-
Removed:
]
375
-
Removed:
[ txt "%s" link_text ])
376
-
Removed:
377
-
Removed:
let li_of_commit ?filter_type ?author ?committer ?(hide_pill = false) repo
378
-
Removed:
(commit : Resolvers.Commit.t) =
379
-
Removed:
let message = parse_commit_message commit.message in
380
-
Removed:
let commit_type, commit_title = parse_conventional message.summary in
381
-
Removed:
let commit_route = Routes.Commit (repo, commit.hash) in
382
-
Removed:
let timestamp_span =
383
-
Removed:
HTML.(
384
-
Removed:
span [ class_ "timestamp" ] [ txt "%s" (timestamp commit.author.date) ])
385
-
Removed:
in
386
-
Removed:
let pill =
387
-
Removed:
match commit_type with
388
-
Removed:
| _ when hide_pill -> HTML.null []
389
-
Removed:
| None -> HTML.null []
390
-
Removed:
| Some ct ->
391
-
Removed:
HTML.(
392
-
Removed:
span
393
-
Removed:
[ class_ "commit-pill commit-pill-%s" ct ]
394
-
Removed:
[
395
-
Removed:
a
396
-
Removed:
[
397
-
Removed:
href "%s"
398
-
Removed:
(commits_url ~filter_type:ct ?author ?committer repo);
399
-
Removed:
]
400
-
Removed:
[ txt "%s" ct ];
401
-
Removed:
])
402
-
Removed:
in
403
-
Removed:
let title_span =
404
-
Removed:
HTML.(
405
-
Removed:
span
406
-
Removed:
[ class_ "commit-title" ]
407
-
Removed:
[ pill; Routes.link_to commit_route (txt "%s" commit_title) ])
408
-
Removed:
in
409
-
Removed:
let ago_span =
410
-
Removed:
HTML.(
411
-
Removed:
span
412
-
Removed:
[ class_ "commit-ago" ]
413
-
Removed:
[ txt "%s" (Time_fmt.relative_time commit.author.date) ])
414
-
Removed:
in
415
-
Removed:
let author_span =
416
-
Removed:
match author with
417
-
Removed:
| Some _ -> HTML.null []
418
-
Removed:
| None ->
419
-
Removed:
HTML.(
420
-
Removed:
span
421
-
Removed:
[ class_ "commit-author" ]
422
-
Removed:
[
423
-
Removed:
identity_link ?filter_type ?author ?committer ~role:`Author repo
424
-
Removed:
commit.author;
425
-
Removed:
])
426
-
Removed:
in
427
-
Removed:
HTML.(li [] [ timestamp_span; title_span; ago_span; author_span ])
428
-
Removed:
429
-
Removed:
let rec li_of_tree_node repo (pe : Resolvers.Tree.tree_node) =
430
-
Removed:
let entry = pe.entry in
431
-
Removed:
let route = Routes.File (repo, entry.hash) in
432
-
Removed:
let is_hidden = String.length entry.name > 0 && entry.name.[0] = '.' in
433
-
Removed:
let hidden_class = if is_hidden then " tree-hidden" else "" in
434
-
Removed:
match pe.children with
435
-
Removed:
| None ->
436
-
Removed:
(* Regular file *)
437
-
Removed:
HTML.(
438
-
Removed:
li
439
-
Removed:
[ class_ "tree-file%s" hidden_class ]
440
-
Removed:
[ Routes.link_to route (txt "%s" entry.name) ])
441
-
Removed:
| Some children ->
442
-
Removed:
let max_display = 16 in
443
-
Removed:
let total = List.length children in
444
-
Removed:
let displayed, overflow =
445
-
Removed:
if total <= max_display then (children, 0)
446
-
Removed:
else (List_ext.take max_display children, total - max_display)
447
-
Removed:
in
448
-
Removed:
let overflow_item =
449
-
Removed:
if overflow = 0 then []
450
-
Removed:
else
451
-
Removed:
HTML.
452
-
Removed:
[
453
-
Removed:
li
454
-
Removed:
[ class_ "tree-overflow" ]
455
-
Removed:
[
456
-
Removed:
Routes.link_to route
457
-
Removed:
(txt "%d more items\xe2\x80\xa6" overflow);
458
-
Removed:
];
459
-
Removed:
]
460
-
Removed:
in
461
-
Removed:
Components.tree_dir ~extra_class:hidden_class ~route
462
-
Removed:
~label:(entry.name ^ "/")
463
-
Removed:
(List.map (li_of_tree_node repo) displayed @ overflow_item)
464
-
Removed:
465
-
Removed:
let summary context ?readme frequency =
466
-
Removed:
let chart_section =
467
-
Removed:
HTML.
468
-
Removed:
[
469
-
Removed:
h3 [] [ txt "Commit activity (past 30 days)" ];
470
-
Removed:
div
471
-
Removed:
[ class_ "chart-container" ]
472
-
Removed:
[
473
-
Removed:
Charts.commit_frequency ~chart_width:600 ~chart_height:200 frequency;
474
-
Removed:
];
475
-
Removed:
p
476
-
Removed:
[ class_ "summary-more" ]
477
-
Removed:
[ Routes.link_to (Commits context.repo) (txt "View all commits") ];
478
-
Removed:
]
479
-
Removed:
in
480
-
Removed:
let readme_section =
481
-
Removed:
match readme with
482
-
Removed:
| None -> HTML.null []
483
-
Removed:
| Some (blob : Resolvers.Blob.t) ->
484
-
Removed:
let formatted =
485
-
Removed:
String.split_on_char '\n' blob.content
486
-
Removed:
|> List.mapi (fun number line ->
487
-
Removed:
let line_number = number + 1 in
488
-
Removed:
HTML.
489
-
Removed:
[
490
-
Removed:
a
491
-
Removed:
[
492
-
Removed:
id "readme-%d" line_number;
493
-
Removed:
class_ "line-anchor";
494
-
Removed:
href "#readme-%d" line_number;
495
-
Removed:
Aria.label "Line %d" line_number;
496
-
Removed:
]
497
-
Removed:
[ txt "%d" line_number ];
498
-
Removed:
span [ class_ "line" ] [ txt "\t%s\n" line ];
499
-
Removed:
])
500
-
Removed:
|> List.concat
501
-
Removed:
in
502
-
Removed:
HTML.(
503
-
Removed:
section
504
-
Removed:
[ class_ "readme-inline" ]
505
-
Removed:
[ h3 [] [ txt "README" ]; div [ class_ "blob" ] formatted ])
506
-
Removed:
in
507
-
Removed:
let clone_link =
508
-
Removed:
HTML.(
509
-
Removed:
a
510
-
Removed:
[
511
-
Removed:
href "/%s" context.repo;
512
-
Removed:
class_ "toolbar-button";
513
-
Removed:
Aria.label "Clone %s" context.repo;
514
-
Removed:
]
515
-
Removed:
[ txt "Clone repo" ])
516
-
Removed:
in
517
-
Removed:
render_page context ~active:Summary ~toolbar:[ clone_link ]
518
-
Removed:
HTML.
519
-
Removed:
[
520
-
Removed:
div
521
-
Removed:
[ class_ "summary-layout" ]
522
-
Removed:
[
523
-
Removed:
div [ class_ "summary-commits" ] chart_section;
524
-
Removed:
div [ class_ "summary-readme" ] [ readme_section ];
525
-
Removed:
];
526
-
Removed:
]
527
-
Removed:
528
-
Removed:
let commits ?filter_type ?author ?committer ~page ~has_prev ~has_next context
529
-
Removed:
commits =
530
-
Removed:
let hide_pill = Option.is_some filter_type in
531
-
Removed:
let filters =
532
-
Removed:
(match filter_type with
533
-
Removed:
| None -> []
534
-
Removed:
| Some commit_type ->
535
-
Removed:
[
536
-
Removed:
( "commit type",
537
-
Removed:
commit_type,
538
-
Removed:
commits_url ?author ?committer context.repo,
539
-
Removed:
"commit-pill commit-pill-" ^ commit_type );
540
-
Removed:
])
541
-
Removed:
@ (match author with
542
-
Removed:
| None -> []
543
-
Removed:
| Some email ->
544
-
Removed:
[
545
-
Removed:
( "author",
546
-
Removed:
"Author: " ^ email,
547
-
Removed:
commits_url ?filter_type ?committer context.repo,
548
-
Removed:
"toolbar-filter-value" );
549
-
Removed:
])
550
-
Removed:
@
551
-
Removed:
match committer with
552
-
Removed:
| None -> []
553
-
Removed:
| Some email ->
554
-
Removed:
[
555
-
Removed:
( "committer",
556
-
Removed:
"Committer: " ^ email,
557
-
Removed:
commits_url ?filter_type ?author context.repo,
558
-
Removed:
"toolbar-filter-value" );
559
-
Removed:
]
560
-
Removed:
in
561
-
Removed:
let page_url page =
562
-
Removed:
commits_url ?filter_type ?author ?committer ~page context.repo
563
-
Removed:
in
564
-
Removed:
let show_pagination = has_prev || has_next in
565
-
Removed:
let filters_toolbar =
566
-
Removed:
match filters with
567
-
Removed:
| [] -> None
568
-
Removed:
| _ ->
569
-
Removed:
let filter_el (filter_name, display, dismiss_href, value_class) =
570
-
Removed:
HTML.(
571
-
Removed:
span
572
-
Removed:
[ class_ "toolbar-filter" ]
573
-
Removed:
[
574
-
Removed:
span [ class_ "%s" value_class ] [ txt "%s" display ];
575
-
Removed:
a
576
-
Removed:
[
577
-
Removed:
href "%s" dismiss_href;
578
-
Removed:
class_ "toolbar-dismiss";
579
-
Removed:
Aria.label "Remove %s filter" filter_name;
580
-
Removed:
]
581
-
Removed:
[ txt "\xc3\x97" ];
582
-
Removed:
])
583
-
Removed:
in
584
-
Removed:
Some
585
-
Removed:
HTML.(div [ class_ "toolbar-filters" ] (List.map filter_el filters))
586
-
Removed:
in
587
-
Removed:
let pagination_toolbar =
588
-
Removed:
if not show_pagination then None
589
-
Removed:
else
590
-
Removed:
Some
591
-
Removed:
HTML.(
592
-
Removed:
nav
593
-
Removed:
[ class_ "toolbar-pagination"; Aria.label "Pagination" ]
594
-
Removed:
[
595
-
Removed:
(if has_prev then
596
-
Removed:
a
597
-
Removed:
[
598
-
Removed:
href "%s" (page_url (page - 1));
599
-
Removed:
class_ "pagination-btn";
600
-
Removed:
Aria.label "Previous page";
601
-
Removed:
]
602
-
Removed:
[ txt "<" ]
603
-
Removed:
else
604
-
Removed:
span
605
-
Removed:
[
606
-
Removed:
class_ "pagination-btn pagination-disabled";
607
-
Removed:
Aria.hidden true;
608
-
Removed:
]
609
-
Removed:
[ txt "<" ]);
610
-
Removed:
span
611
-
Removed:
[ class_ "pagination-page"; Aria.current `page ]
612
-
Removed:
[ txt "%d" page ];
613
-
Removed:
(if has_next then
614
-
Removed:
a
615
-
Removed:
[
616
-
Removed:
href "%s" (page_url (page + 1));
617
-
Removed:
class_ "pagination-btn";
618
-
Removed:
Aria.label "Next page";
619
-
Removed:
]
620
-
Removed:
[ txt ">" ]
621
-
Removed:
else
622
-
Removed:
span
623
-
Removed:
[
624
-
Removed:
class_ "pagination-btn pagination-disabled";
625
-
Removed:
Aria.hidden true;
626
-
Removed:
]
627
-
Removed:
[ txt ">" ]);
628
-
Removed:
])
629
-
Removed:
in
630
-
Removed:
let toolbar =
631
-
Removed:
List.filter_map Fun.id [ filters_toolbar; pagination_toolbar ]
632
-
Removed:
in
633
-
Removed:
render_page context ~active:Commits ~toolbar
634
-
Removed:
HTML.
635
-
Removed:
[
636
-
Removed:
ul
637
-
Removed:
[ id "commit-list" ]
638
-
Removed:
(List.map
639
-
Removed:
(li_of_commit ~hide_pill ?filter_type ?author ?committer
640
-
Removed:
context.repo)
641
-
Removed:
commits);
642
-
Removed:
]
643
-
Removed:
644
-
Removed:
let breadcrumb_pill repo (trail : (string * string) list) =
645
-
Removed:
let repo_name =
646
-
Removed:
match List.rev (String.split_on_char '/' repo) with
647
-
Removed:
| name :: _ -> name
648
-
Removed:
| [] -> repo
649
-
Removed:
in
650
-
Removed:
let repo_anchor =
651
-
Removed:
Routes.link_to (Files repo)
652
-
Removed:
~other_attrs:[ HTML.class_ "path-pill-link" ]
653
-
Removed:
(txt "%s" repo_name)
654
-
Removed:
in
655
-
Removed:
let file_segments =
656
-
Removed:
List.map
657
-
Removed:
(fun (entry_name, hash) ->
658
-
Removed:
HTML.(
659
-
Removed:
null
660
-
Removed:
[
661
-
Removed:
span [ class_ "path-pill-sep" ] [ txt "/" ];
662
-
Removed:
Routes.link_to
663
-
Removed:
(File (repo, hash))
664
-
Removed:
~other_attrs:[ class_ "path-pill-link" ]
665
-
Removed:
(txt "%s" entry_name);
666
-
Removed:
]))
667
-
Removed:
trail
668
-
Removed:
in
669
-
Removed:
HTML.(span [ class_ "path-pill" ] (repo_anchor :: file_segments))
670
-
Removed:
671
-
Removed:
let files context trail ?readme (entries : Resolvers.Tree.tree_node list) =
672
-
Removed:
let pill = breadcrumb_pill context.repo trail in
673
-
Removed:
let readme_section =
674
-
Removed:
match readme with
675
-
Removed:
| None -> HTML.null []
676
-
Removed:
| Some (blob : Resolvers.Blob.t) ->
677
-
Removed:
let formatted =
678
-
Removed:
String.split_on_char '\n' blob.content
679
-
Removed:
|> List.mapi (fun number line ->
680
-
Removed:
let line_number = number + 1 in
681
-
Removed:
HTML.
682
-
Removed:
[
683
-
Removed:
a
684
-
Removed:
[
685
-
Removed:
id "readme-%d" line_number;
686
-
Removed:
class_ "line-anchor";
687
-
Removed:
href "#readme-%d" line_number;
688
-
Removed:
Aria.label "Line %d" line_number;
689
-
Removed:
]
690
-
Removed:
[ txt "%d" line_number ];
691
-
Removed:
span [ class_ "line" ] [ txt "\t%s\n" line ];
692
-
Removed:
])
693
-
Removed:
|> List.concat
694
-
Removed:
in
695
-
Removed:
HTML.(
696
-
Removed:
section
697
-
Removed:
[ class_ "readme-inline" ]
698
-
Removed:
[ h3 [] [ txt "README" ]; div [ class_ "blob" ] formatted ])
699
-
Removed:
in
700
-
Removed:
render_page context ~active:Files ~toolbar:[ pill ]
701
-
Removed:
HTML.
702
-
Removed:
[
703
-
Removed:
ul [ id "file-tree" ] (List.map (li_of_tree_node context.repo) entries);
704
-
Removed:
readme_section;
705
-
Removed:
]
706
-
Removed:
707
-
Removed:
let file ?(active = Layout.Files) context trail (blob : Resolvers.Blob.t) =
708
-
Removed:
let language =
709
-
Removed:
let from_filename =
710
-
Removed:
match List.rev trail with
711
-
Removed:
| (name, _) :: _ -> language_of_filename name
712
-
Removed:
| [] -> None
713
-
Removed:
in
714
-
Removed:
match from_filename with
715
-
Removed:
| Some _ -> from_filename
716
-
Removed:
| None -> language_of_content blob.content
717
-
Removed:
in
718
-
Removed:
let blob_attrs =
719
-
Removed:
match language with
720
-
Removed:
| Some lang -> [ HTML.id "blob"; HTML.class_ "language-%s" lang ]
721
-
Removed:
| None -> [ HTML.id "blob" ]
722
-
Removed:
in
723
-
Removed:
let to_numbered_line number line =
724
-
Removed:
let line_number = number + 1 in
725
-
Removed:
HTML.
726
-
Removed:
[
727
-
Removed:
a
728
-
Removed:
[
729
-
Removed:
id "%d" line_number;
730
-
Removed:
class_ "line-anchor";
731
-
Removed:
href "#%d" line_number;
732
-
Removed:
Aria.label "Line %d" line_number;
733
-
Removed:
]
734
-
Removed:
[ txt "%d" line_number ];
735
-
Removed:
span [ class_ "line" ] [ txt "\t%s\n" line ];
736
-
Removed:
]
737
-
Removed:
in
738
-
Removed:
let formatted_blob =
739
-
Removed:
String.split_on_char '\n' blob.content
740
-
Removed:
|> List.mapi to_numbered_line |> List.concat
741
-
Removed:
in
742
-
Removed:
let raw_link =
743
-
Removed:
match List.rev trail with
744
-
Removed:
| (_, hash) :: _ ->
745
-
Removed:
HTML.(
746
-
Removed:
p []
747
-
Removed:
[ Routes.link_to (Raw_file (context.repo, hash)) (txt "View raw") ])
748
-
Removed:
| [] -> HTML.null []
749
-
Removed:
in
750
-
Removed:
let toolbar =
751
-
Removed:
match active with
752
-
Removed:
| Layout.Readme -> []
753
-
Removed:
| _ -> [ breadcrumb_pill context.repo trail ]
754
-
Removed:
in
755
-
Removed:
render_page context ~active ~toolbar
756
-
Removed:
HTML.[ raw_link; div blob_attrs formatted_blob ]
757
-
Removed:
758
-
Removed:
let commit context (commit : Resolvers.Commit.t) diff =
759
-
Removed:
let message = parse_commit_message commit.message in
760
-
Removed:
let number = function Some number -> string_of_int number | None -> "" in
761
-
Removed:
let line (line : Resolvers.Diff.line) =
762
-
Removed:
let class_name, marker, screen_reader_label =
763
-
Removed:
match line.kind with
764
-
Removed:
| Resolvers.Diff.Context -> ("context", " ", "")
765
-
Removed:
| Resolvers.Diff.Addition -> ("addition", "+", "Added: ")
766
-
Removed:
| Resolvers.Diff.Deletion -> ("deletion", "-", "Removed: ")
767
-
Removed:
in
768
-
Removed:
HTML.(
769
-
Removed:
div
770
-
Removed:
[ class_ "diff-line %s" class_name ]
771
-
Removed:
[
772
-
Removed:
span [ class_ "line-number" ] [ txt "%s" (number line.old_number) ];
773
-
Removed:
span [ class_ "line-number" ] [ txt "%s" (number line.new_number) ];
774
-
Removed:
span [ class_ "diff-marker"; Aria.hidden true ] [ txt "%s" marker ];
775
-
Removed:
span [ class_ "sr-only" ] [ txt "%s" screen_reader_label ];
776
-
Removed:
span [ class_ "diff-text" ] [ txt "%s" line.text ];
777
-
Removed:
])
778
-
Removed:
in
779
-
Removed:
let hunk (hunk : Resolvers.Diff.hunk) =
780
-
Removed:
HTML.
781
-
Removed:
[
782
-
Removed:
details
783
-
Removed:
[ class_ "diff-hunk"; open_ ]
784
-
Removed:
[
785
-
Removed:
summary
786
-
Removed:
[ class_ "hunk-header" ]
787
-
Removed:
[
788
-
Removed:
txt "@@ -%d,%d +%d,%d @@" hunk.old_start hunk.old_count
789
-
Removed:
hunk.new_start hunk.new_count;
790
-
Removed:
];
791
-
Removed:
div
792
-
Removed:
[ class_ "diff-lines-scroll" ]
793
-
Removed:
[ div [ class_ "diff-lines" ] (List.map line hunk.lines) ];
794
-
Removed:
];
795
-
Removed:
]
796
-
Removed:
in
797
-
Removed:
let mode = function
798
-
Removed:
| None -> "000000"
799
-
Removed:
| Some mode -> Printf.sprintf "%06o" mode
800
-
Removed:
in
801
-
Removed:
let hash = function
802
-
Removed:
| None -> "00000000"
803
-
Removed:
| Some hash -> Resolvers.short_hash hash
804
-
Removed:
in
805
-
Removed:
let file (file : Resolvers.Diff.file) =
806
-
Removed:
let file_body =
807
-
Removed:
if file.binary then
808
-
Removed:
HTML.[ p [ class_ "binary-diff" ] [ txt "Binary files differ" ] ]
809
-
Removed:
else List.concat_map hunk file.hunks
810
-
Removed:
in
811
-
Removed:
HTML.(
812
-
Removed:
details
813
-
Removed:
[ class_ "diff-file"; open_ ]
814
-
Removed:
(summary [ class_ "diff-file-header" ] [ txt "%s" file.path ]
815
-
Removed:
:: div
816
-
Removed:
[ class_ "diff-meta" ]
817
-
Removed:
[
818
-
Removed:
txt "index %s..%s %s..%s" (hash file.old_hash)
819
-
Removed:
(hash file.new_hash) (mode file.old_mode) (mode file.new_mode);
820
-
Removed:
]
821
-
Removed:
:: file_body))
822
-
Removed:
in
823
-
Removed:
let diff_content =
824
-
Removed:
match diff with
825
-
Removed:
| [] -> HTML.[ p [] [ txt "No file changes in this commit." ] ]
826
-
Removed:
| files -> List.map file files
827
-
Removed:
in
828
-
Removed:
let commit_type, commit_title = parse_conventional message.summary in
829
-
Removed:
let pill =
830
-
Removed:
match commit_type with
831
-
Removed:
| None -> HTML.null []
832
-
Removed:
| Some ct ->
833
-
Removed:
HTML.(
834
-
Removed:
span
835
-
Removed:
[ class_ "commit-pill commit-pill-%s" ct ]
836
-
Removed:
[
837
-
Removed:
a
838
-
Removed:
[ href "%s" (commits_url ~filter_type:ct context.repo) ]
839
-
Removed:
[ txt "%s" ct ];
840
-
Removed:
])
841
-
Removed:
in
842
-
Removed:
let content =
843
-
Removed:
HTML.(
844
-
Removed:
[ h3 [] [ pill; txt " %s" commit_title ] ]
845
-
Removed:
@ (if message.body = "" then []
846
-
Removed:
else [ p [ class_ "commit-body" ] [ txt "%s" message.body ] ])
847
-
Removed:
@ [
848
-
Removed:
dl
849
-
Removed:
[ class_ "commit-meta" ]
850
-
Removed:
[
851
-
Removed:
dt [] [ txt "Commit" ];
852
-
Removed:
dd [] [ txt "%s" commit.hash ];
853
-
Removed:
dt [] [ txt "Author" ];
854
-
Removed:
dd []
855
-
Removed:
[
856
-
Removed:
identity_link ~show_email:true ~role:`Author context.repo
857
-
Removed:
commit.author;
858
-
Removed:
];
859
-
Removed:
dt [] [ txt "Author date" ];
860
-
Removed:
dd [] [ time_node commit.author.date ];
861
-
Removed:
dt [] [ txt "Committer" ];
862
-
Removed:
dd []
863
-
Removed:
[
864
-
Removed:
identity_link ~show_email:true ~role:`Committer context.repo
865
-
Removed:
commit.committer;
866
-
Removed:
];
867
-
Removed:
dt [] [ txt "Committer date" ];
868
-
Removed:
dd [] [ time_node commit.committer.date ];
869
-
Removed:
];
870
-
Removed:
]
871
-
Removed:
@ diff_content)
872
-
Removed:
in
873
-
Removed:
render_page
874
-
Removed:
~heading:(context.repo ^ " : " ^ Resolvers.short_hash commit.hash)
875
-
Removed:
context ~active:Commits content
876
-
Removed:
877
-
Removed:
let branches context branches =
878
-
Removed:
let content =
879
-
Removed:
match branches with
880
-
Removed:
| [] -> HTML.[ p [] [ txt "No branches for repo %s" context.repo ] ]
881
-
Removed:
| branches ->
882
-
Removed:
HTML.
883
-
Removed:
[
884
-
Removed:
ul
885
-
Removed:
[ id "branch-list" ]
886
-
Removed:
(List.map (li_of_branch context.repo) branches);
887
-
Removed:
]
888
-
Removed:
in
889
-
Removed:
render_page context ~active:Branches content
890
-
Removed:
891
-
Removed:
let tags context tags =
892
-
Removed:
let content =
893
-
Removed:
match tags with
894
-
Removed:
| [] -> HTML.[ p [] [ txt "No tags for repo %s" context.repo ] ]
895
-
Removed:
| tags ->
896
-
Removed:
HTML.[ ul [ id "tag-list" ] (List.map (li_of_tag context.repo) tags) ]
897
-
Removed:
in
898
-
Removed:
render_page context ~active:Tags content
3
+
Added:
(** The repository pages: summary, commit list, file tree, blob, commit detail,
4
+
Added:
branches and tags.
5
+
Added:
6
+
Added:
Each page is a description: it names the parts it is made of and hands them
7
+
Added:
to {!Layout}. Markup lives in {!Ui}, ogit's page parts in {!Components},
8
+
Added:
language guessing in {!Syntax}, and date formatting in {!Time_fmt}. *)
9
+
Added:
10
+
Added:
type context = { repo : string; description : string; site : Layout.site }
11
+
Added:
(** What every repository page needs to know about its subject. *)
12
+
Added:
13
+
Added:
type commit_message = { summary : string; body : string }
14
+
Added:
15
+
Added:
let context ~site ~repo ~description = { repo; description; site }
16
+
Added:
17
+
Added:
(** {1 Commit messages} *)
18
+
Added:
19
+
Added:
let parse_commit_message = function
20
+
Added:
| None -> { summary = ""; body = "" }
21
+
Added:
| Some message -> (
22
+
Added:
match String.split_on_char '\n' message with
23
+
Added:
| [] -> { summary = ""; body = "" }
24
+
Added:
| summary :: rest ->
25
+
Added:
{ summary; body = String.concat "\n" rest |> String.trim })
26
+
Added:
27
+
Added:
let conventional_commit_types =
28
+
Added:
[
29
+
Added:
"feat";
30
+
Added:
"fix";
31
+
Added:
"docs";
32
+
Added:
"style";
33
+
Added:
"refactor";
34
+
Added:
"perf";
35
+
Added:
"test";
36
+
Added:
"build";
37
+
Added:
"ci";
38
+
Added:
"chore";
39
+
Added:
"revert";
40
+
Added:
]
41
+
Added:
42
+
Added:
(** Split a Conventional Commits subject into its type and the remaining title.
43
+
Added:
An unrecognised prefix is left in the title untouched, so non-conforming
44
+
Added:
histories still read correctly. *)
45
+
Added:
let parse_conventional summary =
46
+
Added:
match String.index_opt summary ':' with
47
+
Added:
| None -> (None, summary)
48
+
Added:
| Some colon_pos ->
49
+
Added:
let prefix = String.sub summary 0 colon_pos in
50
+
Added:
let type_name =
51
+
Added:
match String.index_opt prefix '(' with
52
+
Added:
| Some paren_pos -> String.sub prefix 0 paren_pos
53
+
Added:
| None -> prefix
54
+
Added:
in
55
+
Added:
let type_lower = String.lowercase_ascii type_name in
56
+
Added:
if List.mem type_lower conventional_commit_types then
57
+
Added:
let rest =
58
+
Added:
String.sub summary (colon_pos + 1)
59
+
Added:
(String.length summary - colon_pos - 1)
60
+
Added:
|> String.trim
61
+
Added:
in
62
+
Added:
(Some type_lower, rest)
63
+
Added:
else (None, summary)
64
+
Added:
65
+
Added:
(** {1 Links into the commit list} *)
66
+
Added:
67
+
Added:
let commits_url = Components.commits_url
68
+
Added:
69
+
Added:
(** A person's name, linking to the commits attributed to them. Keeps the other
70
+
Added:
active filters intact so identities compose with type filters. *)
71
+
Added:
let identity ?filter_type ?author ?committer ?(show_email = false) ~role repo
72
+
Added:
(user : Resolvers.Commit.user) =
73
+
Added:
let href, role_name =
74
+
Added:
match role with
75
+
Added:
| `Author ->
76
+
Added:
(commits_url ?filter_type ~author:user.email ?committer repo, "author")
77
+
Added:
| `Committer ->
78
+
Added:
( commits_url ?filter_type ?author ~committer:user.email repo,
79
+
Added:
"committer" )
80
+
Added:
in
81
+
Added:
Ui.text_link ~class_:"commit-identity"
82
+
Added:
~label:(Printf.sprintf "Filter commits by %s %s" role_name user.name)
83
+
Added:
~href
84
+
Added:
(if show_email then Printf.sprintf "%s <%s>" user.name user.email
85
+
Added:
else user.name)
86
+
Added:
87
+
Added:
(** {1 Page shell} *)
88
+
Added:
89
+
Added:
let page_title context = context.repo ^ " — " ^ context.description
90
+
Added:
91
+
Added:
let page ?heading ?(toolbar = []) context ~active content =
92
+
Added:
Ui.respond
93
+
Added:
@@ Layout.render context.site ~page_title:(page_title context)
94
+
Added:
{
95
+
Added:
repo = Some context.repo;
96
+
Added:
title = Option.value heading ~default:context.repo;
97
+
Added:
subtitle = context.description;
98
+
Added:
active;
99
+
Added:
toolbar;
100
+
Added:
home_href = None;
101
+
Added:
content;
102
+
Added:
}
103
+
Added:
104
+
Added:
(** {1 Rows} *)
105
+
Added:
106
+
Added:
let branch_row repo (branch : Resolvers.Reference.t) =
107
+
Added:
Ui.item
108
+
Added:
[ Components.route_link (Commits_branch (repo, branch.name)) branch.name ]
109
+
Added:
110
+
Added:
let tag_row repo (tag : Resolvers.Reference.t) =
111
+
Added:
Ui.item [ Components.route_link (Tags repo) tag.name ]
112
+
Added:
113
+
Added:
(** One line of the commit list: when it happened, what changed, and who did it.
114
+
Added:
115
+
Added:
@param hide_pill
116
+
Added:
suppresses the type badge when the list is already filtered to a single
117
+
Added:
type, where repeating it on every row adds nothing.
118
+
Added:
@param author
119
+
Added:
when filtering by author, the author column is dropped for the same
120
+
Added:
reason. *)
121
+
Added:
let commit_row ?filter_type ?author ?committer ?(hide_pill = false) repo
122
+
Added:
(commit : Resolvers.Commit.t) =
123
+
Added:
let message = parse_commit_message commit.message in
124
+
Added:
let commit_type, title = parse_conventional message.summary in
125
+
Added:
let badge =
126
+
Added:
match commit_type with
127
+
Added:
| Some commit_type when not hide_pill ->
128
+
Added:
Components.commit_type_badge
129
+
Added:
~href:(commits_url ~filter_type:commit_type ?author ?committer repo)
130
+
Added:
commit_type
131
+
Added:
| _ -> Ui.nothing
132
+
Added:
in
133
+
Added:
Ui.item
134
+
Added:
[
135
+
Added:
Ui.inline_text ~class_:"timestamp"
136
+
Added:
(Time_fmt.short_time commit.author.date);
137
+
Added:
Ui.inline ~class_:"commit-title"
138
+
Added:
[ badge; Components.route_link (Commit (repo, commit.hash)) title ];
139
+
Added:
Ui.inline_text ~class_:"commit-ago"
140
+
Added:
(Time_fmt.relative_time commit.author.date);
141
+
Added:
(match author with
142
+
Added:
| Some _ -> Ui.nothing
143
+
Added:
| None ->
144
+
Added:
Ui.inline ~class_:"commit-author"
145
+
Added:
[
146
+
Added:
identity ?filter_type ?author ?committer ~role:`Author repo
147
+
Added:
commit.author;
148
+
Added:
]);
149
+
Added:
]
150
+
Added:
151
+
Added:
(** Long directories are truncated with a link to the directory's own page,
152
+
Added:
keeping the tree scannable without hiding anything permanently. *)
153
+
Added:
let tree_display_limit = 16
154
+
Added:
155
+
Added:
let rec tree_row repo (node : Resolvers.Tree.tree_node) =
156
+
Added:
let entry = node.entry in
157
+
Added:
let route = Routes.File (repo, entry.hash) in
158
+
Added:
(* Dotfiles stay visible but are de-emphasised. *)
159
+
Added:
let modifier =
160
+
Added:
if String.length entry.name > 0 && entry.name.[0] = '.' then "tree-hidden"
161
+
Added:
else ""
162
+
Added:
in
163
+
Added:
match node.children with
164
+
Added:
| None -> Components.file_entry ~modifier ~route entry.name
165
+
Added:
| Some children ->
166
+
Added:
let total = List.length children in
167
+
Added:
let shown, omitted =
168
+
Added:
if total <= tree_display_limit then (children, 0)
169
+
Added:
else
170
+
Added:
(List_ext.take tree_display_limit children, total - tree_display_limit)
171
+
Added:
in
172
+
Added:
let overflow =
173
+
Added:
if omitted = 0 then [] else [ Components.truncated ~route omitted ]
174
+
Added:
in
175
+
Added:
Components.directory ~modifier ~route ~name:entry.name
176
+
Added:
(List.map (tree_row repo) shown @ overflow)
177
+
Added:
178
+
Added:
(** {1 Trails} *)
179
+
Added:
180
+
Added:
(** The path from the repository root to the entry being viewed. *)
181
+
Added:
let path_trail repo (trail : (string * string) list) =
182
+
Added:
let repo_name =
183
+
Added:
match List.rev (String.split_on_char '/' repo) with
184
+
Added:
| name :: _ -> name
185
+
Added:
| [] -> repo
186
+
Added:
in
187
+
Added:
let root = Ui.crumb ~href:(Components.url (Files repo)) repo_name in
188
+
Added:
let entries =
189
+
Added:
List.map
190
+
Added:
(fun (name, hash) ->
191
+
Added:
Ui.crumb ~href:(Components.url (File (repo, hash))) name)
192
+
Added:
trail
193
+
Added:
in
194
+
Added:
Ui.breadcrumb ~class_:"path-pill" ~link_class:"path-pill-link"
195
+
Added:
~separator_class:"path-pill-sep" ~separator:"/" (root :: entries)
196
+
Added:
197
+
Added:
(** {1 Pages} *)
198
+
Added:
199
+
Added:
let summary context ?readme frequency =
200
+
Added:
let activity =
201
+
Added:
[
202
+
Added:
Ui.heading ~level:3 [ Ui.text "Commit activity (past 30 days)" ];
203
+
Added:
Ui.block ~class_:"chart-container"
204
+
Added:
[ Charts.commit_frequency ~chart_width:600 ~chart_height:200 frequency ];
205
+
Added:
Ui.paragraph ~class_:"summary-more"
206
+
Added:
[ Components.route_link (Commits context.repo) "View all commits" ];
207
+
Added:
]
208
+
Added:
in
209
+
Added:
let readme_panel =
210
+
Added:
match readme with
211
+
Added:
| None -> Ui.nothing
212
+
Added:
| Some (blob : Resolvers.Blob.t) -> Components.inline_readme blob.content
213
+
Added:
in
214
+
Added:
page context ~active:Summary
215
+
Added:
~toolbar:
216
+
Added:
[
217
+
Added:
Ui.button_link
218
+
Added:
~label:(Printf.sprintf "Clone %s" context.repo)
219
+
Added:
~href:(Components.clone_url context.repo)
220
+
Added:
"Clone repo";
221
+
Added:
]
222
+
Added:
[
223
+
Added:
Ui.block ~class_:"summary-layout"
224
+
Added:
[
225
+
Added:
Ui.block ~class_:"summary-commits" activity;
226
+
Added:
Ui.block ~class_:"summary-readme" [ readme_panel ];
227
+
Added:
];
228
+
Added:
]
229
+
Added:
230
+
Added:
let commits ?filter_type ?author ?committer ~page:page_number ~has_prev
231
+
Added:
~has_next context commits =
232
+
Added:
(* Each active filter offers a control that clears just itself, leaving the
233
+
Added:
others applied. *)
234
+
Added:
let active_filters =
235
+
Added:
(match filter_type with
236
+
Added:
| None -> []
237
+
Added:
| Some commit_type ->
238
+
Added:
[
239
+
Added:
( "commit type",
240
+
Added:
commit_type,
241
+
Added:
commits_url ?author ?committer context.repo,
242
+
Added:
"commit-pill commit-pill-" ^ commit_type );
243
+
Added:
])
244
+
Added:
@ (match author with
245
+
Added:
| None -> []
246
+
Added:
| Some email ->
247
+
Added:
[
248
+
Added:
( "author",
249
+
Added:
"Author: " ^ email,
250
+
Added:
commits_url ?filter_type ?committer context.repo,
251
+
Added:
"toolbar-filter-value" );
252
+
Added:
])
253
+
Added:
@
254
+
Added:
match committer with
255
+
Added:
| None -> []
256
+
Added:
| Some email ->
257
+
Added:
[
258
+
Added:
( "committer",
259
+
Added:
"Committer: " ^ email,
260
+
Added:
commits_url ?filter_type ?author context.repo,
261
+
Added:
"toolbar-filter-value" );
262
+
Added:
]
263
+
Added:
in
264
+
Added:
let filters =
265
+
Added:
match active_filters with
266
+
Added:
| [] -> []
267
+
Added:
| filters ->
268
+
Added:
[
269
+
Added:
Ui.block ~class_:"toolbar-filters"
270
+
Added:
(List.map
271
+
Added:
(fun (name, value, dismiss_href, value_class) ->
272
+
Added:
Ui.dismissible ~value_class ~dismiss_href
273
+
Added:
~dismiss_label:(Printf.sprintf "Remove %s filter" name)
274
+
Added:
value)
275
+
Added:
filters);
276
+
Added:
]
277
+
Added:
in
278
+
Added:
let page_url n =
279
+
Added:
commits_url ?filter_type ?author ?committer ~page:n context.repo
280
+
Added:
in
281
+
Added:
let pagination =
282
+
Added:
if not (has_prev || has_next) then []
283
+
Added:
else
284
+
Added:
[
285
+
Added:
Ui.pagination
286
+
Added:
?previous_href:
287
+
Added:
(if has_prev then Some (page_url (page_number - 1)) else None)
288
+
Added:
?next_href:
289
+
Added:
(if has_next then Some (page_url (page_number + 1)) else None)
290
+
Added:
page_number;
291
+
Added:
]
292
+
Added:
in
293
+
Added:
page context ~active:Commits ~toolbar:(filters @ pagination)
294
+
Added:
[
295
+
Added:
Ui.items_of ~id:"commit-list"
296
+
Added:
(commit_row
297
+
Added:
~hide_pill:(Option.is_some filter_type)
298
+
Added:
?filter_type ?author ?committer context.repo)
299
+
Added:
commits;
300
+
Added:
]
301
+
Added:
302
+
Added:
let files context trail ?readme (entries : Resolvers.Tree.tree_node list) =
303
+
Added:
let readme_panel =
304
+
Added:
match readme with
305
+
Added:
| None -> Ui.nothing
306
+
Added:
| Some (blob : Resolvers.Blob.t) -> Components.inline_readme blob.content
307
+
Added:
in
308
+
Added:
page context ~active:Files
309
+
Added:
~toolbar:[ path_trail context.repo trail ]
310
+
Added:
[
311
+
Added:
Ui.items_of ~id:"file-tree" (tree_row context.repo) entries; readme_panel;
312
+
Added:
]
313
+
Added:
314
+
Added:
let file ?(active = Layout.Files) context trail (blob : Resolvers.Blob.t) =
315
+
Added:
let filename =
316
+
Added:
match List.rev trail with (name, _) :: _ -> Some name | [] -> None
317
+
Added:
in
318
+
Added:
let language = Syntax.detect ~filename blob.content in
319
+
Added:
let raw_link =
320
+
Added:
match List.rev trail with
321
+
Added:
| (_, hash) :: _ ->
322
+
Added:
Ui.paragraph
323
+
Added:
[ Components.route_link (Raw_file (context.repo, hash)) "View raw" ]
324
+
Added:
| [] -> Ui.nothing
325
+
Added:
in
326
+
Added:
(* The README page reuses this view but reaches it without a path, so it has no
327
+
Added:
trail to show. *)
328
+
Added:
let toolbar =
329
+
Added:
match active with
330
+
Added:
| Layout.Readme -> []
331
+
Added:
| _ -> [ path_trail context.repo trail ]
332
+
Added:
in
333
+
Added:
page context ~active ~toolbar
334
+
Added:
[
335
+
Added:
raw_link;
336
+
Added:
Ui.code_listing ~id:"blob"
337
+
Added:
?class_:(Option.map (Printf.sprintf "language-%s") language)
338
+
Added:
blob.content;
339
+
Added:
]
340
+
Added:
341
+
Added:
let commit context (commit : Resolvers.Commit.t) diff =
342
+
Added:
let message = parse_commit_message commit.message in
343
+
Added:
let commit_type, title = parse_conventional message.summary in
344
+
Added:
let number = function Some n -> string_of_int n | None -> "" in
345
+
Added:
let diff_line (line : Resolvers.Diff.line) : Ui.Diff.line =
346
+
Added:
{
347
+
Added:
before = number line.old_number;
348
+
Added:
after = number line.new_number;
349
+
Added:
change =
350
+
Added:
(match line.kind with
351
+
Added:
| Resolvers.Diff.Context -> Ui.Diff.Unchanged
352
+
Added:
| Resolvers.Diff.Addition -> Ui.Diff.Added
353
+
Added:
| Resolvers.Diff.Deletion -> Ui.Diff.Removed);
354
+
Added:
content = line.text;
355
+
Added:
}
356
+
Added:
in
357
+
Added:
let diff_section (hunk : Resolvers.Diff.hunk) : Ui.Diff.section =
358
+
Added:
{
359
+
Added:
section_heading =
360
+
Added:
Printf.sprintf "@@ -%d,%d +%d,%d @@" hunk.old_start hunk.old_count
361
+
Added:
hunk.new_start hunk.new_count;
362
+
Added:
lines = List.map diff_line hunk.lines;
363
+
Added:
}
364
+
Added:
in
365
+
Added:
let mode = function
366
+
Added:
| None -> "000000"
367
+
Added:
| Some mode -> Printf.sprintf "%06o" mode
368
+
Added:
in
369
+
Added:
let hash = function
370
+
Added:
| None -> "00000000"
371
+
Added:
| Some hash -> Resolvers.short_hash hash
372
+
Added:
in
373
+
Added:
let diff_file (file : Resolvers.Diff.file) : Ui.Diff.file =
374
+
Added:
{
375
+
Added:
path = file.path;
376
+
Added:
detail =
377
+
Added:
Printf.sprintf "index %s..%s %s..%s" (hash file.old_hash)
378
+
Added:
(hash file.new_hash) (mode file.old_mode) (mode file.new_mode);
379
+
Added:
sections = List.map diff_section file.hunks;
380
+
Added:
note = (if file.binary then Some "Binary files differ" else None);
381
+
Added:
}
382
+
Added:
in
383
+
Added:
let badge =
384
+
Added:
match commit_type with
385
+
Added:
| None -> Ui.nothing
386
+
Added:
| Some commit_type ->
387
+
Added:
Components.commit_type_badge
388
+
Added:
~href:(commits_url ~filter_type:commit_type context.repo)
389
+
Added:
commit_type
390
+
Added:
in
391
+
Added:
let body =
392
+
Added:
if message.body = "" then []
393
+
Added:
else [ Ui.paragraph_text ~class_:"commit-body" message.body ]
394
+
Added:
in
395
+
Added:
let timestamp date =
396
+
Added:
let machine, display = Time_fmt.exact_time date in
397
+
Added:
Ui.timestamp ~machine display
398
+
Added:
in
399
+
Added:
let metadata =
400
+
Added:
Ui.definitions ~class_:"commit-meta"
401
+
Added:
[
402
+
Added:
("Commit", [ Ui.text commit.hash ]);
403
+
Added:
( "Author",
404
+
Added:
[ identity ~show_email:true ~role:`Author context.repo commit.author ]
405
+
Added:
);
406
+
Added:
("Author date", [ timestamp commit.author.date ]);
407
+
Added:
( "Committer",
408
+
Added:
[
409
+
Added:
identity ~show_email:true ~role:`Committer context.repo
410
+
Added:
commit.committer;
411
+
Added:
] );
412
+
Added:
("Committer date", [ timestamp commit.committer.date ]);
413
+
Added:
]
414
+
Added:
in
415
+
Added:
page
416
+
Added:
~heading:(context.repo ^ " : " ^ Resolvers.short_hash commit.hash)
417
+
Added:
context ~active:Commits
418
+
Added:
((Ui.heading ~level:3 [ badge; Ui.text (" " ^ title) ] :: body)
419
+
Added:
@ [ metadata ]
420
+
Added:
@ Ui.Diff.view ~empty_message:"No file changes in this commit."
421
+
Added:
(List.map diff_file diff))
422
+
Added:
423
+
Added:
let branches context branches =
424
+
Added:
page context ~active:Branches
425
+
Added:
(match branches with
426
+
Added:
| [] ->
427
+
Added:
[
428
+
Added:
Ui.paragraph_text
429
+
Added:
(Printf.sprintf "No branches for repo %s" context.repo);
430
+
Added:
]
431
+
Added:
| branches ->
432
+
Added:
[ Ui.items_of ~id:"branch-list" (branch_row context.repo) branches ])
433
+
Added:
434
+
Added:
let tags context tags =
435
+
Added:
page context ~active:Tags
436
+
Added:
(match tags with
437
+
Added:
| [] ->
438
+
Added:
[
439
+
Added:
Ui.paragraph_text (Printf.sprintf "No tags for repo %s" context.repo);
440
+
Added:
]
441
+
Added:
| tags -> [ Ui.items_of ~id:"tag-list" (tag_row context.repo) tags ])