Repositories / gitweb2.git
gitweb2.git
Clone (read-only): git clone http://git.guha-anderson.com/git/gitweb2.git
@@ -138,7 +138,8 @@ let page (title : string) (body : string) : string = h2 { margin: 0 0 10px; font-size: 20px; line-height: 1.25; overflow-wrap: anywhere; }\n\ .repo-list, .commits { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }\n\ .repo-list li, .commits li { border-top: 1px solid var(--line); } .repo-list li:first-child, .commits li:first-child { border-top: 0; }\n\ - .repo-list a, .commits li { display: grid; gap: 3px; padding: 14px 16px; }\n\ + .repo-list a, .commits a { display: grid; gap: 3px; padding: 14px 16px; color: var(--ink); }\n\ + .commits li:hover { background: var(--wash); }\n\ .repo-list strong, .commits strong { font-size: 16px; overflow-wrap: anywhere; } .repo-list small, .commits span { display: block; overflow-wrap: anywhere; }\n\ .panel { border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }\n\ table { width: 100%; border-collapse: collapse; } td { padding: 10px 12px; border-top: 1px solid var(--line); vertical-align: top; overflow-wrap: anywhere; }\n\ @@ -152,6 +153,20 @@ let page (title : string) (body : string) : string = .readme { margin-top: 24px; } .file-meta { padding: 10px 12px; border-bottom: 1px solid var(--line); background: #fff; }\n\ .notice { padding: 16px; border: 1px solid var(--line); border-radius: 8px; background: var(--wash); }\n\ .clone { margin: 6px 0 12px; color: var(--muted); } .clone code { background: var(--wash); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; font: 13px ui-monospace, SFMono-Regular, Consolas, monospace; color: var(--ink); user-select: all; }\n\ + .commit-meta { padding: 16px 18px; margin-bottom: 16px; background: #fff; }\n\ + .commit-meta h2 { margin: 0 0 8px; }\n\ + .commit-body { white-space: pre-wrap; background: var(--wash); border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px; margin: 10px 0 14px; font: 13px/1.5 ui-monospace, SFMono-Regular, Consolas, monospace; overflow: auto; }\n\ + .commit-fields { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0; font-size: 13px; }\n\ + .commit-fields dt { color: var(--muted); } .commit-fields dd { margin: 0; overflow-wrap: anywhere; }\n\ + .commit-fields code { font: 13px ui-monospace, SFMono-Regular, Consolas, monospace; }\n\ + .diff-file { margin-top: 14px; }\n\ + .diff-file .file-meta { font: 13px ui-monospace, SFMono-Regular, Consolas, monospace; }\n\ + pre.diff { margin: 0; padding: 8px 0; background: var(--wash); overflow: auto; font: 13px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre; }\n\ + pre.diff span { display: block; padding: 0 14px; min-height: 1.45em; }\n\ + pre.diff .diff-add { background: #e6ffec; color: #1a7f37; }\n\ + pre.diff .diff-del { background: #ffebe9; color: #cf222e; }\n\ + pre.diff .diff-hunk { background: #ddf4ff; color: #0550ae; }\n\ + pre.diff .diff-meta { color: var(--muted); }\n\ @media (max-width: 640px) { main { padding: 20px 12px 40px; } h1 { font-size: 23px; } .kind { width: 78px; } }\n\ </style>\n</head>\n<body><main>"; Buffer.add_string buffer body; @@ -206,6 +221,9 @@ let repo_url (repo : repo_info) (branch : string) (path : string) : string = let prefix = "/repo/" ^ url_encode repo.key ^ "/" ^ url_encode branch in if path = "" then prefix else prefix ^ "/" ^ (split_path path |> List.map url_encode |> String.concat "/") +let commit_url (repo : repo_info) (branch : string) (id : string) : string = + "/repo/" ^ url_encode repo.key ^ "/" ^ url_encode branch ^ "/-/commit/" ^ url_encode id + let repo_summary (repo : repo_info) : repo_summary = try Ocaml_git.with_repo repo.path @@ fun git -> @@ -304,6 +322,88 @@ let blob_view ?(clone_base : string option) ~(pygments_command : string) (repo : Buffer.add_string buffer "</div>"; Buffer.contents buffer +let starts_with (prefix : string) (s : string) : bool = + let lp = String.length prefix and ls = String.length s in + ls >= lp && String.sub s 0 lp = prefix + +(* Best-effort path extraction from "diff --git a/foo b/foo". Falls back to the + full line if the format is unexpected (e.g. paths containing spaces). *) +let diff_header_path (line : string) : string = + match String.split_on_char ' ' line with + | _ :: _ :: _ :: b :: _ when starts_with "b/" b -> String.sub b 2 (String.length b - 2) + | _ :: _ :: a :: _ :: _ when starts_with "a/" a -> String.sub a 2 (String.length a - 2) + | _ -> line + +let render_diff (text : string) : string = + let buffer = Buffer.create (String.length text + 1024) in + let in_file = ref false in + let close_file () = if !in_file then (Buffer.add_string buffer "</pre></div>"; in_file := false) in + let line_class line = + if line = "" then "diff-ctx" + else if starts_with "+++ " line || starts_with "--- " line then "diff-meta" + else if starts_with "index " line || starts_with "new file" line || starts_with "deleted file" line + || starts_with "similarity " line || starts_with "rename " line || starts_with "copy " line + || starts_with "old mode" line || starts_with "new mode" line + || starts_with "Binary files" line then "diff-meta" + else if starts_with "@@" line then "diff-hunk" + else if line.[0] = '+' then "diff-add" + else if line.[0] = '-' then "diff-del" + else "diff-ctx" + in + List.iter + (fun line -> + if starts_with "diff --git " line then ( + close_file (); + let path = diff_header_path line in + Printf.bprintf buffer + [%i {|<div class="diff-file panel"><div class="file-meta">{%s html path}</div><pre class="diff">|} ]; + in_file := true) + else if !in_file then + let cls = line_class line in + Printf.bprintf buffer [%i {|<span class="{%s cls}">{%s html line}</span> +|} ]) + (String.split_on_char '\n' text); + close_file (); + Buffer.contents buffer + +let format_signature_date (sig_ : Ocaml_git.signature) : string = + let offset = sig_.timezone_offset_minutes in + let local_seconds = sig_.epoch_seconds + (offset * 60) in + let tm = Unix.gmtime (float_of_int local_seconds) in + let sign = if offset >= 0 then "+" else "-" in + let abs_offset = abs offset in + Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d %s%02d%02d" (tm.Unix.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday + tm.tm_hour tm.tm_min tm.tm_sec sign (abs_offset / 60) (abs_offset mod 60) + +let commit_view ?(clone_base : string option) (repo : repo_info) (branch : string) (commit_id : string) : string = + Ocaml_git.with_repo repo.path @@ fun git -> + let branches = local_branches git in + let buffer = Buffer.create 8192 in + Buffer.add_string buffer (repo_header ?clone_base repo branch "" branches "history"); + (match + try Some (Ocaml_git.commit_lookup git commit_id) with Ocaml_git.Git_error _ -> None + with + | None -> Printf.bprintf buffer [%i {|<p class="notice">Unknown commit {%s html commit_id}</p>|} ] + | Some commit -> + let subject = commit.summary in + (* The full message starts with the subject; strip it to get just the body. *) + let body = + let msg = String.trim commit.message in + if subject <> "" && starts_with subject msg then + String.trim (String.sub msg (String.length subject) (String.length msg - String.length subject)) + else msg + in + let author = commit.author.name in + let author_email = commit.author.email in + let date = format_signature_date commit.author in + Printf.bprintf buffer [%i {|<section class="commit-meta panel"><h2>{%s html subject}</h2>|} ]; + if body <> "" then Printf.bprintf buffer [%i {|<pre class="commit-body">{%s html body}</pre>|} ]; + Printf.bprintf buffer + [%i + {|<dl class="commit-fields"><dt>Author</dt><dd>{%s html author} <{%s html author_email}></dd><dt>Date</dt><dd>{%s html date}</dd><dt>Commit</dt><dd><code>{%s html commit.id}</code></dd></dl></section>|} ]; + Buffer.add_string buffer (render_diff (Ocaml_git.commit_diff git commit_id))); + Buffer.contents buffer + let commits_page ?(clone_base : string option) (repo : repo_info) (branch : string) : string = Ocaml_git.with_repo repo.path @@ fun git -> let buffer = Buffer.create 4096 in @@ -311,7 +411,9 @@ let commits_page ?(clone_base : string option) (repo : repo_info) (branch : stri Printf.bprintf buffer [%i {|<ol class="commits">|} ]; List.iter (fun (commit : Ocaml_git.commit) -> - Printf.bprintf buffer [%i {|<li><strong>{%s html commit.summary}</strong><span>{%s html (String.sub commit.id 0 (min 12 (String.length commit.id)))} · {%s html commit.author.name}</span></li>|} ]) + let short = String.sub commit.id 0 (min 12 (String.length commit.id)) in + let href = commit_url repo branch commit.id in + Printf.bprintf buffer [%i {|<li><a href="{%s href}"><strong>{%s html commit.summary}</strong><span>{%s html short} · {%s html commit.author.name}</span></a></li>|} ]) (Ocaml_git.commits ~limit:100 git branch); Printf.bprintf buffer [%i {|</ol>|} ]; page (branch ^ " history") (Buffer.contents buffer) @@ -376,6 +478,11 @@ let route ?(pygments_command : string = default_pygments_command) ?(clone_base : html_response 200 (repo_page ?clone_base ~pygments_command repo branch "") | branch :: "-" :: "commits" :: [] -> html_response 200 (commits_page ?clone_base repo (url_decode branch)) + | branch :: "-" :: "commit" :: id :: [] -> + let branch = url_decode branch in + let id = url_decode id in + let title = "commit " ^ String.sub id 0 (min 12 (String.length id)) in + html_response 200 (page title (commit_view ?clone_base repo branch id)) | branch :: path_parts -> let path = path_parts |> List.map url_decode |> String.concat "/" in html_response 200 (repo_page ?clone_base ~pygments_command repo (url_decode branch) path)))