aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.github/workflows/build.yml11+6 −5
-rw-r--r--Cargo.toml2+1 −1
-rw-r--r--Dockerfile2+1 −1
-rw-r--r--cgit/cgit.c17+1 −16
-rw-r--r--cgit/cgit.h45+0 −45
-rw-r--r--cgit/cmd.c51+0 −51
-rw-r--r--cgit/filter.c57+0 −57
-rw-r--r--cgit/shared.c81+0 −81
-rw-r--r--cgit/ui-commit.c144+0 −144
-rw-r--r--cgit/ui-commit.h10+0 −10
-rw-r--r--cgit/ui-diff.c507+0 −507
-rw-r--r--cgit/ui-diff.h19+0 −19
-rw-r--r--cgit/ui-patch.c104+0 −104
-rw-r--r--cgit/ui-patch.h11+0 −11
-rw-r--r--cgit/ui-shared.c332+7 −325
-rw-r--r--cgit/ui-shared.h25+0 −25
-rw-r--r--cgit/ui-snapshot.c326+0 −326
-rw-r--r--cgit/ui-snapshot.h11+0 −11
-rw-r--r--cgit/ui-ssdiff.c426+0 −426
-rw-r--r--cgit/ui-ssdiff.h29+0 −29
-rw-r--r--cgit/ui-stats.c439+0 −439
-rw-r--r--cgit/ui-stats.h32+0 −32
-rw-r--r--crates/gilti/Cargo.toml1+1 −0
-rw-r--r--crates/gilti/src/lfs.rs9+4 −5
-rw-r--r--crates/gilti/src/main.rs153+69 −84
-rw-r--r--crates/gilti/src/models/archive.rs41+41 −0
-rw-r--r--crates/gilti/src/models/commit.rs151+151 −0
-rw-r--r--crates/gilti/src/models/diff.rs159+159 −0
-rw-r--r--crates/gilti/src/models/mod.rs89+89 −0
-rw-r--r--crates/gilti/src/models/overview.rs22+11 −11
-rw-r--r--crates/gilti/src/models/patch.rs28+28 −0
-rw-r--r--crates/gilti/src/models/repository.rs12+12 −0
-rw-r--r--crates/gilti/src/models/stats.rs191+191 −0
-rw-r--r--crates/gilti/src/views/archive.rs188+188 −0
-rw-r--r--crates/gilti/src/views/diff.rs456+456 −0
-rw-r--r--crates/gilti/src/views/mod.rs39+39 −0
-rw-r--r--crates/gilti/src/views/patch.rs63+63 −0
-rw-r--r--crates/gilti/src/views/revision.rs122+122 −0
-rw-r--r--crates/gilti/src/views/shared.rs26+24 −2
-rw-r--r--crates/gilti/src/views/stats.rs122+122 −0
-rw-r--r--crates/gilti/src/views/tree.rs2+1 −1
-rwxr-xr-xtests/smoke.sh18+17 −1
42 files changed, 1804 insertions, 2769 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9253edd..31fe9c8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -70,22 +70,23 @@ jobs:
- name: Build container artifacts
env:
- ALPINE_IMAGE: docker.io/library/alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1
+ RUST_IMAGE: docker.io/library/rust:1.97.1-alpine3.22@sha256:df4efa4e0cdfb5245fa06e3f431387b2bcc96782ce5681b7fb6b0297d745bc29
run: |
docker run --rm \
--volume "$PWD:/src" \
--workdir /src \
--env "HOST_UID=$(id -u)" \
--env "HOST_GID=$(id -g)" \
- "$ALPINE_IMAGE" sh -euxc '
+ "$RUST_IMAGE" sh -euxc '
+ rustc --version | grep -qx "rustc 1.97.1 (8bab26f4f 2026-07-14)"
+ cargo --version | grep -q "^cargo 1.97.1 "
apk add --no-cache \
build-base=0.5-r3 \
- cargo=1.87.0-r1 \
cmake \
- rust=1.87.0-r1 \
git=2.49.1-r0 \
python3 \
- zlib-dev=1.3.2-r0
+ zlib-dev=1.3.2-r0 \
+ zlib-static=1.3.2-r0
scripts/build-artifacts.sh "${{ matrix.arch }}"
chown -R "$HOST_UID:$HOST_GID" .container
'
diff --git a/Cargo.toml b/Cargo.toml
index 9cedeae..2f300b2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ resolver = "3"
[workspace.package]
publish = false
edition = "2024"
-rust-version = "1.87"
+rust-version = "1.97"
version = "0.1.0"
license = "AGPL-3.0-or-later"
authors = ["Nikolay Govorov <me@govorov.online>"]
diff --git a/Dockerfile b/Dockerfile
index 9db5a5b..7dd66ba 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -28,7 +28,7 @@ RUN apk add --no-cache \
openssh-server=10.0_p1-r10 \
su-exec=0.2-r3 \
tini=0.19.0-r3 \
- xz=5.8.1-r0 \
+ xz=5.8.3-r0 \
zstd=1.5.7-r0 && \
addgroup -S -g 10000 git && \
install -d -m 0755 -o root -g root /var/lib/gilti && \
diff --git a/cgit/cgit.c b/cgit/cgit.c
index 344ff55..0ef5015 100644
--- a/cgit/cgit.c
+++ b/cgit/cgit.c
@@ -100,7 +100,6 @@ static void prepare_context(void)
ctx.cfg.enable_log_filecount = config_integer("CGIT_ENABLE_LOG_FILECOUNT");
ctx.cfg.enable_log_linecount = config_integer("CGIT_ENABLE_LOG_LINECOUNT");
ctx.cfg.enable_remote_branches = config_integer("CGIT_ENABLE_REMOTE_BRANCHES");
- ctx.cfg.enable_subject_links = config_integer("CGIT_ENABLE_SUBJECT_LINKS");
ctx.cfg.favicon = config_string("CGIT_FAVICON");
ctx.cfg.footer = config_optional_string("CGIT_FOOTER");
ctx.cfg.head_include = config_optional_string("CGIT_HEAD_INCLUDE");
@@ -111,9 +110,6 @@ static void prepare_context(void)
ctx.cfg.max_atom_items = config_integer("CGIT_MAX_ATOM_ITEMS");
ctx.cfg.max_commit_count = config_integer("CGIT_MAX_COMMIT_COUNT");
ctx.cfg.max_msg_len = config_integer("CGIT_MAX_MESSAGE_LENGTH");
- ctx.cfg.max_stats = config_integer("CGIT_MAX_STATS");
- ctx.cfg.mimetype_file = config_optional_string("CGIT_MIMETYPE_FILE");
- ctx.cfg.module_link = config_optional_string("CGIT_MODULE_LINK");
ctx.cfg.noheader = config_integer("CGIT_NOHEADER");
ctx.cfg.noplainemail = config_integer("CGIT_NOPLAINEMAIL");
cgit_default_repo_desc = config_string("CGIT_REPO_DEFAULT_DESC");
@@ -123,11 +119,9 @@ static void prepare_context(void)
ctx.cfg.root_title = config_string("CGIT_ROOT_TITLE");
ctx.cfg.script_name = config_string("SCRIPT_NAME");
ctx.cfg.section = config_string("CGIT_SECTION");
- ctx.cfg.snapshots = config_integer("CGIT_SNAPSHOTS");
ctx.cfg.virtual_root = ensure_end(config_value("CGIT_VIRTUAL_ROOT"), '/');
string_list_init_dup(&ctx.cfg.css);
string_list_init_dup(&ctx.cfg.js);
- string_list_init_dup(&ctx.cfg.mimetypes);
string_list_init_dup(&ctx.cfg.readme);
value = config_value("CGIT_CSS");
if (*value)
@@ -164,23 +158,15 @@ static void prepare_request(void)
ctx.qry.oid = xstrdup(ctx.qry.head);
ctx.qry.has_oid = 1;
}
- ctx.qry.oid2 = request_optional_string("GILTI_OLD_REVISION");
ctx.qry.path = request_optional_string("GILTI_PATH");
- ctx.qry.format = request_optional_string("GILTI_FORMAT");
- ctx.qry.signature = request_optional_integer("GILTI_SIGNATURE");
ctx.qry.search = request_optional_string("GILTI_QUERY_SEARCH");
ctx.qry.grep = request_optional_string("GILTI_QUERY_GREP");
- ctx.qry.sort = request_optional_string("GILTI_QUERY_SORT");
- ctx.qry.period = request_optional_string("GILTI_QUERY_PERIOD");
ctx.qry.ofs = request_optional_integer("GILTI_QUERY_OFFSET");
ctx.qry.showmsg = request_optional_integer("GILTI_QUERY_SHOWMSG");
ctx.qry.context = request_optional_integer("GILTI_QUERY_CONTEXT");
ctx.qry.ignorews = request_optional_integer("GILTI_QUERY_IGNOREWS");
ctx.qry.follow = request_optional_integer("GILTI_QUERY_FOLLOW");
- if (getenv("GILTI_QUERY_DIFFTYPE")) {
- ctx.qry.difftype = request_optional_integer("GILTI_QUERY_DIFFTYPE");
- ctx.qry.has_difftype = 1;
- }
+ ctx.qry.difftype = request_optional_integer("GILTI_QUERY_DIFFTYPE");
}
static void prepare_repository(void)
@@ -352,7 +338,6 @@ static int prepare_repo_cmd(int nongit)
"Invalid revision: %s", ctx.qry.head);
return 1;
}
- string_list_sort(&ctx.repo->submodules);
cgit_prepare_repo_env(ctx.repo);
choose_readme();
return 0;
diff --git a/cgit/cgit.h b/cgit/cgit.h
index fedec59..eb0ed9b 100644
--- a/cgit/cgit.h
+++ b/cgit/cgit.h
@@ -66,13 +66,6 @@ typedef enum {
DIFF_UNIFIED, DIFF_SSDIFF, DIFF_STATONLY
} diff_type;
-struct cgit_exec_filter {
- char *cmd;
- char **argv;
- int old_stdout;
- int pid;
-};
-
struct cgit_repo {
char *url;
char *name;
@@ -82,24 +75,18 @@ struct cgit_repo {
char *owner;
char *homepage;
char *defbranch;
- char *module_link;
struct string_list readme;
char *section;
char *clone_url;
char *logo;
char *logo_link;
- char *snapshot_prefix;
- int snapshots;
int enable_commit_graph;
int enable_follow_links;
int enable_log_filecount;
int enable_log_linecount;
int enable_remote_branches;
- int enable_subject_links;
- int max_stats;
int commit_sort;
time_t mtime;
- struct string_list submodules;
int hide;
int ignore;
};
@@ -144,21 +131,15 @@ struct reflist {
struct cgit_query {
int has_oid;
- int has_difftype;
char *repo;
char *page;
char *search;
char *grep;
char *head;
char *oid;
- char *oid2;
char *path;
char *url;
- char *period;
- char *format;
- int signature;
int ofs;
- char *sort;
int showmsg;
diff_type difftype;
int show_all;
@@ -177,8 +158,6 @@ struct cgit_config {
char *header;
char *logo;
char *logo_link;
- char *mimetype_file;
- char *module_link;
struct string_list readme;
struct string_list css;
char *robots;
@@ -194,19 +173,15 @@ struct cgit_config {
int enable_log_filecount;
int enable_log_linecount;
int enable_remote_branches;
- int enable_subject_links;
int local_time;
int max_atom_items;
int max_commit_count;
int max_msg_len;
- int max_stats;
int noplainemail;
int noheader;
int renamelimit;
- int snapshots;
diff_type difftype;
int commit_sort;
- struct string_list mimetypes;
struct string_list js;
};
@@ -240,18 +215,9 @@ struct cgit_context {
struct cgit_page page;
};
-typedef int (*write_archive_fn_t)(const char *, const char *);
-
-struct cgit_snapshot_format {
- const char *suffix;
- const char *mimetype;
- write_archive_fn_t write_func;
-};
-
extern const char *cgit_version;
extern struct cgit_context ctx;
-extern const struct cgit_snapshot_format cgit_snapshot_formats[];
extern char *cgit_default_repo_desc;
extern struct cgit_repo *cgit_add_repo(const char *url);
@@ -298,15 +264,6 @@ extern struct commitinfo *cgit_parse_commit(struct commit *commit);
extern struct taginfo *cgit_parse_tag(struct tag *tag);
extern const char *cgit_repobasename(const char *reponame);
-extern int cgit_parse_snapshots_mask(const char *str);
-extern const struct object_id *cgit_snapshot_get_sig(const char *ref,
- const struct cgit_snapshot_format *f);
-extern const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f);
-
-extern void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv);
-extern int cgit_open_exec_filter(struct cgit_exec_filter *filter);
-extern int cgit_close_exec_filter(struct cgit_exec_filter *filter);
-
extern void cgit_prepare_repo_env(struct cgit_repo * repo);
extern int read_first_line(const char *path, char **buf, size_t *size);
@@ -315,6 +272,4 @@ extern char *strdup_first_line(const char *txt);
extern char *expand_macros(const char *txt);
-extern char *get_mimetype_for_filename(const char *filename);
-
#endif /* CGIT_H */
diff --git a/cgit/cmd.c b/cgit/cmd.c
index 024b115..5b32399 100644
--- a/cgit/cmd.c
+++ b/cgit/cmd.c
@@ -13,35 +13,14 @@
#include "cgit.h"
#include "cmd.h"
-#include "ui-shared.h"
#include "ui-atom.h"
-#include "ui-commit.h"
-#include "ui-diff.h"
#include "ui-log.h"
-#include "ui-patch.h"
-#include "ui-snapshot.h"
-#include "ui-stats.h"
static void atom_fn(void)
{
cgit_print_atom(ctx.qry.head, ctx.qry.path, ctx.cfg.max_atom_items);
}
-static void revision_fn(void)
-{
- cgit_print_commit(ctx.qry.oid, NULL);
-}
-
-static void diff_fn(void)
-{
- cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 0);
-}
-
-static void rawdiff_fn(void)
-{
- cgit_print_diff(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path, 1, 1);
-}
-
static void log_fn(void)
{
cgit_print_log(ctx.qry.oid, ctx.qry.ofs, ctx.cfg.max_commit_count,
@@ -50,30 +29,6 @@ static void log_fn(void)
ctx.repo->commit_sort);
}
-static void patch_fn(void)
-{
- cgit_print_patch(ctx.qry.oid, ctx.qry.oid2, ctx.qry.path);
-}
-
-static void snapshot_fn(void)
-{
- char *filename;
-
- if (!ctx.qry.format) {
- cgit_print_error_page(400, "Bad request", "Archive format is required");
- return;
- }
- filename = fmtalloc("%s.%s%s", cgit_snapshot_prefix(ctx.repo),
- ctx.qry.format, ctx.qry.signature ? ".asc" : "");
- cgit_print_snapshot(ctx.qry.head, ctx.qry.oid, filename, 0);
- free(filename);
-}
-
-static void stats_fn(void)
-{
- cgit_show_stats();
-}
-
#define def_cmd(name, want_repo, want_vpath) \
{#name, name##_fn, want_repo, want_vpath}
@@ -81,13 +36,7 @@ struct cgit_cmd *cgit_get_cmd(void)
{
static struct cgit_cmd cmds[] = {
def_cmd(atom, 1, 0),
- def_cmd(diff, 1, 1),
def_cmd(log, 1, 1),
- def_cmd(patch, 1, 1),
- def_cmd(rawdiff, 1, 1),
- def_cmd(revision, 1, 0),
- def_cmd(snapshot, 1, 0),
- def_cmd(stats, 1, 1),
};
int i;
diff --git a/cgit/filter.c b/cgit/filter.c
deleted file mode 100644
--- a/cgit/filter.c
+++ /dev/null
@@ -1,57 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* filter.c: executable filter used by snapshot compression
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#include "cgit.h"
-
-void cgit_exec_filter_init(struct cgit_exec_filter *filter, char *cmd, char **argv)
-{
- memset(filter, 0, sizeof(*filter));
- filter->cmd = cmd;
- filter->argv = argv;
-}
-
-int cgit_open_exec_filter(struct cgit_exec_filter *filter)
-{
- int pipe_fh[2];
-
- filter->old_stdout = chk_positive(dup(STDOUT_FILENO),
- "Unable to duplicate STDOUT");
- chk_zero(pipe(pipe_fh), "Unable to create pipe to subprocess");
- filter->pid = chk_non_negative(fork(), "Unable to create subprocess");
- if (filter->pid == 0) {
- close(pipe_fh[1]);
- chk_non_negative(dup2(pipe_fh[0], STDIN_FILENO),
- "Unable to use pipe as STDIN");
- execvp(filter->cmd, filter->argv);
- die_errno("Unable to exec subprocess %s", filter->cmd);
- }
- close(pipe_fh[0]);
- chk_non_negative(dup2(pipe_fh[1], STDOUT_FILENO),
- "Unable to use pipe as STDOUT");
- close(pipe_fh[1]);
- return 0;
-}
-
-int cgit_close_exec_filter(struct cgit_exec_filter *filter)
-{
- int exit_status = 0;
-
- chk_non_negative(dup2(filter->old_stdout, STDOUT_FILENO),
- "Unable to restore STDOUT");
- close(filter->old_stdout);
- if (filter->pid < 0)
- return WEXITSTATUS(exit_status);
- waitpid(filter->pid, &exit_status, 0);
- if (!WIFEXITED(exit_status))
- die("Subprocess %s exited abnormally", filter->cmd);
- return WEXITSTATUS(exit_status);
-}
diff --git a/cgit/shared.c b/cgit/shared.c
index dd86e24..8a7fb99 100644
--- a/cgit/shared.c
+++ b/cgit/shared.c
@@ -51,20 +51,15 @@ struct cgit_repo *cgit_add_repo(const char *url)
ret->owner = NULL;
ret->homepage = NULL;
ret->section = ctx.cfg.section;
- ret->snapshots = ctx.cfg.snapshots;
ret->enable_commit_graph = ctx.cfg.enable_commit_graph;
ret->enable_follow_links = ctx.cfg.enable_follow_links;
ret->enable_log_filecount = ctx.cfg.enable_log_filecount;
ret->enable_log_linecount = ctx.cfg.enable_log_linecount;
ret->enable_remote_branches = ctx.cfg.enable_remote_branches;
- ret->enable_subject_links = ctx.cfg.enable_subject_links;
- ret->max_stats = ctx.cfg.max_stats;
ret->commit_sort = ctx.cfg.commit_sort;
- ret->module_link = ctx.cfg.module_link;
ret->readme = ctx.cfg.readme;
ret->mtime = -1;
ret->clone_url = ctx.cfg.clone_url;
- ret->submodules.strdup_strings = 1;
ret->hide = ret->ignore = 0;
return ret;
}
@@ -350,37 +345,6 @@ void cgit_diff_commit(struct commit *commit, filepair_fn fn, const char *prefix)
ctx.qry.ignorews);
}
-int cgit_parse_snapshots_mask(const char *str)
-{
- struct string_list tokens = STRING_LIST_INIT_DUP;
- struct string_list_item *item;
- const struct cgit_snapshot_format *f;
- int rv = 0;
-
- /* favor legacy setting */
- if (atoi(str))
- return 1;
-
- if (strcmp(str, "all") == 0)
- return INT_MAX;
-
- string_list_split(&tokens, str, " ", -1);
- string_list_remove_empty_items(&tokens, 0);
-
- for_each_string_list_item(item, &tokens) {
- for (f = cgit_snapshot_formats; f->suffix; f++) {
- if (!strcmp(item->string, f->suffix) ||
- !strcmp(item->string, f->suffix + 1)) {
- rv |= cgit_snapshot_format_bit(f);
- break;
- }
- }
- }
-
- string_list_clear(&tokens, 0);
- return rv;
-}
-
typedef struct {
char * name;
char * value;
@@ -516,48 +480,3 @@ char *expand_macros(const char *txt)
}
return result;
}
-
-char *get_mimetype_for_filename(const char *filename)
-{
- const char *ext;
- char *mimetype, line[1024];
- struct string_list list = STRING_LIST_INIT_NODUP;
- int i;
- FILE *file;
- struct string_list_item *mime;
-
- if (!filename)
- return NULL;
-
- ext = strrchr(filename, '.');
- if (!ext)
- return NULL;
- ++ext;
- if (!ext[0])
- return NULL;
- mime = string_list_lookup(&ctx.cfg.mimetypes, ext);
- if (mime)
- return xstrdup(mime->util);
-
- if (!ctx.cfg.mimetype_file)
- return NULL;
- file = fopen(ctx.cfg.mimetype_file, "r");
- if (!file)
- return NULL;
- while (fgets(line, sizeof(line), file)) {
- if (!line[0] || line[0] == '#')
- continue;
- string_list_split_in_place(&list, line, " \t\r\n", -1);
- string_list_remove_empty_items(&list, 0);
- mimetype = list.items[0].string;
- for (i = 1; i < list.nr; i++) {
- if (!strcasecmp(ext, list.items[i].string)) {
- fclose(file);
- return xstrdup(mimetype);
- }
- }
- string_list_clear(&list, 0);
- }
- fclose(file);
- return NULL;
-}
diff --git a/cgit/ui-commit.c b/cgit/ui-commit.c
deleted file mode 100644
--- a/cgit/ui-commit.c
+++ /dev/null
@@ -1,144 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* ui-commit.c: generate commit view
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#define USE_THE_REPOSITORY_VARIABLE
-
-#include "cgit.h"
-#include "ui-commit.h"
-#include "html.h"
-#include "ui-shared.h"
-#include "ui-diff.h"
-#include "ui-log.h"
-
-void cgit_print_commit(char *hex, const char *prefix)
-{
- struct commit *commit, *parent;
- struct commitinfo *info, *parent_info;
- struct commit_list *p;
- struct strbuf notes = STRBUF_INIT;
- struct object_id oid;
- char *tmp, *tmp2;
- int parents = 0;
-
- if (!hex)
- hex = ctx.qry.head;
-
- if (repo_get_oid(the_repository, hex, &oid)) {
- cgit_print_error_page(400, "Bad request",
- "Bad object id: %s", hex);
- return;
- }
- commit = lookup_commit_reference(the_repository, &oid);
- if (!commit) {
- cgit_print_error_page(404, "Not found",
- "Bad commit reference: %s", hex);
- return;
- }
- info = cgit_parse_commit(commit);
-
- format_display_notes(&oid, &notes, PAGE_ENCODING, 1);
-
- load_ref_decorations(NULL, DECORATE_FULL_REFS);
-
- ctx.page.title = fmtalloc("%s - %s", info->subject, ctx.page.title);
- cgit_print_layout_start();
- cgit_print_diff_ctrls();
- html("<table summary='commit info' class='commit-info'>\n");
- html("<tr><th>author</th><td>");
- html_txt(info->author);
- if (!ctx.cfg.noplainemail) {
- html(" ");
- html_txt(info->author_email);
- }
- html("</td><td class='right'>");
- html_txt(show_date(info->author_date, info->author_tz,
- cgit_date_mode(DATE_ISO8601)));
- html("</td></tr>\n");
- html("<tr><th>committer</th><td>");
- html_txt(info->committer);
- if (!ctx.cfg.noplainemail) {
- html(" ");
- html_txt(info->committer_email);
- }
- html("</td><td class='right'>");
- html_txt(show_date(info->committer_date, info->committer_tz,
- cgit_date_mode(DATE_ISO8601)));
- html("</td></tr>\n");
- html("<tr><th>commit</th><td colspan='2' class='oid'>");
- tmp = oid_to_hex(&commit->object.oid);
- cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, prefix);
- html(" (");
- cgit_patch_link("patch", NULL, NULL, NULL, tmp, prefix);
- html(")</td></tr>\n");
- html("<tr><th>tree</th><td colspan='2' class='oid'>");
- tmp = xstrdup(hex);
- cgit_tree_link(oid_to_hex(get_commit_tree_oid(commit)), NULL, NULL,
- ctx.qry.head, tmp, NULL);
- if (prefix) {
- html(" /");
- cgit_tree_link(prefix, NULL, NULL, ctx.qry.head, tmp, prefix);
- }
- free(tmp);
- html("</td></tr>\n");
- for (p = commit->parents; p; p = p->next) {
- parent = lookup_commit_reference(the_repository, &p->item->object.oid);
- if (!parent) {
- html("<tr><td colspan='3'>");
- cgit_print_error("Error reading parent commit");
- html("</td></tr>");
- continue;
- }
- html("<tr><th>parent</th>"
- "<td colspan='2' class='oid'>");
- tmp = tmp2 = oid_to_hex(&p->item->object.oid);
- if (ctx.repo->enable_subject_links) {
- parent_info = cgit_parse_commit(parent);
- tmp2 = parent_info->subject;
- }
- cgit_commit_link(tmp2, NULL, NULL, ctx.qry.head, tmp, prefix);
- html(" (");
- cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex,
- oid_to_hex(&p->item->object.oid), prefix);
- html(")</td></tr>");
- parents++;
- }
- if (ctx.repo->snapshots) {
- html("<tr><th>download</th><td colspan='2' class='oid'>");
- cgit_print_snapshot_links(ctx.repo, hex, "<br/>");
- html("</td></tr>");
- }
- html("</table>\n");
- html("<div class='commit-subject'>");
- html_txt(info->subject);
- show_commit_decorations(commit);
- html("</div>");
- html("<div class='commit-msg'>");
- html_txt(info->msg);
- html("</div>");
- if (notes.len != 0) {
- html("<div class='notes-header'>Notes</div>");
- html("<div class='notes'>");
- html_txt(notes.buf);
- html("</div>");
- html("<div class='notes-footer'></div>");
- }
- if (parents < 3) {
- if (parents)
- tmp = oid_to_hex(&commit->parents->item->object.oid);
- else
- tmp = NULL;
- cgit_print_diff(ctx.qry.oid, tmp, prefix, 0, 0);
- }
- strbuf_release(&notes);
- cgit_free_commitinfo(info);
- cgit_print_layout_end();
-}
diff --git a/cgit/ui-commit.h b/cgit/ui-commit.h
deleted file mode 100644
--- a/cgit/ui-commit.h
+++ /dev/null
@@ -1,10 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_COMMIT_H
-#define UI_COMMIT_H
-
-extern void cgit_print_commit(char *hex, const char *prefix);
-
-#endif /* UI_COMMIT_H */
diff --git a/cgit/ui-diff.c b/cgit/ui-diff.c
deleted file mode 100644
--- a/cgit/ui-diff.c
+++ /dev/null
@@ -1,507 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* ui-diff.c: show diff between two blobs
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#define USE_THE_REPOSITORY_VARIABLE
-
-#include "cgit.h"
-#include "ui-diff.h"
-#include "html.h"
-#include "ui-shared.h"
-#include "ui-ssdiff.h"
-
-struct object_id old_rev_oid[1];
-struct object_id new_rev_oid[1];
-
-static int files, slots;
-static int total_adds, total_rems, max_changes;
-static int lines_added, lines_removed;
-
-static struct fileinfo {
- char status;
- struct object_id old_oid[1];
- struct object_id new_oid[1];
- unsigned short old_mode;
- unsigned short new_mode;
- char *old_path;
- char *new_path;
- unsigned int added;
- unsigned int removed;
- unsigned long old_size;
- unsigned long new_size;
- unsigned int binary:1;
-} *items;
-
-static int use_ssdiff = 0;
-static struct diff_filepair *current_filepair;
-static const char *current_prefix;
-
-struct diff_filespec *cgit_get_current_old_file(void)
-{
- return current_filepair->one;
-}
-
-struct diff_filespec *cgit_get_current_new_file(void)
-{
- return current_filepair->two;
-}
-
-static void print_fileinfo(struct fileinfo *info)
-{
- char *class;
-
- switch (info->status) {
- case DIFF_STATUS_ADDED:
- class = "add";
- break;
- case DIFF_STATUS_COPIED:
- class = "cpy";
- break;
- case DIFF_STATUS_DELETED:
- class = "del";
- break;
- case DIFF_STATUS_MODIFIED:
- class = "upd";
- break;
- case DIFF_STATUS_RENAMED:
- class = "mov";
- break;
- case DIFF_STATUS_TYPE_CHANGED:
- class = "typ";
- break;
- case DIFF_STATUS_UNKNOWN:
- class = "unk";
- break;
- case DIFF_STATUS_UNMERGED:
- class = "stg";
- break;
- default:
- die("bug: unhandled diff status %c", info->status);
- }
-
- html("<tr>");
- html("<td class='mode'>");
- if (is_null_oid(info->new_oid)) {
- cgit_print_filemode(info->old_mode);
- } else {
- cgit_print_filemode(info->new_mode);
- }
-
- if (info->old_mode != info->new_mode &&
- !is_null_oid(info->old_oid) &&
- !is_null_oid(info->new_oid)) {
- html("<span class='modechange'>[");
- cgit_print_filemode(info->old_mode);
- html("]</span>");
- }
- htmlf("</td><td class='%s'>", class);
- cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.oid,
- ctx.qry.oid2, info->new_path);
- if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) {
- htmlf(" (%s from ",
- info->status == DIFF_STATUS_COPIED ? "copied" : "renamed");
- html_txt(info->old_path);
- html(")");
- }
- html("</td><td class='right'>");
- if (info->binary) {
- htmlf("bin</td><td class='graph'>%ld -> %ld bytes",
- info->old_size, info->new_size);
- return;
- }
- htmlf("%d", info->added + info->removed);
- html("</td><td class='graph'>");
- htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
- htmlf("<td class='add' style='width: %.1f%%;'/>",
- info->added * 100.0 / max_changes);
- htmlf("<td class='rem' style='width: %.1f%%;'/>",
- info->removed * 100.0 / max_changes);
- htmlf("<td class='none' style='width: %.1f%%;'/>",
- (max_changes - info->removed - info->added) * 100.0 / max_changes);
- html("</tr></table></td></tr>\n");
-}
-
-static void count_diff_lines(char *line, int len)
-{
- if (line && (len > 0)) {
- if (line[0] == '+')
- lines_added++;
- else if (line[0] == '-')
- lines_removed++;
- }
-}
-
-static int show_filepair(struct diff_filepair *pair)
-{
- /* Always show if we have no limiting prefix. */
- if (!current_prefix)
- return 1;
-
- /* Show if either path in the pair begins with the prefix. */
- if (starts_with(pair->one->path, current_prefix) ||
- starts_with(pair->two->path, current_prefix))
- return 1;
-
- /* Otherwise we don't want to show this filepair. */
- return 0;
-}
-
-static void inspect_filepair(struct diff_filepair *pair)
-{
- int binary = 0;
- unsigned long old_size = 0;
- unsigned long new_size = 0;
-
- if (!show_filepair(pair))
- return;
-
- files++;
- lines_added = 0;
- lines_removed = 0;
- cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size, &new_size,
- &binary, 0, ctx.qry.ignorews, count_diff_lines);
- if (files >= slots) {
- if (slots == 0)
- slots = 4;
- else
- slots = slots * 2;
- items = xrealloc(items, slots * sizeof(struct fileinfo));
- }
- items[files-1].status = pair->status;
- oidcpy(items[files-1].old_oid, &pair->one->oid);
- oidcpy(items[files-1].new_oid, &pair->two->oid);
- items[files-1].old_mode = pair->one->mode;
- items[files-1].new_mode = pair->two->mode;
- items[files-1].old_path = xstrdup(pair->one->path);
- items[files-1].new_path = xstrdup(pair->two->path);
- items[files-1].added = lines_added;
- items[files-1].removed = lines_removed;
- items[files-1].old_size = old_size;
- items[files-1].new_size = new_size;
- items[files-1].binary = binary;
- if (lines_added + lines_removed > max_changes)
- max_changes = lines_added + lines_removed;
- total_adds += lines_added;
- total_rems += lines_removed;
-}
-
-static void cgit_print_diffstat(const struct object_id *old_oid,
- const struct object_id *new_oid,
- const char *prefix)
-{
- int i;
-
- html("<div class='diffstat-header'>");
- cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.oid,
- ctx.qry.oid2, NULL);
- if (prefix) {
- html(" (limited to '");
- html_txt(prefix);
- html("')");
- }
- html("</div>");
- html("<table summary='diffstat' class='diffstat'>");
- max_changes = 0;
- cgit_diff_tree(old_oid, new_oid, inspect_filepair, prefix,
- ctx.qry.ignorews);
- for (i = 0; i<files; i++)
- print_fileinfo(&items[i]);
- html("</table>");
- html("<div class='diffstat-summary'>");
- htmlf("%d files changed, %d insertions, %d deletions",
- files, total_adds, total_rems);
- html("</div>");
-}
-
-
-/*
- * print a single line returned from xdiff
- */
-static void print_line(char *line, int len)
-{
- char *class = "ctx";
- char c = line[len-1];
-
- if (line[0] == '+')
- class = "add";
- else if (line[0] == '-')
- class = "del";
- else if (line[0] == '@')
- class = "hunk";
-
- htmlf("<div class='%s'>", class);
- line[len-1] = '\0';
- html_txt(line);
- html("</div>");
- line[len-1] = c;
-}
-
-static void header(const struct object_id *oid1, char *path1, int mode1,
- const struct object_id *oid2, char *path2, int mode2)
-{
- char *abbrev1, *abbrev2;
- int subproject;
-
- subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
- html("<div class='head'>");
- html("diff --git a/");
- html_txt(path1);
- html(" b/");
- html_txt(path2);
-
- if (mode1 == 0)
- htmlf("<br/>new file mode %.6o", mode2);
-
- if (mode2 == 0)
- htmlf("<br/>deleted file mode %.6o", mode1);
-
- if (!subproject) {
- abbrev1 = xstrdup(repo_find_unique_abbrev(the_repository, oid1, DEFAULT_ABBREV));
- abbrev2 = xstrdup(repo_find_unique_abbrev(the_repository, oid2, DEFAULT_ABBREV));
- htmlf("<br/>index %s..%s", abbrev1, abbrev2);
- free(abbrev1);
- free(abbrev2);
- if (mode1 != 0 && mode2 != 0) {
- htmlf(" %.6o", mode1);
- if (mode2 != mode1)
- htmlf("..%.6o", mode2);
- }
- if (is_null_oid(oid1)) {
- path1 = "dev/null";
- html("<br/>--- /");
- } else
- html("<br/>--- a/");
- if (mode1 != 0)
- cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
- oid_to_hex(old_rev_oid), path1);
- else
- html_txt(path1);
- if (is_null_oid(oid2)) {
- path2 = "dev/null";
- html("<br/>+++ /");
- } else
- html("<br/>+++ b/");
- if (mode2 != 0)
- cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
- oid_to_hex(new_rev_oid), path2);
- else
- html_txt(path2);
- }
- html("</div>");
-}
-
-static void filepair_cb(struct diff_filepair *pair)
-{
- unsigned long old_size = 0;
- unsigned long new_size = 0;
- int binary = 0;
- linediff_fn print_line_fn = print_line;
-
- if (!show_filepair(pair))
- return;
-
- current_filepair = pair;
- if (use_ssdiff) {
- cgit_ssdiff_header_begin();
- print_line_fn = cgit_ssdiff_line_cb;
- }
- header(&pair->one->oid, pair->one->path, pair->one->mode,
- &pair->two->oid, pair->two->path, pair->two->mode);
- if (use_ssdiff)
- cgit_ssdiff_header_end();
- if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
- if (S_ISGITLINK(pair->one->mode))
- print_line_fn(fmt("-Subproject %s", oid_to_hex(&pair->one->oid)), 52);
- if (S_ISGITLINK(pair->two->mode))
- print_line_fn(fmt("+Subproject %s", oid_to_hex(&pair->two->oid)), 52);
- if (use_ssdiff)
- cgit_ssdiff_footer();
- return;
- }
- if (cgit_diff_files(&pair->one->oid, &pair->two->oid, &old_size,
- &new_size, &binary, ctx.qry.context,
- ctx.qry.ignorews, print_line_fn))
- cgit_print_error("Error running diff");
- if (binary) {
- if (use_ssdiff)
- html("<tr><td colspan='4'>Binary files differ</td></tr>");
- else
- html("Binary files differ");
- }
- if (use_ssdiff)
- cgit_ssdiff_footer();
-}
-
-void cgit_print_diff_ctrls(void)
-{
- int i, curr;
-
- html("<div class='cgit-panel'>");
- html("<b>diff options</b>");
- html("<form method='get'>");
- cgit_add_hidden_formfields(1, 0, ctx.qry.page);
- html("<table>");
- html("<tr><td colspan='2'/></tr>");
- html("<tr>");
- html("<td class='label'>context:</td>");
- html("<td class='ctrl'>");
- html("<select name='context' onchange='this.form.submit();'>");
- curr = ctx.qry.context;
- if (!curr)
- curr = 3;
- for (i = 1; i <= 10; i++)
- html_intoption(i, fmt("%d", i), curr);
- for (i = 15; i <= 40; i += 5)
- html_intoption(i, fmt("%d", i), curr);
- html("</select>");
- html("</td>");
- html("</tr><tr>");
- html("<td class='label'>space:</td>");
- html("<td class='ctrl'>");
- html("<select name='ignorews' onchange='this.form.submit();'>");
- html_intoption(0, "include", ctx.qry.ignorews);
- html_intoption(1, "ignore", ctx.qry.ignorews);
- html("</select>");
- html("</td>");
- html("</tr><tr>");
- html("<td class='label'>mode:</td>");
- html("<td class='ctrl'>");
- html("<select name='dt' onchange='this.form.submit();'>");
- curr = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
- html_intoption(0, "unified", curr);
- html_intoption(1, "ssdiff", curr);
- html_intoption(2, "stat only", curr);
- html("</select></td></tr>");
- html("<tr><td/><td class='ctrl'>");
- html("<noscript><input type='submit' value='reload'/></noscript>");
- html("</td></tr></table>");
- html("</form>");
- html("</div>");
-}
-
-void cgit_print_diff(const char *new_rev, const char *old_rev,
- const char *prefix, int show_ctrls, int raw)
-{
- struct commit *commit, *commit2;
- const struct object_id *old_tree_oid, *new_tree_oid;
- diff_type difftype;
-
- /*
- * If "follow" is set then the diff machinery needs to examine the
- * entire commit to detect renames so we must limit the paths in our
- * own callbacks and not pass the prefix to the diff machinery.
- */
- if (ctx.qry.follow && ctx.repo->enable_follow_links) {
- current_prefix = prefix;
- prefix = "";
- } else {
- current_prefix = NULL;
- }
-
- if (!new_rev)
- new_rev = ctx.qry.head;
- if (repo_get_oid(the_repository, new_rev, new_rev_oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad object name: %s", new_rev);
- return;
- }
- commit = lookup_commit_reference(the_repository, new_rev_oid);
- if (!commit || repo_parse_commit(the_repository, commit)) {
- cgit_print_error_page(404, "Not found",
- "Bad commit: %s", oid_to_hex(new_rev_oid));
- return;
- }
- new_tree_oid = get_commit_tree_oid(commit);
-
- if (old_rev) {
- if (repo_get_oid(the_repository, old_rev, old_rev_oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad object name: %s", old_rev);
- return;
- }
- } else if (commit->parents && commit->parents->item) {
- oidcpy(old_rev_oid, &commit->parents->item->object.oid);
- } else {
- oidclr(old_rev_oid, the_repository->hash_algo);
- }
-
- if (!is_null_oid(old_rev_oid)) {
- commit2 = lookup_commit_reference(the_repository, old_rev_oid);
- if (!commit2 || repo_parse_commit(the_repository, commit2)) {
- cgit_print_error_page(404, "Not found",
- "Bad commit: %s", oid_to_hex(old_rev_oid));
- return;
- }
- old_tree_oid = get_commit_tree_oid(commit2);
- } else {
- old_tree_oid = NULL;
- }
-
- if (raw) {
- struct diff_options diffopt;
-
- repo_diff_setup(the_repository, &diffopt);
- diffopt.output_format = DIFF_FORMAT_PATCH;
- diffopt.flags.recursive = 1;
- diff_setup_done(&diffopt);
-
- ctx.page.mimetype = "text/plain";
- cgit_print_http_headers();
- if (old_tree_oid) {
- diff_tree_oid(old_tree_oid, new_tree_oid, "",
- &diffopt);
- } else {
- diff_root_tree_oid(new_tree_oid, "", &diffopt);
- }
- diffcore_std(&diffopt);
- diff_flush(&diffopt);
-
- return;
- }
-
- difftype = ctx.qry.has_difftype ? ctx.qry.difftype : ctx.cfg.difftype;
- use_ssdiff = difftype == DIFF_SSDIFF;
-
- if (show_ctrls) {
- cgit_print_layout_start();
- cgit_print_diff_ctrls();
- }
-
- /*
- * Clicking on a link to a file in the diff stat should show a diff
- * of the file, showing the diff stat limited to a single file is
- * pretty useless. All links from this point on will be to
- * individual files, so we simply reset the difftype in the query
- * here to avoid propagating DIFF_STATONLY to the individual files.
- */
- if (difftype == DIFF_STATONLY)
- ctx.qry.difftype = ctx.cfg.difftype;
-
- cgit_print_diffstat(old_rev_oid, new_rev_oid, prefix);
-
- if (difftype == DIFF_STATONLY)
- return;
-
- if (use_ssdiff) {
- html("<table summary='ssdiff' class='ssdiff'>");
- } else {
- html("<table summary='diff' class='diff'>");
- html("<tr><td>");
- }
- cgit_diff_tree(old_rev_oid, new_rev_oid, filepair_cb, prefix,
- ctx.qry.ignorews);
- if (!use_ssdiff)
- html("</td></tr>");
- html("</table>");
-
- if (show_ctrls)
- cgit_print_layout_end();
-}
diff --git a/cgit/ui-diff.h b/cgit/ui-diff.h
deleted file mode 100644
--- a/cgit/ui-diff.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_DIFF_H
-#define UI_DIFF_H
-
-extern void cgit_print_diff_ctrls(void);
-
-extern void cgit_print_diff(const char *new_hex, const char *old_hex,
- const char *prefix, int show_ctrls, int raw);
-
-extern struct diff_filespec *cgit_get_current_old_file(void);
-extern struct diff_filespec *cgit_get_current_new_file(void);
-
-extern struct object_id old_rev_oid[1];
-extern struct object_id new_rev_oid[1];
-
-#endif /* UI_DIFF_H */
diff --git a/cgit/ui-patch.c b/cgit/ui-patch.c
deleted file mode 100644
--- a/cgit/ui-patch.c
+++ /dev/null
@@ -1,104 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* ui-patch.c: generate patch view
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#define USE_THE_REPOSITORY_VARIABLE
-
-#include "cgit.h"
-#include "ui-patch.h"
-#include "html.h"
-#include "ui-shared.h"
-
-/* two commit hashes with two dots in between and termination */
-#define REV_RANGE_LEN 2 * GIT_MAX_HEXSZ + 3
-
-void cgit_print_patch(const char *new_rev, const char *old_rev,
- const char *prefix)
-{
- struct rev_info rev;
- struct commit *commit;
- struct object_id new_rev_oid, old_rev_oid;
- char rev_range[REV_RANGE_LEN];
- const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL };
- int rev_argc = ARRAY_SIZE(rev_argv) - 1;
- char *patchname;
-
- if (!prefix)
- rev_argc--;
-
- if (!new_rev)
- new_rev = ctx.qry.head;
-
- if (repo_get_oid(the_repository, new_rev, &new_rev_oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad object id: %s", new_rev);
- return;
- }
- commit = lookup_commit_reference(the_repository, &new_rev_oid);
- if (!commit) {
- cgit_print_error_page(404, "Not found",
- "Bad commit reference: %s", new_rev);
- return;
- }
-
- if (old_rev) {
- if (repo_get_oid(the_repository, old_rev, &old_rev_oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad object id: %s", old_rev);
- return;
- }
- if (!lookup_commit_reference(the_repository, &old_rev_oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad commit reference: %s", old_rev);
- return;
- }
- } else if (commit->parents && commit->parents->item) {
- oidcpy(&old_rev_oid, &commit->parents->item->object.oid);
- } else {
- oidclr(&old_rev_oid, the_repository->hash_algo);
- }
-
- if (is_null_oid(&old_rev_oid)) {
- memcpy(rev_range, oid_to_hex(&new_rev_oid), the_hash_algo->hexsz + 1);
- } else {
- xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_rev_oid),
- oid_to_hex(&new_rev_oid));
- }
-
- patchname = fmt("%s.patch", rev_range);
- ctx.page.mimetype = "text/plain";
- ctx.page.filename = patchname;
- cgit_print_http_headers();
-
- if (ctx.cfg.noplainemail) {
- rev_argv[2] = "--format=format:From %H Mon Sep 17 00:00:00 "
- "2001%nFrom: %an%nDate: %aD%n%w(78,0,1)Subject: "
- "%s%n%n%w(0)%b";
- }
-
- repo_init_revisions(the_repository, &rev, NULL);
- rev.abbrev = DEFAULT_ABBREV;
- rev.verbose_header = 1;
- rev.diff = 1;
- rev.show_root_diff = 1;
- rev.max_parents = 1;
- rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT |
- DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY;
- if (prefix)
- rev.diffopt.stat_sep = fmt("(limited to '%s')\n\n", prefix);
- setup_revisions(rev_argc, rev_argv, &rev, NULL);
- prepare_revision_walk(&rev);
-
- while ((commit = get_revision(&rev)) != NULL) {
- log_tree_commit(&rev, commit);
- printf("-- \ncgit %s\n\n", cgit_version);
- }
-}
diff --git a/cgit/ui-patch.h b/cgit/ui-patch.h
deleted file mode 100644
--- a/cgit/ui-patch.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_PATCH_H
-#define UI_PATCH_H
-
-extern void cgit_print_patch(const char *new_rev, const char *old_rev,
- const char *prefix);
-
-#endif /* UI_PATCH_H */
diff --git a/cgit/ui-shared.c b/cgit/ui-shared.c
index 49e9c00..cb3f349 100644
--- a/cgit/ui-shared.c
+++ b/cgit/ui-shared.c
@@ -230,14 +230,6 @@ const char *cgit_repobasename(const char *reponame)
return rvbuf;
}
-const char *cgit_snapshot_prefix(const struct cgit_repo *repo)
-{
- if (repo->snapshot_prefix)
- return repo->snapshot_prefix;
-
- return cgit_repobasename(repo->url);
-}
-
static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
{
char *delim = "?";
@@ -365,18 +357,6 @@ void cgit_tree_link(const char *name, const char *title, const char *class,
reporevlink("tree", name, title, class, head, rev, path);
}
-void cgit_plain_link(const char *name, const char *title, const char *class,
- const char *head, const char *rev, const char *path)
-{
- reporevlink("plain", name, title, class, head, rev, path);
-}
-
-void cgit_blame_link(const char *name, const char *title, const char *class,
- const char *head, const char *rev, const char *path)
-{
- reporevlink("blame", name, title, class, head, rev, path);
-}
-
void cgit_log_link(const char *name, const char *title, const char *class,
const char *head, const char *rev, const char *path,
int ofs, const char *grep, const char *pattern, int showmsg,
@@ -458,34 +438,6 @@ void cgit_refs_link(const char *name, const char *title, const char *class,
reporevlink("refs", name, title, class, head, rev, path);
}
-void cgit_snapshot_link(const char *name, const char *title, const char *class,
- const char *head, const char *rev,
- const char *archivename)
-{
- const char *format = NULL;
- const struct cgit_snapshot_format *f;
- char *url, *full;
- int signature = ends_with(archivename, ".asc");
-
- for (f = cgit_snapshot_formats; f->suffix; f++)
- if (strstr(archivename, f->suffix)) {
- format = f->suffix + 1;
- break;
- }
- if (!format)
- return;
- url = repo_view_url(ctx.repo->url,
- signature ? "archive-signature" : "archive",
- link_revision(head, rev), NULL);
- full = fmtalloc("%s?format=%s", url, format);
- free(url);
- link_start(full, title, class);
- free(full);
- html("'>");
- html_txt(name);
- html("</a>");
-}
-
void cgit_diff_link(const char *name, const char *title, const char *class,
const char *head, const char *new_rev, const char *old_rev,
const char *path)
@@ -529,38 +481,6 @@ void cgit_diff_link(const char *name, const char *title, const char *class,
html("</a>");
}
-void cgit_patch_link(const char *name, const char *title, const char *class,
- const char *head, const char *rev, const char *path)
-{
- struct strbuf url = STRBUF_INIT;
- struct object_id oid;
- struct commit *commit;
- const char *new_rev = link_revision(head, rev);
- char *old_rev = xstrdup("HEAD");
-
- if (!repo_get_oid(the_repository, new_rev, &oid) &&
- (commit = lookup_commit_reference(the_repository, &oid)) &&
- commit->parents) {
- free(old_rev);
- old_rev = xstrdup(oid_to_hex(&commit->parents->item->object.oid));
- }
- add_repo_url(&url, ctx.repo->url);
- strbuf_addstr(&url, "/+/patch/");
- add_revision(&url, old_rev);
- strbuf_addstr(&url, "..");
- add_revision(&url, new_rev);
- if (path) {
- strbuf_addstr(&url, "/+/");
- add_url_path(&url, path);
- }
- link_start(url.buf, title, class);
- strbuf_release(&url);
- free(old_rev);
- html("'>");
- html_txt(name);
- html("</a>");
-}
-
void cgit_stats_link(const char *name, const char *title, const char *class,
const char *head, const char *path)
{
@@ -569,150 +489,18 @@ void cgit_stats_link(const char *name, const char *title, const char *class,
static void cgit_self_link(char *name, const char *title, const char *class)
{
- if (!strcmp(ctx.qry.page, "repolist"))
- cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
- ctx.qry.ofs, 1);
- else if (!strcmp(ctx.qry.page, "summary"))
- cgit_summary_link(name, title, class, ctx.qry.head);
- else if (!strcmp(ctx.qry.page, "tag"))
- cgit_tag_link(name, title, class, ctx.qry.has_oid ?
- ctx.qry.oid : ctx.qry.head);
- else if (!strcmp(ctx.qry.page, "tree"))
- cgit_tree_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "plain"))
- cgit_plain_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "blame"))
- cgit_blame_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "log"))
+ if (!strcmp(ctx.qry.page, "log"))
cgit_log_link(name, title, class, ctx.qry.head,
ctx.qry.has_oid ? ctx.qry.oid : NULL,
ctx.qry.path, ctx.qry.ofs,
ctx.qry.grep, ctx.qry.search,
ctx.qry.showmsg, ctx.qry.follow);
- else if (!strcmp(ctx.qry.page, "commit") || !strcmp(ctx.qry.page, "revision"))
- cgit_commit_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "patch"))
- cgit_patch_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "refs"))
- cgit_refs_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "snapshot"))
- cgit_snapshot_link(name, title, class, ctx.qry.head,
- ctx.qry.has_oid ? ctx.qry.oid : NULL,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "diff"))
- cgit_diff_link(name, title, class, ctx.qry.head,
- ctx.qry.oid, ctx.qry.oid2,
- ctx.qry.path);
- else if (!strcmp(ctx.qry.page, "stats"))
- cgit_stats_link(name, title, class, ctx.qry.head,
- ctx.qry.path);
else {
- /* Don't known how to make link for this page */
repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
- html("><!-- cgit_self_link() doesn't know how to make link for page '");
- html_txt(ctx.qry.page);
- html("' -->");
- html_txt(name);
- html("</a>");
- }
-}
-
-void cgit_object_link(struct object *obj)
-{
- char *page, *shortrev, *fullrev, *name;
-
- fullrev = oid_to_hex(&obj->oid);
- shortrev = xstrdup(fullrev);
- shortrev[10] = '\0';
- if (obj->type == OBJ_COMMIT) {
- cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
- ctx.qry.head, fullrev, NULL);
- return;
- } else if (obj->type == OBJ_TREE)
- page = "tree";
- else if (obj->type == OBJ_TAG)
- page = "tag";
- else
- page = "blob";
- name = fmt("%s %s...", type_name(obj->type), shortrev);
- reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
-}
-
-static struct string_list_item *lookup_path(struct string_list *list,
- const char *path)
-{
- struct string_list_item *item;
-
- while (path && path[0]) {
- if ((item = string_list_lookup(list, path)))
- return item;
- if (!(path = strchr(path, '/')))
- break;
- path++;
- }
- return NULL;
-}
-
-void cgit_submodule_link(const char *class, char *path, const char *rev)
-{
- struct string_list *list;
- struct string_list_item *item;
- char tail, *dir;
- size_t len;
-
- len = 0;
- tail = 0;
- list = &ctx.repo->submodules;
- item = lookup_path(list, path);
- if (!item) {
- len = strlen(path);
- tail = path[len - 1];
- if (tail == '/') {
- path[len - 1] = 0;
- item = lookup_path(list, path);
- }
- }
- if (item || ctx.repo->module_link) {
- html("<a ");
- if (class)
- htmlf("class='%s' ", class);
- html("href='");
- if (item) {
- html_attrf(item->util, rev);
- } else {
- dir = strrchr(path, '/');
- if (dir)
- dir++;
- else
- dir = path;
- html_attrf(ctx.repo->module_link, dir, rev);
- }
html("'>");
- html_txt(path);
+ html_txt(name);
html("</a>");
- } else {
- html("<span");
- if (class)
- htmlf(" class='%s'", class);
- html(">");
- html_txt(path);
- html("</span>");
}
- html_txtf(" @ %.7s", rev);
- if (item && tail)
- path[len - 1] = tail;
}
const struct date_mode cgit_date_mode(enum date_mode_type type)
@@ -798,14 +586,6 @@ void cgit_print_http_headers(void)
exit(0);
}
-void cgit_redirect(const char *url, bool permanent)
-{
- htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
- html("Location: ");
- html_url_path(url);
- html("\n\n");
-}
-
static void print_rel_vcs_link(const char *url)
{
html("<link rel='vcs-git' href='");
@@ -1135,20 +915,15 @@ void cgit_print_pageheader(void)
cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
NULL, ctx.qry.vpath, 0, NULL, NULL,
ctx.qry.showmsg, ctx.qry.follow);
- if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
- cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
- ctx.qry.oid, ctx.qry.vpath);
- else
- cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
- ctx.qry.oid, ctx.qry.vpath);
+ cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
+ ctx.qry.oid, ctx.qry.vpath);
cgit_commit_link("commit", NULL,
ctx.qry.page && !strcmp(ctx.qry.page, "revision") ? "active" : hc("commit"),
ctx.qry.head, ctx.qry.oid, ctx.qry.vpath);
cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
- ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath);
- if (ctx.repo->max_stats)
- cgit_stats_link("stats", NULL, hc("stats"),
- ctx.qry.head, ctx.qry.vpath);
+ ctx.qry.oid, NULL, ctx.qry.vpath);
+ cgit_stats_link("stats", NULL, hc("stats"),
+ ctx.qry.head, ctx.qry.vpath);
if (ctx.repo->homepage) {
html("<a href='");
html_attr(ctx.repo->homepage);
@@ -1209,96 +984,3 @@ void cgit_print_pageheader(void)
}
html("<div class='content'>");
}
-
-void cgit_print_filemode(unsigned short mode)
-{
- if (S_ISDIR(mode))
- html("d");
- else if (S_ISLNK(mode))
- html("l");
- else if (S_ISGITLINK(mode))
- html("m");
- else
- html("-");
- html_fileperm(mode >> 6);
- html_fileperm(mode >> 3);
- html_fileperm(mode);
-}
-
-void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
- const char *ref)
-{
- struct object_id oid;
-
- /*
- * Prettify snapshot names by stripping leading "v" or "V" if the tag
- * name starts with {v,V}[0-9] and the prettify mapping is injective,
- * i.e. each stripped tag can be inverted without ambiguities.
- */
- if (repo_get_oid(the_repository, fmt("refs/tags/%s", ref), &oid) == 0 &&
- (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
- ((repo_get_oid(the_repository, fmt("refs/tags/%s", ref + 1), &oid) == 0) +
- (repo_get_oid(the_repository, fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
- (repo_get_oid(the_repository, fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
- ref++;
-
- strbuf_addf(filename, "%s-%s", base, ref);
-}
-
-void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref,
- const char *separator)
-{
- const struct cgit_snapshot_format *f;
- struct strbuf filename = STRBUF_INIT;
- const char *basename;
- size_t prefixlen;
-
- basename = cgit_snapshot_prefix(repo);
- if (starts_with(ref, basename))
- strbuf_addstr(&filename, ref);
- else
- cgit_compose_snapshot_prefix(&filename, basename, ref);
-
- prefixlen = filename.len;
- for (f = cgit_snapshot_formats; f->suffix; f++) {
- if (!(repo->snapshots & cgit_snapshot_format_bit(f)))
- continue;
- strbuf_setlen(&filename, prefixlen);
- strbuf_addstr(&filename, f->suffix);
- cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
- filename.buf);
- if (cgit_snapshot_get_sig(ref, f)) {
- strbuf_addstr(&filename, ".asc");
- html(" (");
- cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
- filename.buf);
- html(")");
- } else if (starts_with(f->suffix, ".tar") && cgit_snapshot_get_sig(ref, &cgit_snapshot_formats[0])) {
- strbuf_setlen(&filename, strlen(filename.buf) - strlen(f->suffix));
- strbuf_addstr(&filename, ".tar.asc");
- html(" (");
- cgit_snapshot_link("sig", NULL, NULL, NULL, NULL,
- filename.buf);
- html(")");
- }
- html(separator);
- }
- strbuf_release(&filename);
-}
-
-void cgit_set_title_from_path(const char *path)
-{
- struct strbuf sb = STRBUF_INIT;
- const char *slash, *last_slash;
-
- if (!path)
- return;
-
- for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) {
- strbuf_add(&sb, slash + 1, last_slash - slash - 1);
- strbuf_addstr(&sb, " \xc2\xab ");
- }
- strbuf_add(&sb, path, last_slash - path);
- strbuf_addf(&sb, " - %s", ctx.page.title);
- ctx.page.title = strbuf_detach(&sb, NULL);
-}
diff --git a/cgit/ui-shared.h b/cgit/ui-shared.h
index 152cb0a..b43a436 100644
--- a/cgit/ui-shared.h
+++ b/cgit/ui-shared.h
@@ -29,12 +29,6 @@ extern void cgit_tag_link(const char *name, const char *title,
extern void cgit_tree_link(const char *name, const char *title,
const char *class, const char *head,
const char *rev, const char *path);
-extern void cgit_plain_link(const char *name, const char *title,
- const char *class, const char *head,
- const char *rev, const char *path);
-extern void cgit_blame_link(const char *name, const char *title,
- const char *class, const char *head,
- const char *rev, const char *path);
extern void cgit_log_link(const char *name, const char *title,
const char *class, const char *head, const char *rev,
const char *path, int ofs, const char *grep,
@@ -42,15 +36,9 @@ extern void cgit_log_link(const char *name, const char *title,
extern void cgit_commit_link(const char *name, const char *title,
const char *class, const char *head,
const char *rev, const char *path);
-extern void cgit_patch_link(const char *name, const char *title,
- const char *class, const char *head,
- const char *rev, const char *path);
extern void cgit_refs_link(const char *name, const char *title,
const char *class, const char *head,
const char *rev, const char *path);
-extern void cgit_snapshot_link(const char *name, const char *title,
- const char *class, const char *head,
- const char *rev, const char *archivename);
extern void cgit_diff_link(const char *name, const char *title,
const char *class, const char *head,
const char *new_rev, const char *old_rev,
@@ -58,11 +46,6 @@ extern void cgit_diff_link(const char *name, const char *title,
extern void cgit_stats_link(const char *name, const char *title,
const char *class, const char *head,
const char *path);
-extern void cgit_object_link(struct object *obj);
-
-extern void cgit_submodule_link(const char *class, char *path,
- const char *rev);
-
extern void cgit_print_layout_start(void);
extern void cgit_print_layout_end(void);
@@ -73,22 +56,14 @@ extern void cgit_vprint_error(const char *fmt, va_list ap);
extern const struct date_mode cgit_date_mode(enum date_mode_type type);
extern void cgit_print_age(time_t t, int tz, time_t max_relative);
extern void cgit_print_http_headers(void);
-extern void cgit_redirect(const char *url, bool permanent);
extern void cgit_print_docstart(void);
extern void cgit_print_docend(void);
__attribute__((format (printf,3,4)))
extern void cgit_print_error_page(int code, const char *msg, const char *fmt, ...);
extern void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap);
extern void cgit_print_pageheader(void);
-extern void cgit_print_filemode(unsigned short mode);
-extern void cgit_compose_snapshot_prefix(struct strbuf *filename,
- const char *base, const char *ref);
-extern void cgit_print_snapshot_links(const struct cgit_repo *repo,
- const char *ref, const char *separator);
-extern const char *cgit_snapshot_prefix(const struct cgit_repo *repo);
extern void cgit_add_clone_urls(void (*fn)(const char *));
extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
const char *page);
-extern void cgit_set_title_from_path(const char *path);
#endif /* UI_SHARED_H */
diff --git a/cgit/ui-snapshot.c b/cgit/ui-snapshot.c
deleted file mode 100644
--- a/cgit/ui-snapshot.c
+++ /dev/null
@@ -1,326 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* ui-snapshot.c: generate snapshot of a commit
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#define USE_THE_REPOSITORY_VARIABLE
-
-#include "cgit.h"
-#include "ui-snapshot.h"
-#include "html.h"
-#include "ui-shared.h"
-
-static int write_archive_type(const char *format, const char *hex, const char *prefix)
-{
- struct strvec argv = STRVEC_INIT;
- const char **nargv;
- int result;
- strvec_push(&argv, "snapshot");
- strvec_push(&argv, format);
- if (prefix) {
- struct strbuf buf = STRBUF_INIT;
- strbuf_addstr(&buf, prefix);
- strbuf_addch(&buf, '/');
- strvec_push(&argv, "--prefix");
- strvec_push(&argv, buf.buf);
- strbuf_release(&buf);
- }
- strvec_push(&argv, hex);
- if (ctx.qry.path) {
- strvec_push(&argv, "--");
- strvec_push(&argv, ctx.qry.path);
- }
- /*
- * Now we need to copy the pointers to arguments into a new
- * structure because write_archive will rearrange its arguments
- * which may result in duplicated/missing entries causing leaks
- * or double-frees in strvec_clear.
- */
- nargv = xmalloc(sizeof(char *) * (argv.nr + 1));
- /* strvec guarantees a trailing NULL entry. */
- memcpy(nargv, argv.v, sizeof(char *) * (argv.nr + 1));
-
- result = write_archive(argv.nr, nargv, NULL, the_repository, NULL, 0);
- strvec_clear(&argv);
- free(nargv);
- return result;
-}
-
-static int write_tar_archive(const char *hex, const char *prefix)
-{
- return write_archive_type("--format=tar", hex, prefix);
-}
-
-static int write_zip_archive(const char *hex, const char *prefix)
-{
- return write_archive_type("--format=zip", hex, prefix);
-}
-
-static int write_compressed_tar_archive(const char *hex,
- const char *prefix,
- char *filter_argv[])
-{
- int rv;
- struct cgit_exec_filter f;
- cgit_exec_filter_init(&f, filter_argv[0], filter_argv);
-
- cgit_open_exec_filter(&f);
- rv = write_tar_archive(hex, prefix);
- cgit_close_exec_filter(&f);
- return rv;
-}
-
-static int write_tar_gzip_archive(const char *hex, const char *prefix)
-{
- char *argv[] = { "gzip", "-n", NULL };
- return write_compressed_tar_archive(hex, prefix, argv);
-}
-
-static int write_tar_bzip2_archive(const char *hex, const char *prefix)
-{
- char *argv[] = { "bzip2", NULL };
- return write_compressed_tar_archive(hex, prefix, argv);
-}
-
-static int write_tar_lzip_archive(const char *hex, const char *prefix)
-{
- char *argv[] = { "lzip", NULL };
- return write_compressed_tar_archive(hex, prefix, argv);
-}
-
-static int write_tar_xz_archive(const char *hex, const char *prefix)
-{
- char *argv[] = { "xz", NULL };
- return write_compressed_tar_archive(hex, prefix, argv);
-}
-
-static int write_tar_zstd_archive(const char *hex, const char *prefix)
-{
- char *argv[] = { "zstd", "-T0", NULL };
- return write_compressed_tar_archive(hex, prefix, argv);
-}
-
-const struct cgit_snapshot_format cgit_snapshot_formats[] = {
- /* .tar must remain the 0 index */
- { ".tar", "application/x-tar", write_tar_archive },
- { ".tar.gz", "application/x-gzip", write_tar_gzip_archive },
- { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive },
- { ".tar.lz", "application/x-lzip", write_tar_lzip_archive },
- { ".tar.xz", "application/x-xz", write_tar_xz_archive },
- { ".tar.zst", "application/x-zstd", write_tar_zstd_archive },
- { ".zip", "application/x-zip", write_zip_archive },
- { NULL }
-};
-
-static struct notes_tree snapshot_sig_notes[ARRAY_SIZE(cgit_snapshot_formats)];
-
-const struct object_id *cgit_snapshot_get_sig(const char *ref,
- const struct cgit_snapshot_format *f)
-{
- struct notes_tree *tree;
- struct object_id oid;
-
- if (repo_get_oid(the_repository, ref, &oid))
- return NULL;
-
- tree = &snapshot_sig_notes[f - &cgit_snapshot_formats[0]];
- if (!tree->initialized) {
- struct strbuf notes_ref = STRBUF_INIT;
-
- strbuf_addf(&notes_ref, "refs/notes/signatures/%s",
- f->suffix + 1);
-
- init_notes(tree, notes_ref.buf, combine_notes_ignore, 0);
- strbuf_release(&notes_ref);
- }
-
- return get_note(tree, &oid);
-}
-
-static const struct cgit_snapshot_format *get_format(const char *filename)
-{
- const struct cgit_snapshot_format *fmt;
-
- for (fmt = cgit_snapshot_formats; fmt->suffix; fmt++) {
- if (ends_with(filename, fmt->suffix))
- return fmt;
- }
- return NULL;
-}
-
-const unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f)
-{
- return BIT(f - &cgit_snapshot_formats[0]);
-}
-
-static int make_snapshot(const struct cgit_snapshot_format *format,
- const char *hex, const char *prefix,
- const char *filename)
-{
- struct object_id oid;
-
- if (repo_get_oid(the_repository, hex, &oid)) {
- cgit_print_error_page(404, "Not found",
- "Bad object id: %s", hex);
- return 1;
- }
- if (!lookup_commit_reference(the_repository, &oid)) {
- cgit_print_error_page(400, "Bad request",
- "Not a commit reference: %s", hex);
- return 1;
- }
- ctx.page.etag = oid_to_hex(&oid);
- ctx.page.mimetype = xstrdup(format->mimetype);
- ctx.page.filename = xstrdup(filename);
- cgit_print_http_headers();
- init_archivers();
- format->write_func(hex, prefix);
- return 0;
-}
-
-static int write_sig(const struct cgit_snapshot_format *format,
- const char *hex, const char *archive,
- const char *filename)
-{
- const struct object_id *note = cgit_snapshot_get_sig(hex, format);
- enum object_type type;
- unsigned long size;
- char *buf;
-
- if (!note) {
- cgit_print_error_page(404, "Not found",
- "No signature for %s", archive);
- return 0;
- }
-
- buf = odb_read_object(the_repository->objects, note, &type, &size);
- if (!buf) {
- cgit_print_error_page(404, "Not found", "Not found");
- return 0;
- }
-
- html("X-Content-Type-Options: nosniff\n");
- html("Content-Security-Policy: default-src 'none'\n");
- ctx.page.etag = oid_to_hex(note);
- ctx.page.mimetype = xstrdup("application/pgp-signature");
- ctx.page.filename = xstrdup(filename);
- cgit_print_http_headers();
-
- html_raw(buf, size);
- free(buf);
- return 0;
-}
-
-/* Try to guess the requested revision from the requested snapshot name.
- * First the format extension is stripped, e.g. "cgit-0.7.2.tar.gz" become
- * "cgit-0.7.2". If this is a valid commit object name we've got a winner.
- * Otherwise, if the snapshot name has a prefix matching the result from
- * repo_basename(), we strip the basename and any following '-' and '_'
- * characters ("cgit-0.7.2" -> "0.7.2") and check the resulting name once
- * more. If this still isn't a valid commit object name, we check if pre-
- * pending a 'v' or a 'V' to the remaining snapshot name ("0.7.2" ->
- * "v0.7.2") gives us something valid.
- */
-static const char *get_ref_from_filename(const struct cgit_repo *repo,
- const char *filename,
- const struct cgit_snapshot_format *format)
-{
- const char *reponame;
- struct object_id oid;
- struct strbuf snapshot = STRBUF_INIT;
- int result = 1;
-
- strbuf_addstr(&snapshot, filename);
- strbuf_setlen(&snapshot, snapshot.len - strlen(format->suffix));
-
- if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
- goto out;
-
- reponame = cgit_snapshot_prefix(repo);
- if (starts_with(snapshot.buf, reponame)) {
- const char *new_start = snapshot.buf;
- new_start += strlen(reponame);
- while (new_start && (*new_start == '-' || *new_start == '_'))
- new_start++;
- strbuf_splice(&snapshot, 0, new_start - snapshot.buf, "", 0);
- }
-
- if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
- goto out;
-
- strbuf_insert(&snapshot, 0, "v", 1);
- if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
- goto out;
-
- strbuf_splice(&snapshot, 0, 1, "V", 1);
- if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
- goto out;
-
- result = 0;
- strbuf_release(&snapshot);
-
-out:
- return result ? strbuf_detach(&snapshot, NULL) : NULL;
-}
-
-void cgit_print_snapshot(const char *head, const char *hex,
- const char *filename, int dwim)
-{
- const struct cgit_snapshot_format* f;
- const char *sig_filename = NULL;
- char *adj_filename = NULL;
- char *prefix = NULL;
-
- if (!filename) {
- cgit_print_error_page(400, "Bad request",
- "No snapshot name specified");
- return;
- }
-
- if (ends_with(filename, ".asc")) {
- sig_filename = filename;
-
- /* Strip ".asc" from filename for common format processing */
- adj_filename = xstrdup(filename);
- adj_filename[strlen(adj_filename) - 4] = '\0';
- filename = adj_filename;
- }
-
- f = get_format(filename);
- if (!f || (!sig_filename && !(ctx.repo->snapshots & cgit_snapshot_format_bit(f)))) {
- cgit_print_error_page(400, "Bad request",
- "Unsupported snapshot format: %s", filename);
- return;
- }
-
- if (!hex && dwim) {
- hex = get_ref_from_filename(ctx.repo, filename, f);
- if (hex == NULL) {
- cgit_print_error_page(404, "Not found", "Not found");
- return;
- }
- prefix = xstrdup(filename);
- prefix[strlen(filename) - strlen(f->suffix)] = '\0';
- }
-
- if (!hex)
- hex = head;
-
- if (!prefix)
- prefix = xstrdup(cgit_snapshot_prefix(ctx.repo));
-
- if (sig_filename)
- write_sig(f, hex, filename, sig_filename);
- else
- make_snapshot(f, hex, prefix, filename);
-
- free(prefix);
- free(adj_filename);
-}
diff --git a/cgit/ui-snapshot.h b/cgit/ui-snapshot.h
deleted file mode 100644
--- a/cgit/ui-snapshot.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_SNAPSHOT_H
-#define UI_SNAPSHOT_H
-
-extern void cgit_print_snapshot(const char *head, const char *hex,
- const char *filename, int dwim);
-
-#endif /* UI_SNAPSHOT_H */
diff --git a/cgit/ui-ssdiff.c b/cgit/ui-ssdiff.c
deleted file mode 100644
--- a/cgit/ui-ssdiff.c
+++ /dev/null
@@ -1,426 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#include "cgit.h"
-#include "ui-ssdiff.h"
-#include "html.h"
-#include "ui-shared.h"
-#include "ui-diff.h"
-
-extern int use_ssdiff;
-
-static int current_old_line, current_new_line;
-static int **L = NULL;
-
-struct deferred_lines {
- int line_no;
- char *line;
- struct deferred_lines *next;
-};
-
-static struct deferred_lines *deferred_old, *deferred_old_last;
-static struct deferred_lines *deferred_new, *deferred_new_last;
-
-static void create_or_reset_lcs_table(void)
-{
- int i;
-
- if (L != NULL) {
- memset(*L, 0, sizeof(int) * MAX_SSDIFF_SIZE);
- return;
- }
-
- // xcalloc will die if we ran out of memory;
- // not very helpful for debugging
- L = (int**)xcalloc(MAX_SSDIFF_M, sizeof(int *));
- *L = (int*)xcalloc(MAX_SSDIFF_SIZE, sizeof(int));
-
- for (i = 1; i < MAX_SSDIFF_M; i++) {
- L[i] = *L + i * MAX_SSDIFF_N;
- }
-}
-
-static char *longest_common_subsequence(char *A, char *B)
-{
- int i, j, ri;
- int m = strlen(A);
- int n = strlen(B);
- int tmp1, tmp2;
- int lcs_length;
- char *result;
-
- // We bail if the lines are too long
- if (m >= MAX_SSDIFF_M || n >= MAX_SSDIFF_N)
- return NULL;
-
- create_or_reset_lcs_table();
-
- for (i = m; i >= 0; i--) {
- for (j = n; j >= 0; j--) {
- if (A[i] == '\0' || B[j] == '\0') {
- L[i][j] = 0;
- } else if (A[i] == B[j]) {
- L[i][j] = 1 + L[i + 1][j + 1];
- } else {
- tmp1 = L[i + 1][j];
- tmp2 = L[i][j + 1];
- L[i][j] = (tmp1 > tmp2 ? tmp1 : tmp2);
- }
- }
- }
-
- lcs_length = L[0][0];
- result = xmalloc(lcs_length + 2);
- memset(result, 0, sizeof(*result) * (lcs_length + 2));
-
- ri = 0;
- i = 0;
- j = 0;
- while (i < m && j < n) {
- if (A[i] == B[j]) {
- result[ri] = A[i];
- ri += 1;
- i += 1;
- j += 1;
- } else if (L[i + 1][j] >= L[i][j + 1]) {
- i += 1;
- } else {
- j += 1;
- }
- }
-
- return result;
-}
-
-static int line_from_hunk(char *line, char type)
-{
- char *buf1, *buf2;
- int len, res;
-
- buf1 = strchr(line, type);
- if (buf1 == NULL)
- return 0;
- buf1 += 1;
- buf2 = strchr(buf1, ',');
- if (buf2 == NULL)
- return 0;
- len = buf2 - buf1;
- buf2 = xmalloc(len + 1);
- strlcpy(buf2, buf1, len + 1);
- res = atoi(buf2);
- free(buf2);
- return res;
-}
-
-static char *replace_tabs(char *line)
-{
- char *prev_buf = line;
- char *cur_buf;
- size_t linelen = strlen(line);
- int n_tabs = 0;
- int i;
- char *result;
- size_t result_len;
-
- if (linelen == 0) {
- result = xmalloc(1);
- result[0] = '\0';
- return result;
- }
-
- for (i = 0; i < linelen; i++) {
- if (line[i] == '\t')
- n_tabs += 1;
- }
- result_len = linelen + n_tabs * 8;
- result = xmalloc(result_len + 1);
- result[0] = '\0';
-
- for (;;) {
- cur_buf = strchr(prev_buf, '\t');
- if (!cur_buf) {
- linelen = strlen(result);
- strlcpy(&result[linelen], prev_buf, result_len - linelen + 1);
- break;
- } else {
- linelen = strlen(result);
- strlcpy(&result[linelen], prev_buf, cur_buf - prev_buf + 1);
- linelen = strlen(result);
- memset(&result[linelen], ' ', 8 - (linelen % 8));
- result[linelen + 8 - (linelen % 8)] = '\0';
- }
- prev_buf = cur_buf + 1;
- }
- return result;
-}
-
-static int calc_deferred_lines(struct deferred_lines *start)
-{
- struct deferred_lines *item = start;
- int result = 0;
- while (item) {
- result += 1;
- item = item->next;
- }
- return result;
-}
-
-static void deferred_old_add(char *line, int line_no)
-{
- struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
- item->line = xstrdup(line);
- item->line_no = line_no;
- item->next = NULL;
- if (deferred_old) {
- deferred_old_last->next = item;
- deferred_old_last = item;
- } else {
- deferred_old = deferred_old_last = item;
- }
-}
-
-static void deferred_new_add(char *line, int line_no)
-{
- struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
- item->line = xstrdup(line);
- item->line_no = line_no;
- item->next = NULL;
- if (deferred_new) {
- deferred_new_last->next = item;
- deferred_new_last = item;
- } else {
- deferred_new = deferred_new_last = item;
- }
-}
-
-static void print_part_with_lcs(char *class, char *line, char *lcs)
-{
- int line_len = strlen(line);
- int i, j;
- char c[2] = " ";
- int same = 1;
-
- j = 0;
- for (i = 0; i < line_len; i++) {
- c[0] = line[i];
- if (same) {
- if (line[i] == lcs[j])
- j += 1;
- else {
- same = 0;
- htmlf("<span class='%s'>", class);
- }
- } else if (line[i] == lcs[j]) {
- same = 1;
- html("</span>");
- j += 1;
- }
- html_txt(c);
- }
- if (!same)
- html("</span>");
-}
-
-static void print_ssdiff_line(char *class,
- int old_line_no,
- char *old_line,
- int new_line_no,
- char *new_line, int individual_chars)
-{
- char *lcs = NULL;
-
- if (old_line)
- old_line = replace_tabs(old_line + 1);
- if (new_line)
- new_line = replace_tabs(new_line + 1);
- if (individual_chars && old_line && new_line)
- lcs = longest_common_subsequence(old_line, new_line);
- html("<tr>\n");
- if (old_line_no > 0) {
- struct diff_filespec *old_file = cgit_get_current_old_file();
- char *lineno_str = fmt("n%d", old_line_no);
- char *fileurl = cgit_treeurl(ctx.repo->url,
- is_null_oid(&old_file->oid) ? "HEAD" : oid_to_hex(old_rev_oid),
- old_file->path, lineno_str);
- html("<td class='lineno'><a href='");
- html(fileurl);
- htmlf("'>%s</a>", lineno_str + 1);
- html("</td>");
- htmlf("<td class='%s'>", class);
- free(fileurl);
- } else if (old_line)
- htmlf("<td class='lineno'></td><td class='%s'>", class);
- else
- htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
- if (old_line) {
- if (lcs)
- print_part_with_lcs("del", old_line, lcs);
- else
- html_txt(old_line);
- }
-
- html("</td>\n");
- if (new_line_no > 0) {
- struct diff_filespec *new_file = cgit_get_current_new_file();
- char *lineno_str = fmt("n%d", new_line_no);
- char *fileurl = cgit_treeurl(ctx.repo->url,
- is_null_oid(&new_file->oid) ? "HEAD" : oid_to_hex(new_rev_oid),
- new_file->path, lineno_str);
- html("<td class='lineno'><a href='");
- html(fileurl);
- htmlf("'>%s</a>", lineno_str + 1);
- html("</td>");
- htmlf("<td class='%s'>", class);
- free(fileurl);
- } else if (new_line)
- htmlf("<td class='lineno'></td><td class='%s'>", class);
- else
- htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
- if (new_line) {
- if (lcs)
- print_part_with_lcs("add", new_line, lcs);
- else
- html_txt(new_line);
- }
-
- html("</td></tr>");
- if (lcs)
- free(lcs);
- if (new_line)
- free(new_line);
- if (old_line)
- free(old_line);
-}
-
-static void print_deferred_old_lines(void)
-{
- struct deferred_lines *iter_old, *tmp;
- iter_old = deferred_old;
- while (iter_old) {
- print_ssdiff_line("del", iter_old->line_no,
- iter_old->line, -1, NULL, 0);
- tmp = iter_old->next;
- free(iter_old);
- iter_old = tmp;
- }
-}
-
-static void print_deferred_new_lines(void)
-{
- struct deferred_lines *iter_new, *tmp;
- iter_new = deferred_new;
- while (iter_new) {
- print_ssdiff_line("add", -1, NULL,
- iter_new->line_no, iter_new->line, 0);
- tmp = iter_new->next;
- free(iter_new);
- iter_new = tmp;
- }
-}
-
-static void print_deferred_changed_lines(void)
-{
- struct deferred_lines *iter_old, *iter_new, *tmp;
- int n_old_lines = calc_deferred_lines(deferred_old);
- int n_new_lines = calc_deferred_lines(deferred_new);
- int individual_chars = (n_old_lines == n_new_lines ? 1 : 0);
-
- iter_old = deferred_old;
- iter_new = deferred_new;
- while (iter_old || iter_new) {
- if (iter_old && iter_new)
- print_ssdiff_line("changed", iter_old->line_no,
- iter_old->line,
- iter_new->line_no, iter_new->line,
- individual_chars);
- else if (iter_old)
- print_ssdiff_line("changed", iter_old->line_no,
- iter_old->line, -1, NULL, 0);
- else if (iter_new)
- print_ssdiff_line("changed", -1, NULL,
- iter_new->line_no, iter_new->line, 0);
- if (iter_old) {
- tmp = iter_old->next;
- free(iter_old);
- iter_old = tmp;
- }
-
- if (iter_new) {
- tmp = iter_new->next;
- free(iter_new);
- iter_new = tmp;
- }
- }
-}
-
-void cgit_ssdiff_print_deferred_lines(void)
-{
- if (!deferred_old && !deferred_new)
- return;
- if (deferred_old && !deferred_new)
- print_deferred_old_lines();
- else if (!deferred_old && deferred_new)
- print_deferred_new_lines();
- else
- print_deferred_changed_lines();
- deferred_old = deferred_old_last = NULL;
- deferred_new = deferred_new_last = NULL;
-}
-
-/*
- * print a single line returned from xdiff
- */
-void cgit_ssdiff_line_cb(char *line, int len)
-{
- char c = line[len - 1];
- line[len - 1] = '\0';
- if (line[0] == '@') {
- current_old_line = line_from_hunk(line, '-');
- current_new_line = line_from_hunk(line, '+');
- }
-
- if (line[0] == ' ') {
- if (deferred_old || deferred_new)
- cgit_ssdiff_print_deferred_lines();
- print_ssdiff_line("ctx", current_old_line, line,
- current_new_line, line, 0);
- current_old_line += 1;
- current_new_line += 1;
- } else if (line[0] == '+') {
- deferred_new_add(line, current_new_line);
- current_new_line += 1;
- } else if (line[0] == '-') {
- deferred_old_add(line, current_old_line);
- current_old_line += 1;
- } else if (line[0] == '@') {
- html("<tr><td colspan='4' class='hunk'>");
- html_txt(line);
- html("</td></tr>");
- } else {
- html("<tr><td colspan='4' class='ctx'>");
- html_txt(line);
- html("</td></tr>");
- }
- line[len - 1] = c;
-}
-
-void cgit_ssdiff_header_begin(void)
-{
- current_old_line = -1;
- current_new_line = -1;
- html("<tr><td class='space' colspan='4'><div></div></td></tr>");
- html("<tr><td class='head' colspan='4'>");
-}
-
-void cgit_ssdiff_header_end(void)
-{
- html("</td></tr>");
-}
-
-void cgit_ssdiff_footer(void)
-{
- if (deferred_old || deferred_new)
- cgit_ssdiff_print_deferred_lines();
- html("<tr><td class='foot' colspan='4'></td></tr>");
-}
diff --git a/cgit/ui-ssdiff.h b/cgit/ui-ssdiff.h
deleted file mode 100644
--- a/cgit/ui-ssdiff.h
+++ /dev/null
@@ -1,29 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_SSDIFF_H
-#define UI_SSDIFF_H
-
-/*
- * ssdiff line limits
- */
-#ifndef MAX_SSDIFF_M
-#define MAX_SSDIFF_M 128
-#endif
-
-#ifndef MAX_SSDIFF_N
-#define MAX_SSDIFF_N 128
-#endif
-#define MAX_SSDIFF_SIZE ((MAX_SSDIFF_M) * (MAX_SSDIFF_N))
-
-extern void cgit_ssdiff_print_deferred_lines(void);
-
-extern void cgit_ssdiff_line_cb(char *line, int len);
-
-extern void cgit_ssdiff_header_begin(void);
-extern void cgit_ssdiff_header_end(void);
-
-extern void cgit_ssdiff_footer(void);
-
-#endif /* UI_SSDIFF_H */
diff --git a/cgit/ui-stats.c b/cgit/ui-stats.c
deleted file mode 100644
--- a/cgit/ui-stats.c
+++ /dev/null
@@ -1,439 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* ui-stats.c: generate stats view
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * Licensed under GNU General Public License v2
- * (see COPYING for full license text)
- */
-
-#define USE_THE_REPOSITORY_VARIABLE
-
-#include "cgit.h"
-#include "ui-stats.h"
-#include "html.h"
-#include "ui-shared.h"
-
-struct authorstat {
- long total;
- struct string_list list;
-};
-
-#define DAY_SECS (60 * 60 * 24)
-#define WEEK_SECS (DAY_SECS * 7)
-
-static void trunc_week(struct tm *tm)
-{
- time_t t = timegm(tm);
- t -= ((tm->tm_wday + 6) % 7) * DAY_SECS;
- gmtime_r(&t, tm);
-}
-
-static void dec_week(struct tm *tm)
-{
- time_t t = timegm(tm);
- t -= WEEK_SECS;
- gmtime_r(&t, tm);
-}
-
-static void inc_week(struct tm *tm)
-{
- time_t t = timegm(tm);
- t += WEEK_SECS;
- gmtime_r(&t, tm);
-}
-
-static char *pretty_week(struct tm *tm)
-{
- static char buf[10];
-
- strftime(buf, sizeof(buf), "W%V %G", tm);
- return buf;
-}
-
-static void trunc_month(struct tm *tm)
-{
- tm->tm_mday = 1;
-}
-
-static void dec_month(struct tm *tm)
-{
- tm->tm_mon--;
- if (tm->tm_mon < 0) {
- tm->tm_year--;
- tm->tm_mon = 11;
- }
-}
-
-static void inc_month(struct tm *tm)
-{
- tm->tm_mon++;
- if (tm->tm_mon > 11) {
- tm->tm_year++;
- tm->tm_mon = 0;
- }
-}
-
-static char *pretty_month(struct tm *tm)
-{
- static const char *months[] = {
- "Jan", "Feb", "Mar", "Apr", "May", "Jun",
- "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
- };
- return fmt("%s %d", months[tm->tm_mon], tm->tm_year + 1900);
-}
-
-static void trunc_quarter(struct tm *tm)
-{
- trunc_month(tm);
- while (tm->tm_mon % 3 != 0)
- dec_month(tm);
-}
-
-static void dec_quarter(struct tm *tm)
-{
- dec_month(tm);
- dec_month(tm);
- dec_month(tm);
-}
-
-static void inc_quarter(struct tm *tm)
-{
- inc_month(tm);
- inc_month(tm);
- inc_month(tm);
-}
-
-static char *pretty_quarter(struct tm *tm)
-{
- return fmt("Q%d %d", tm->tm_mon / 3 + 1, tm->tm_year + 1900);
-}
-
-static void trunc_year(struct tm *tm)
-{
- trunc_month(tm);
- tm->tm_mon = 0;
-}
-
-static void dec_year(struct tm *tm)
-{
- tm->tm_year--;
-}
-
-static void inc_year(struct tm *tm)
-{
- tm->tm_year++;
-}
-
-static char *pretty_year(struct tm *tm)
-{
- return fmt("%d", tm->tm_year + 1900);
-}
-
-static const struct cgit_period periods[] = {
- {'w', "week", 12, 4, trunc_week, dec_week, inc_week, pretty_week},
- {'m', "month", 12, 4, trunc_month, dec_month, inc_month, pretty_month},
- {'q', "quarter", 12, 4, trunc_quarter, dec_quarter, inc_quarter, pretty_quarter},
- {'y', "year", 12, 4, trunc_year, dec_year, inc_year, pretty_year},
-};
-
-/* Given a period code or name, return a period index (1, 2, 3 or 4)
- * and update the period pointer to the correcsponding struct.
- * If no matching code is found, return 0.
- */
-int cgit_find_stats_period(const char *expr, const struct cgit_period **period)
-{
- int i;
- char code = '\0';
-
- if (!expr)
- return 0;
-
- if (strlen(expr) == 1)
- code = expr[0];
-
- for (i = 0; i < sizeof(periods) / sizeof(periods[0]); i++)
- if (periods[i].code == code || !strcmp(periods[i].name, expr)) {
- if (period)
- *period = &periods[i];
- return i + 1;
- }
- return 0;
-}
-
-const char *cgit_find_stats_periodname(int idx)
-{
- if (idx > 0 && idx < 4)
- return periods[idx - 1].name;
- else
- return "";
-}
-
-static void add_commit(struct string_list *authors, struct commit *commit,
- const struct cgit_period *period)
-{
- struct commitinfo *info;
- struct string_list_item *author, *item;
- struct authorstat *authorstat;
- struct string_list *items;
- char *tmp;
- struct tm date;
- time_t t;
- uintptr_t *counter;
-
- info = cgit_parse_commit(commit);
- tmp = xstrdup(info->author);
- author = string_list_insert(authors, tmp);
- if (!author->util)
- author->util = xcalloc(1, sizeof(struct authorstat));
- else
- free(tmp);
- authorstat = author->util;
- items = &authorstat->list;
- t = info->committer_date;
- gmtime_r(&t, &date);
- period->trunc(&date);
- tmp = xstrdup(period->pretty(&date));
- item = string_list_insert(items, tmp);
- counter = (uintptr_t *)&item->util;
- if (*counter)
- free(tmp);
- (*counter)++;
-
- authorstat->total++;
- cgit_free_commitinfo(info);
-}
-
-static int cmp_total_commits(const void *a1, const void *a2)
-{
- const struct string_list_item *i1 = a1;
- const struct string_list_item *i2 = a2;
- const struct authorstat *auth1 = i1->util;
- const struct authorstat *auth2 = i2->util;
-
- return auth2->total - auth1->total;
-}
-
-/* Walk the commit DAG and collect number of commits per author per
- * timeperiod into a nested string_list collection.
- */
-static struct string_list collect_stats(const struct cgit_period *period)
-{
- struct string_list authors;
- struct rev_info rev;
- struct commit *commit;
- const char *argv[] = {NULL, ctx.qry.head, NULL, NULL, NULL, NULL};
- int argc = 3;
- time_t now;
- long i;
- struct tm tm;
- char tmp[11];
-
- time(&now);
- gmtime_r(&now, &tm);
- period->trunc(&tm);
- for (i = 1; i < period->count; i++)
- period->dec(&tm);
- strftime(tmp, sizeof(tmp), "%Y-%m-%d", &tm);
- argv[2] = xstrdup(fmt("--since=%s", tmp));
- if (ctx.qry.path) {
- argv[3] = "--";
- argv[4] = ctx.qry.path;
- argc += 2;
- }
- repo_init_revisions(the_repository, &rev, NULL);
- rev.abbrev = DEFAULT_ABBREV;
- rev.commit_format = CMIT_FMT_DEFAULT;
- rev.max_parents = 1;
- rev.verbose_header = 1;
- rev.show_root_diff = 0;
- setup_revisions(argc, argv, &rev, NULL);
- prepare_revision_walk(&rev);
- memset(&authors, 0, sizeof(authors));
- while ((commit = get_revision(&rev)) != NULL) {
- add_commit(&authors, commit, period);
- release_commit_memory(the_repository->parsed_objects, commit);
- commit->parents = NULL;
- }
- return authors;
-}
-
-static void print_combined_authorrow(struct string_list *authors, int from,
- int to, const char *name,
- const char *leftclass,
- const char *centerclass,
- const char *rightclass,
- const struct cgit_period *period)
-{
- struct string_list_item *author;
- struct authorstat *authorstat;
- struct string_list *items;
- struct string_list_item *date;
- time_t now;
- long i, j, total, subtotal;
- struct tm tm;
- char *tmp;
-
- time(&now);
- gmtime_r(&now, &tm);
- period->trunc(&tm);
- for (i = 1; i < period->count; i++)
- period->dec(&tm);
-
- total = 0;
- htmlf("<tr><td class='%s'>%s</td>", leftclass,
- fmt(name, to - from + 1));
- for (j = 0; j < period->count; j++) {
- tmp = period->pretty(&tm);
- period->inc(&tm);
- subtotal = 0;
- for (i = from; i <= to; i++) {
- author = &authors->items[i];
- authorstat = author->util;
- items = &authorstat->list;
- date = string_list_lookup(items, tmp);
- if (date)
- subtotal += (uintptr_t)date->util;
- }
- htmlf("<td class='%s'>%ld</td>", centerclass, subtotal);
- total += subtotal;
- }
- htmlf("<td class='%s'>%ld</td></tr>", rightclass, total);
-}
-
-static void print_authors(struct string_list *authors, int top,
- const struct cgit_period *period)
-{
- struct string_list_item *author;
- struct authorstat *authorstat;
- struct string_list *items;
- struct string_list_item *date;
- time_t now;
- long i, j, total;
- struct tm tm;
- char *tmp;
-
- time(&now);
- gmtime_r(&now, &tm);
- period->trunc(&tm);
- for (i = 1; i < period->count; i++)
- period->dec(&tm);
-
- html("<table class='stats'><tr><th>Author</th>");
- for (j = 0; j < period->count; j++) {
- tmp = period->pretty(&tm);
- htmlf("<th>%s</th>", tmp);
- period->inc(&tm);
- }
- html("<th>Total</th></tr>\n");
-
- if (top <= 0 || top > authors->nr)
- top = authors->nr;
-
- for (i = 0; i < top; i++) {
- author = &authors->items[i];
- html("<tr><td class='left'>");
- html_txt(author->string);
- html("</td>");
- authorstat = author->util;
- items = &authorstat->list;
- total = 0;
- for (j = 0; j < period->count; j++)
- period->dec(&tm);
- for (j = 0; j < period->count; j++) {
- tmp = period->pretty(&tm);
- period->inc(&tm);
- date = string_list_lookup(items, tmp);
- if (!date)
- html("<td>0</td>");
- else {
- htmlf("<td>%lu</td>", (uintptr_t)date->util);
- total += (uintptr_t)date->util;
- }
- }
- htmlf("<td class='sum'>%ld</td></tr>", total);
- }
-
- if (top < authors->nr)
- print_combined_authorrow(authors, top, authors->nr - 1,
- "Others (%ld)", "left", "", "sum", period);
-
- print_combined_authorrow(authors, 0, authors->nr - 1, "Total",
- "total", "sum", "sum", period);
- html("</table>");
-}
-
-/* Create a sorted string_list with one entry per author. The util-field
- * for each author is another string_list which is used to calculate the
- * number of commits per time-interval.
- */
-void cgit_show_stats(void)
-{
- struct string_list authors;
- const struct cgit_period *period;
- int top, i;
- const char *code = "w";
-
- if (ctx.qry.period)
- code = ctx.qry.period;
-
- i = cgit_find_stats_period(code, &period);
- if (!i) {
- cgit_print_error_page(404, "Not found",
- "Unknown statistics type: %c", code[0]);
- return;
- }
- if (i > ctx.repo->max_stats) {
- cgit_print_error_page(400, "Bad request",
- "Statistics type disabled: %s", period->name);
- return;
- }
- authors = collect_stats(period);
- qsort(authors.items, authors.nr, sizeof(struct string_list_item),
- cmp_total_commits);
-
- top = ctx.qry.ofs;
- if (!top)
- top = 10;
-
- cgit_print_layout_start();
- html("<div class='cgit-panel'>");
- html("<b>stat options</b>");
- html("<form method='get'>");
- cgit_add_hidden_formfields(1, 0, "stats");
- html("<table><tr><td colspan='2'/></tr>");
- if (ctx.repo->max_stats > 1) {
- html("<tr><td class='label'>Period:</td>");
- html("<td class='ctrl'><select name='period' onchange='this.form.submit();'>");
- for (i = 0; i < ctx.repo->max_stats; i++)
- html_option(fmt("%c", periods[i].code),
- periods[i].name, fmt("%c", period->code));
- html("</select></td></tr>");
- }
- html("<tr><td class='label'>Authors:</td>");
- html("<td class='ctrl'><select name='ofs' onchange='this.form.submit();'>");
- html_intoption(10, "10", top);
- html_intoption(25, "25", top);
- html_intoption(50, "50", top);
- html_intoption(100, "100", top);
- html_intoption(-1, "all", top);
- html("</select></td></tr>");
- html("<tr><td/><td class='ctrl'>");
- html("<noscript><input type='submit' value='Reload'/></noscript>");
- html("</td></tr></table>");
- html("</form>");
- html("</div>");
- htmlf("<h2>Commits per author per %s", period->name);
- if (ctx.qry.path) {
- html(" (path '");
- html_txt(ctx.qry.path);
- html("')");
- }
- html("</h2>");
- print_authors(&authors, top, period);
- cgit_print_layout_end();
-}
-
diff --git a/cgit/ui-stats.h b/cgit/ui-stats.h
deleted file mode 100644
--- a/cgit/ui-stats.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-#ifndef UI_STATS_H
-#define UI_STATS_H
-
-#include "cgit.h"
-
-struct cgit_period {
- const char code;
- const char *name;
- int max_periods;
- int count;
-
- /* Convert a tm value to the first day in the period */
- void (*trunc)(struct tm *tm);
-
- /* Update tm value to start of next/previous period */
- void (*dec)(struct tm *tm);
- void (*inc)(struct tm *tm);
-
- /* Pretty-print a tm value */
- char *(*pretty)(struct tm *tm);
-};
-
-extern int cgit_find_stats_period(const char *expr, const struct cgit_period **period);
-extern const char *cgit_find_stats_periodname(int idx);
-
-extern void cgit_show_stats(void);
-
-#endif /* UI_STATS_H */
diff --git a/crates/gilti/Cargo.toml b/crates/gilti/Cargo.toml
index a20c3dd..c98b0a2 100644
--- a/crates/gilti/Cargo.toml
+++ b/crates/gilti/Cargo.toml
@@ -17,6 +17,7 @@ rust-version.workspace = true
[dependencies]
axum.workspace = true
git2.workspace = true
+libc.workspace = true
percent-encoding.workspace = true
tokio.workspace = true
tower.workspace = true
diff --git a/crates/gilti/src/lfs.rs b/crates/gilti/src/lfs.rs
index 01371ee..8eaf88c 100644
--- a/crates/gilti/src/lfs.rs
+++ b/crates/gilti/src/lfs.rs
@@ -74,13 +74,12 @@ pub async fn serve(
if path == "objects/batch" && method == axum::http::Method::POST {
return batch(objects, write_enabled, request).await;
}
- if method == axum::http::Method::POST {
- if let Some(oid) = path
+ if method == axum::http::Method::POST
+ && let Some(oid) = path
.strip_prefix("objects/")
.and_then(|value| value.strip_suffix("/verify"))
- {
- return verify(&objects, oid, request).await;
- }
+ {
+ return verify(&objects, oid, request).await;
}
if let Some(oid) = path.strip_prefix("objects/") {
if !valid_oid(oid) {
diff --git a/crates/gilti/src/main.rs b/crates/gilti/src/main.rs
index 2fbc1f6..1d42963 100644
--- a/crates/gilti/src/main.rs
+++ b/crates/gilti/src/main.rs
@@ -18,8 +18,15 @@ const GIT_HTTP_BACKENDS: &[&str] = &[
"/usr/libexec/git-core/git-http-backend",
"/usr/lib/git-core/git-http-backend",
];
+const GIT: &str = "/usr/bin/git";
const GIT_HOME: &str = "/var/lib/gilti/git";
const REPOSITORIES: &str = "/var/lib/gilti/git/repositories";
+const ARCHIVE_COMPRESSORS: &[&str] = &[
+ "/usr/bin/bzip2",
+ "/usr/bin/lzip",
+ "/usr/bin/xz",
+ "/usr/bin/zstd",
+];
const CGIT_CSS: &str = "/usr/share/webapps/cgit/cgit.css";
const CGIT_JS: &str = "/usr/share/webapps/cgit/cgit.js";
@@ -37,7 +44,6 @@ const CGIT_ENVIRONMENT: &[(&str, &str)] = &[
("CGIT_ENABLE_LOG_FILECOUNT", "1"),
("CGIT_ENABLE_LOG_LINECOUNT", "1"),
("CGIT_ENABLE_REMOTE_BRANCHES", "0"),
- ("CGIT_ENABLE_SUBJECT_LINKS", "0"),
("CGIT_FAVICON", "/-/assets/favicon.ico"),
("CGIT_FOOTER", ""),
("CGIT_HEADER", ""),
@@ -49,9 +55,6 @@ const CGIT_ENVIRONMENT: &[(&str, &str)] = &[
("CGIT_MAX_ATOM_ITEMS", "10"),
("CGIT_MAX_COMMIT_COUNT", "50"),
("CGIT_MAX_MESSAGE_LENGTH", "80"),
- ("CGIT_MAX_STATS", "4"),
- ("CGIT_MIMETYPE_FILE", ""),
- ("CGIT_MODULE_LINK", ""),
("CGIT_NOHEADER", "0"),
("CGIT_NOPLAINEMAIL", "0"),
("CGIT_README_0", ":README.md"),
@@ -60,7 +63,6 @@ const CGIT_ENVIRONMENT: &[(&str, &str)] = &[
("CGIT_RENAMELIMIT", "-1"),
("CGIT_ROBOTS", "index, nofollow"),
("CGIT_SECTION", ""),
- ("CGIT_SNAPSHOTS", "2147483647"),
("CGIT_VIRTUAL_ROOT", "/"),
("GIT_ATTR_NOSYSTEM", "1"),
("GIT_CONFIG_NOSYSTEM", "1"),
@@ -240,7 +242,11 @@ fn git_http_backend() -> std::io::Result<std::path::PathBuf> {
}
fn check_files(git_http_backend: &std::path::Path) -> std::io::Result<()> {
- for path in [std::path::Path::new(CGIT), git_http_backend] {
+ for path in std::iter::once(std::path::Path::new(CGIT))
+ .chain(std::iter::once(git_http_backend))
+ .chain(std::iter::once(std::path::Path::new(GIT)))
+ .chain(ARCHIVE_COMPRESSORS.iter().map(std::path::Path::new))
+ {
let metadata = std::fs::metadata(path)?;
if !metadata.is_file()
|| std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o111 == 0
@@ -320,17 +326,16 @@ impl RepositoryService {
router::Route::Repositories
| router::Route::Overview(_)
| router::Route::About(_)
+ | router::Route::Stats(_)
| router::Route::Object(_)
| router::Route::Refs(_)
+ | router::Route::Revision(_)
| router::Route::Tree(_)
| router::Route::Blame(_)
+ | router::Route::Archive(_)
| router::Route::ArchiveSignature(_)
- ) || matches!(
- &route,
- router::Route::Revision(router::RepoRoute {
- params: router::Revision::Ref(reference),
- ..
- }) if reference.starts_with("refs/tags/")
+ | router::Route::Diff(_)
+ | router::Route::Patch(_)
);
let query = if migrated {
let query = match request_query(request.uri().query()) {
@@ -371,6 +376,20 @@ impl RepositoryService {
router::Route::About(route) => {
views::about::serve(&self.views, route, request.method().clone()).await
}
+ router::Route::Stats(route) => {
+ let query = match views::stats::Query::from_request(
+ query.as_ref().expect("query parsed"),
+ ) {
+ Ok(query) => query,
+ Err(views::stats::QueryError::BadRequest) => {
+ return views::bad_request("bad query\n");
+ }
+ Err(views::stats::QueryError::NotFound) => {
+ return plain_response(axum::http::StatusCode::NOT_FOUND, "not found\n");
+ }
+ };
+ views::stats::serve(&self.views, route, query, request.method().clone()).await
+ }
router::Route::Object(route) => {
views::object::serve(REPOSITORIES, route, request.method().clone()).await
}
@@ -383,6 +402,12 @@ impl RepositoryService {
router::Route::Blame(route) => {
views::blame::serve(&self.views, route, request.method().clone()).await
}
+ router::Route::Archive(route) => {
+ let Some(format) = views::archive::Format::parse(format) else {
+ return plain_response(axum::http::StatusCode::NOT_FOUND, "not found\n");
+ };
+ views::archive::serve(&self.views, route, format, request.method().clone()).await
+ }
router::Route::ArchiveSignature(route) => {
views::archive_signature::serve(
&self.views,
@@ -400,6 +425,32 @@ impl RepositoryService {
{
views::tag::serve(&self.views, route, request.method().clone()).await
}
+ router::Route::Revision(route) => {
+ let query =
+ match views::diff::Query::from_request(query.as_ref().expect("query parsed")) {
+ Ok(query) => query,
+ Err(()) => return views::bad_request("bad query\n"),
+ };
+ views::revision::serve(&self.views, route, query, request.method().clone()).await
+ }
+ router::Route::Diff(route) => {
+ let query =
+ match views::diff::Query::from_request(query.as_ref().expect("query parsed")) {
+ Ok(query) => query,
+ Err(()) => return views::bad_request("bad query\n"),
+ };
+ views::diff::serve(
+ &self.views,
+ route,
+ query,
+ format == Some("raw"),
+ request.method().clone(),
+ )
+ .await
+ }
+ router::Route::Patch(route) => {
+ views::patch::serve(&self.views, route, request.method().clone()).await
+ }
router::Route::GitLfs(route) => {
lfs::serve(
std::path::Path::new(REPOSITORIES),
@@ -617,20 +668,10 @@ fn cgit_environment(
current_url: &str,
) -> Result<Vec<(std::ffi::OsString, std::ffi::OsString)>, ()> {
let mut environment = query.environment;
- let format = query.format.as_deref();
let mut set = |name: &str, value: String| environment.push((name.into(), value.into()));
set("GILTI_CURRENT_URL", current_url.to_owned());
match route {
- router::Route::Stats(route) => {
- set("GILTI_REPOSITORY", route.repo);
- set("GILTI_PAGE", "stats".to_owned());
- }
- router::Route::Revision(route) => {
- set("GILTI_REPOSITORY", route.repo);
- set("GILTI_PAGE", "revision".to_owned());
- set("GILTI_REVISION", revision(route.params));
- }
router::Route::Log(route) => {
set("GILTI_REPOSITORY", route.repo);
set("GILTI_PAGE", "log".to_owned());
@@ -639,15 +680,6 @@ fn cgit_environment(
set("GILTI_PATH", path);
}
}
- router::Route::Archive(route) => {
- set("GILTI_REPOSITORY", route.repo);
- set("GILTI_PAGE", "snapshot".to_owned());
- set("GILTI_REVISION", revision(route.params.rev));
- set("GILTI_FORMAT", format.unwrap_or("tar.gz").to_owned());
- if let Some(path) = route.params.path {
- set("GILTI_PATH", path);
- }
- }
router::Route::AtomFeed(route) => {
set("GILTI_REPOSITORY", route.repo);
set("GILTI_PAGE", "atom".to_owned());
@@ -656,49 +688,11 @@ fn cgit_environment(
set("GILTI_PATH", path);
}
}
- router::Route::Diff(route) => {
- return comparison_environment(environment, route, format, false);
- }
- router::Route::Patch(route) => {
- return comparison_environment(environment, route, format, true);
- }
_ => return Err(()),
}
Ok(environment)
}
-fn comparison_environment(
- mut environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
- route: router::RepoRoute<router::Comparison>,
- format: Option<&str>,
- patch: bool,
-) -> Result<Vec<(std::ffi::OsString, std::ffi::OsString)>, ()> {
- environment.push(("GILTI_REPOSITORY".into(), route.repo.into()));
- environment.push((
- "GILTI_PAGE".into(),
- if patch {
- "patch"
- } else if format == Some("raw") {
- "rawdiff"
- } else {
- "diff"
- }
- .into(),
- ));
- environment.push((
- "GILTI_OLD_REVISION".into(),
- revision(route.params.old_rev).into(),
- ));
- environment.push((
- "GILTI_REVISION".into(),
- revision(route.params.new_rev).into(),
- ));
- if let Some(path) = route.params.path {
- environment.push(("GILTI_PATH".into(), path.into()));
- }
- Ok(environment)
-}
-
fn revision(revision: router::Revision) -> String {
match revision {
router::Revision::Head => "HEAD".to_owned(),
@@ -782,24 +776,15 @@ mod tests {
#[test]
fn route_parameters_become_trusted_cgit_environment() {
- let route = super::router::parse(
- "/group/repo/+/diff/refs/heads/main..0123456789abcdef0123456789abcdef01234567/+/src/lib.rs",
- )
- .unwrap();
- let query = super::request_query(Some("format=raw&context=5")).unwrap();
+ let route = super::router::parse("/group/repo/+/refs/heads/main/+/log/src/lib.rs").unwrap();
+ let query = super::request_query(Some("ofs=5&follow=1")).unwrap();
let values = super::cgit_environment(route, query, "/group/repo").unwrap();
assert_eq!(environment(&values, "GILTI_REPOSITORY"), "group/repo");
- assert_eq!(environment(&values, "GILTI_PAGE"), "rawdiff");
- assert_eq!(
- environment(&values, "GILTI_OLD_REVISION"),
- "refs/heads/main"
- );
- assert_eq!(
- environment(&values, "GILTI_REVISION"),
- "0123456789abcdef0123456789abcdef01234567"
- );
+ assert_eq!(environment(&values, "GILTI_PAGE"), "log");
+ assert_eq!(environment(&values, "GILTI_REVISION"), "refs/heads/main");
assert_eq!(environment(&values, "GILTI_PATH"), "src/lib.rs");
- assert_eq!(environment(&values, "GILTI_QUERY_CONTEXT"), "5");
+ assert_eq!(environment(&values, "GILTI_QUERY_OFFSET"), "5");
+ assert_eq!(environment(&values, "GILTI_QUERY_FOLLOW"), "1");
}
#[test]
diff --git a/crates/gilti/src/models/archive.rs b/crates/gilti/src/models/archive.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/models/archive.rs
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::path::{Path, PathBuf};
+
+pub struct Archive {
+ pub repository_path: PathBuf,
+ pub oid: String,
+ pub prefix: String,
+}
+
+impl Archive {
+ pub fn load(
+ root: &Path,
+ name: &str,
+ revision: &crate::router::Revision,
+ path: Option<&str>,
+ ) -> Result<Self, super::Error> {
+ let repository = super::repository::open(root, name)?;
+ let commit = super::revision::commit(&repository, revision)?;
+ if let Some(path) = path {
+ commit
+ .tree()
+ .and_then(|tree| tree.get_path(Path::new(path)))
+ .map_err(|_| super::Error::NotFound)?;
+ }
+ let repository_path = repository.path().to_owned();
+ let prefix = name
+ .rsplit('/')
+ .next()
+ .unwrap_or(name)
+ .strip_suffix(".git")
+ .unwrap_or_else(|| name.rsplit('/').next().unwrap_or(name))
+ .to_owned();
+ Ok(Self {
+ repository_path,
+ oid: commit.id().to_string(),
+ prefix,
+ })
+ }
+}
diff --git a/crates/gilti/src/models/commit.rs b/crates/gilti/src/models/commit.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/models/commit.rs
@@ -0,0 +1,151 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::path::Path;
+
+pub struct Commit {
+ pub repository: super::repository::Info,
+ pub revision: String,
+ pub oid: String,
+ pub tree: String,
+ pub author: Identity,
+ pub committer: Identity,
+ pub subject: String,
+ pub message: String,
+ pub parents: Vec<String>,
+ pub decorations: Vec<Decoration>,
+ pub notes: Option<String>,
+ pub diff: Option<super::diff::Diff>,
+}
+
+pub struct Identity {
+ pub name: String,
+ pub email: String,
+ pub timestamp: i64,
+ pub offset_minutes: i32,
+}
+
+pub struct Decoration {
+ pub label: String,
+ pub reference: Option<String>,
+}
+
+impl Commit {
+ pub fn load(
+ root: &Path,
+ name: &str,
+ revision: crate::router::Revision,
+ diff_options: super::diff::Options,
+ ) -> Result<Self, super::Error> {
+ let repository = super::repository::open(root, name)?;
+ let info = super::repository::info(&repository, name);
+ let commit = super::revision::commit(&repository, &revision)?;
+ let oid = commit.id();
+ let parents = commit
+ .parent_ids()
+ .map(|id| id.to_string())
+ .collect::<Vec<_>>();
+ let author = identity(commit.author());
+ let committer = identity(commit.committer());
+ let message = String::from_utf8_lossy(commit.message_bytes());
+ let subject =
+ String::from_utf8_lossy(commit.summary_bytes().unwrap_or_default()).into_owned();
+ let body = message
+ .split_once('\n')
+ .map_or("", |(_, body)| body.trim_start_matches('\n'))
+ .to_owned();
+ let notes = repository
+ .find_note(None, oid)
+ .ok()
+ .map(|note| String::from_utf8_lossy(note.message_bytes()).into_owned());
+ let refs = super::refs::Refs::load(root, name)?;
+ let mut decorations = Vec::new();
+ if repository
+ .head()
+ .ok()
+ .and_then(|head| head.peel_to_commit().ok())
+ .is_some_and(|head| head.id() == oid)
+ {
+ decorations.push(Decoration {
+ label: "HEAD".to_owned(),
+ reference: None,
+ });
+ }
+ for branch in refs.branches {
+ if repository
+ .find_reference(&branch.reference)
+ .ok()
+ .and_then(|reference| reference.peel_to_commit().ok())
+ .is_some_and(|target| target.id() == oid)
+ {
+ decorations.push(Decoration {
+ label: branch.name,
+ reference: Some(branch.reference),
+ });
+ }
+ }
+ for tag in refs.tags {
+ if repository
+ .find_reference(&tag.reference)
+ .ok()
+ .and_then(|reference| reference.peel_to_commit().ok())
+ .is_some_and(|target| target.id() == oid)
+ {
+ decorations.push(Decoration {
+ label: tag.name,
+ reference: Some(tag.reference),
+ });
+ }
+ }
+ let old_revision = (parents.len() < 3)
+ .then(|| parents.first().cloned())
+ .flatten()
+ .map(crate::router::Revision::Commit);
+ let show_diff = parents.len() < 3;
+ let revision_name = super::revision::selector(&revision);
+ let tree = commit.tree_id().to_string();
+ drop(commit);
+ drop(repository);
+ let diff = show_diff
+ .then(|| {
+ super::diff::Diff::load(
+ root,
+ name,
+ old_revision,
+ revision.clone(),
+ None,
+ diff_options,
+ )
+ })
+ .transpose()?;
+ Ok(Self {
+ repository: info,
+ revision: revision_name,
+ oid: oid.to_string(),
+ tree,
+ author,
+ committer,
+ subject,
+ message: body,
+ parents,
+ decorations,
+ notes,
+ diff,
+ })
+ }
+}
+
+fn identity(signature: git2::Signature<'_>) -> Identity {
+ Identity {
+ name: signature
+ .name()
+ .map(str::to_owned)
+ .unwrap_or_else(|_| String::from_utf8_lossy(signature.name_bytes()).into_owned()),
+ email: signature
+ .email()
+ .map(str::to_owned)
+ .unwrap_or_else(|_| String::from_utf8_lossy(signature.email_bytes()).into_owned()),
+ timestamp: signature.when().seconds(),
+ offset_minutes: signature.when().offset_minutes(),
+ }
+}
diff --git a/crates/gilti/src/models/diff.rs b/crates/gilti/src/models/diff.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/models/diff.rs
@@ -0,0 +1,159 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::path::Path;
+
+#[derive(Clone, Copy)]
+pub struct Options {
+ pub context: u32,
+ pub ignore_whitespace: bool,
+}
+
+pub struct Diff {
+ pub repository: super::repository::Info,
+ pub old_revision: Option<String>,
+ pub new_revision: String,
+ pub old_oid: Option<String>,
+ pub new_oid: String,
+ pub files: Vec<File>,
+ pub additions: usize,
+ pub deletions: usize,
+}
+
+pub struct File {
+ pub status: git2::Delta,
+ pub old_path: String,
+ pub new_path: String,
+ pub old_oid: Option<String>,
+ pub new_oid: Option<String>,
+ pub old_mode: u32,
+ pub new_mode: u32,
+ pub old_size: u64,
+ pub new_size: u64,
+ pub binary: bool,
+ pub additions: usize,
+ pub deletions: usize,
+ pub hunks: Vec<Hunk>,
+}
+
+pub struct Hunk {
+ pub header: String,
+ pub lines: Vec<Line>,
+}
+
+pub struct Line {
+ pub origin: char,
+ pub content: String,
+ pub old_line: Option<u32>,
+ pub new_line: Option<u32>,
+}
+
+impl Diff {
+ pub fn load(
+ root: &Path,
+ name: &str,
+ old_revision: Option<crate::router::Revision>,
+ new_revision: crate::router::Revision,
+ path: Option<String>,
+ options: Options,
+ ) -> Result<Self, super::Error> {
+ let repository = super::repository::open(root, name)?;
+ let info = super::repository::info(&repository, name);
+ let new_commit = super::revision::commit(&repository, &new_revision)?;
+ let old_commit = old_revision
+ .as_ref()
+ .map(|revision| super::revision::commit(&repository, revision))
+ .transpose()?;
+ let old_tree = old_commit
+ .as_ref()
+ .map(git2::Commit::tree)
+ .transpose()
+ .map_err(super::Error::from_git)?;
+ let new_tree = new_commit.tree().map_err(super::Error::from_git)?;
+ let mut diff_options = git2::DiffOptions::new();
+ diff_options.context_lines(options.context);
+ if options.ignore_whitespace {
+ diff_options.ignore_whitespace(true);
+ }
+ if let Some(path) = &path {
+ diff_options.pathspec(path).disable_pathspec_match(true);
+ }
+ let mut diff = repository
+ .diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), Some(&mut diff_options))
+ .map_err(super::Error::from_git)?;
+ diff.find_similar(None).map_err(super::Error::from_git)?;
+ let stats = diff.stats().map_err(super::Error::from_git)?;
+ let mut files = Vec::with_capacity(diff.deltas().len());
+ for (index, delta) in diff.deltas().enumerate() {
+ let old_file = delta.old_file();
+ let new_file = delta.new_file();
+ let patch = git2::Patch::from_diff(&diff, index).map_err(super::Error::from_git)?;
+ let (additions, deletions, hunks) = if let Some(patch) = patch {
+ let (_, additions, deletions) =
+ patch.line_stats().map_err(super::Error::from_git)?;
+ let mut hunks = Vec::with_capacity(patch.num_hunks());
+ for hunk_index in 0..patch.num_hunks() {
+ let (hunk, lines) = patch.hunk(hunk_index).map_err(super::Error::from_git)?;
+ let mut result = Hunk {
+ header: String::from_utf8_lossy(hunk.header())
+ .trim_end_matches('\n')
+ .to_owned(),
+ lines: Vec::with_capacity(lines),
+ };
+ for line_index in 0..lines {
+ let line = patch
+ .line_in_hunk(hunk_index, line_index)
+ .map_err(super::Error::from_git)?;
+ result.lines.push(Line {
+ origin: line.origin(),
+ content: String::from_utf8_lossy(line.content())
+ .trim_end_matches('\n')
+ .to_owned(),
+ old_line: line.old_lineno(),
+ new_line: line.new_lineno(),
+ });
+ }
+ hunks.push(result);
+ }
+ (additions, deletions, hunks)
+ } else {
+ (0, 0, Vec::new())
+ };
+ files.push(File {
+ status: delta.status(),
+ old_path: path_string(old_file.path_bytes()),
+ new_path: path_string(new_file.path_bytes()),
+ old_oid: oid(old_file.id()),
+ new_oid: oid(new_file.id()),
+ old_mode: old_file.mode().into(),
+ new_mode: new_file.mode().into(),
+ old_size: old_file.size(),
+ new_size: new_file.size(),
+ binary: old_file.is_binary() || new_file.is_binary(),
+ additions,
+ deletions,
+ hunks,
+ });
+ }
+ Ok(Self {
+ repository: info,
+ old_revision: old_revision.as_ref().map(super::revision::selector),
+ new_revision: super::revision::selector(&new_revision),
+ old_oid: old_commit.map(|commit| commit.id().to_string()),
+ new_oid: new_commit.id().to_string(),
+ files,
+ additions: stats.insertions(),
+ deletions: stats.deletions(),
+ })
+ }
+}
+
+fn path_string(path: Option<&[u8]>) -> String {
+ path.map_or_else(String::new, |path| {
+ String::from_utf8_lossy(path).into_owned()
+ })
+}
+
+fn oid(oid: git2::Oid) -> Option<String> {
+ (!oid.is_zero()).then(|| oid.to_string())
+}
diff --git a/crates/gilti/src/models/mod.rs b/crates/gilti/src/models/mod.rs
index 4eafed6..f60f9f3 100644
--- a/crates/gilti/src/models/mod.rs
+++ b/crates/gilti/src/models/mod.rs
@@ -2,14 +2,19 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod about;
+pub mod archive;
pub mod archive_signature;
pub mod blame;
+pub mod commit;
+pub mod diff;
pub mod object;
pub mod overview;
+pub mod patch;
pub mod refs;
pub mod repositories;
pub mod repository;
pub mod revision;
+pub mod stats;
pub mod tag;
pub mod tree;
@@ -31,3 +36,87 @@ impl Error {
}
}
}
+
+#[cfg(test)]
+mod migration_tests {
+ #[test]
+ fn loads_diff_commit_stats_archive_and_patch_models() {
+ let root =
+ std::env::temp_dir().join(format!("gilti-five-model-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&root);
+ std::fs::create_dir_all(&root).unwrap();
+ let repository = git2::Repository::init_bare(root.join("example.git")).unwrap();
+ let signature = git2::Signature::now("Model Tester", "model@example.test").unwrap();
+ let first_blob = repository.blob(b"first\n").unwrap();
+ let mut builder = repository.treebuilder(None).unwrap();
+ builder.insert("file.txt", first_blob, 0o100644).unwrap();
+ let first_tree_oid = builder.write().unwrap();
+ drop(builder);
+ let first_tree = repository.find_tree(first_tree_oid).unwrap();
+ let first = repository
+ .commit(
+ Some("refs/heads/main"),
+ &signature,
+ &signature,
+ "first",
+ &first_tree,
+ &[],
+ )
+ .unwrap();
+ let first = repository.find_commit(first).unwrap();
+ let first_oid = first.id();
+ let second_blob = repository.blob(b"second\n").unwrap();
+ let mut builder = repository.treebuilder(None).unwrap();
+ builder.insert("file.txt", second_blob, 0o100644).unwrap();
+ let second_tree_oid = builder.write().unwrap();
+ drop(builder);
+ let second_tree = repository.find_tree(second_tree_oid).unwrap();
+ let second = repository
+ .commit(
+ Some("refs/heads/main"),
+ &signature,
+ &signature,
+ "second\n\nbody",
+ &second_tree,
+ &[&first],
+ )
+ .unwrap();
+ repository.set_head("refs/heads/main").unwrap();
+ drop(first);
+ drop(first_tree);
+ drop(second_tree);
+ drop(repository);
+
+ let old = crate::router::Revision::Commit(first_oid.to_string());
+ let new = crate::router::Revision::Commit(second.to_string());
+ let options = super::diff::Options {
+ context: 3,
+ ignore_whitespace: false,
+ };
+ let diff = super::diff::Diff::load(
+ &root,
+ "example",
+ Some(old.clone()),
+ new.clone(),
+ None,
+ options,
+ )
+ .unwrap();
+ assert_eq!(diff.files.len(), 1);
+ assert_eq!((diff.additions, diff.deletions), (1, 1));
+ let commit = super::commit::Commit::load(&root, "example", new.clone(), options).unwrap();
+ assert_eq!(commit.subject, "second");
+ assert_eq!(commit.parents, [first_oid.to_string()]);
+ assert!(commit.diff.is_some());
+ let stats =
+ super::stats::Stats::load(&root, "example", super::stats::Period::Week).unwrap();
+ assert_eq!(stats.totals.iter().sum::<usize>(), 2);
+ let archive =
+ super::archive::Archive::load(&root, "example", &new, Some("file.txt")).unwrap();
+ assert_eq!(archive.oid, second.to_string());
+ let patch = super::patch::Patch::load(&root, "example", &old, &new).unwrap();
+ assert_eq!(patch.old_oid, first_oid.to_string());
+ assert_eq!(patch.new_oid, second.to_string());
+ std::fs::remove_dir_all(root).unwrap();
+ }
+}
diff --git a/crates/gilti/src/models/overview.rs b/crates/gilti/src/models/overview.rs
index f9be49d..a84e6be 100644
--- a/crates/gilti/src/models/overview.rs
+++ b/crates/gilti/src/models/overview.rs
@@ -63,17 +63,17 @@ impl Overview {
}
}
for tag in &refs.tags {
- if let Ok(reference) = repository.find_reference(&tag.reference) {
- if let Ok(commit) = reference.peel_to_commit() {
- decorations
- .entry(commit.id())
- .or_default()
- .push(Decoration {
- label: tag.name.clone(),
- reference: Some(tag.reference.clone()),
- tag: true,
- });
- }
+ if let Ok(reference) = repository.find_reference(&tag.reference)
+ && let Ok(commit) = reference.peel_to_commit()
+ {
+ decorations
+ .entry(commit.id())
+ .or_default()
+ .push(Decoration {
+ label: tag.name.clone(),
+ reference: Some(tag.reference.clone()),
+ tag: true,
+ });
}
}
let mut walk = repository.revwalk().map_err(super::Error::from_git)?;
diff --git a/crates/gilti/src/models/patch.rs b/crates/gilti/src/models/patch.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/models/patch.rs
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::path::{Path, PathBuf};
+
+pub struct Patch {
+ pub repository_path: PathBuf,
+ pub old_oid: String,
+ pub new_oid: String,
+}
+
+impl Patch {
+ pub fn load(
+ root: &Path,
+ name: &str,
+ old: &crate::router::Revision,
+ new: &crate::router::Revision,
+ ) -> Result<Self, super::Error> {
+ let repository = super::repository::open(root, name)?;
+ let old = super::revision::commit(&repository, old)?;
+ let new = super::revision::commit(&repository, new)?;
+ Ok(Self {
+ repository_path: repository.path().to_owned(),
+ old_oid: old.id().to_string(),
+ new_oid: new.id().to_string(),
+ })
+ }
+}
diff --git a/crates/gilti/src/models/repository.rs b/crates/gilti/src/models/repository.rs
index b8a36ee..150c853 100644
--- a/crates/gilti/src/models/repository.rs
+++ b/crates/gilti/src/models/repository.rs
@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
pub struct Info {
pub name: String,
pub description: String,
+ pub has_readme: bool,
}
pub fn info(repository: &git2::Repository, name: &str) -> Info {
@@ -14,9 +15,20 @@ pub fn info(repository: &git2::Repository, name: &str) -> Info {
.map(|description| description.trim().to_owned())
.filter(|description| !description.is_empty())
.unwrap_or_else(|| "[no description]".to_owned());
+ let has_readme = repository
+ .head()
+ .and_then(|head| head.peel_to_tree())
+ .ok()
+ .is_some_and(|tree| {
+ ["README.md", "README"].iter().any(|path| {
+ tree.get_path(Path::new(path))
+ .is_ok_and(|entry| entry.kind() == Some(git2::ObjectType::Blob))
+ })
+ });
Info {
name: name.to_owned(),
description,
+ has_readme,
}
}
diff --git a/crates/gilti/src/models/stats.rs b/crates/gilti/src/models/stats.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/models/stats.rs
@@ -0,0 +1,191 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use std::collections::BTreeMap;
+use std::path::Path;
+
+#[derive(Clone, Copy, Eq, PartialEq)]
+pub enum Period {
+ Week,
+ Month,
+ Quarter,
+ Year,
+}
+
+impl Period {
+ pub fn name(self) -> &'static str {
+ match self {
+ Self::Week => "week",
+ Self::Month => "month",
+ Self::Quarter => "quarter",
+ Self::Year => "year",
+ }
+ }
+}
+
+pub struct Stats {
+ pub repository: super::repository::Info,
+ pub labels: Vec<String>,
+ pub authors: Vec<Author>,
+ pub totals: Vec<usize>,
+}
+
+pub struct Author {
+ pub name: String,
+ pub counts: Vec<usize>,
+ pub total: usize,
+}
+
+impl Stats {
+ pub fn load(root: &Path, name: &str, period: Period) -> Result<Self, super::Error> {
+ let repository = super::repository::open(root, name)?;
+ let info = super::repository::info(&repository, name);
+ let head = super::revision::commit(&repository, &crate::router::Revision::Head)?;
+ let now = std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .map_or(0, |duration| duration.as_secs() as i64);
+ let labels = labels(period, now);
+ let positions = labels
+ .iter()
+ .enumerate()
+ .map(|(index, label)| (label.clone(), index))
+ .collect::<BTreeMap<_, _>>();
+ let mut authors = BTreeMap::<String, Vec<usize>>::new();
+ let mut walk = repository.revwalk().map_err(super::Error::from_git)?;
+ walk.push(head.id()).map_err(super::Error::from_git)?;
+ walk.set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
+ .map_err(super::Error::from_git)?;
+ for oid in walk {
+ let commit = repository
+ .find_commit(oid.map_err(super::Error::from_git)?)
+ .map_err(super::Error::from_git)?;
+ if commit.parent_count() > 1 {
+ continue;
+ }
+ let label = label(period, commit.time().seconds());
+ let Some(index) = positions.get(&label).copied() else {
+ continue;
+ };
+ let signature = commit.author();
+ let author = signature
+ .name()
+ .map(str::to_owned)
+ .unwrap_or_else(|_| String::from_utf8_lossy(signature.name_bytes()).into_owned());
+ authors.entry(author).or_insert_with(|| vec![0; 4])[index] += 1;
+ }
+ let mut authors = authors
+ .into_iter()
+ .map(|(name, counts)| Author {
+ total: counts.iter().sum(),
+ name,
+ counts,
+ })
+ .collect::<Vec<_>>();
+ authors.sort_by(|left, right| {
+ right
+ .total
+ .cmp(&left.total)
+ .then_with(|| left.name.cmp(&right.name))
+ });
+ let totals = (0..4)
+ .map(|index| authors.iter().map(|author| author.counts[index]).sum())
+ .collect();
+ Ok(Self {
+ repository: info,
+ labels,
+ authors,
+ totals,
+ })
+ }
+}
+
+fn labels(period: Period, now: i64) -> Vec<String> {
+ let mut time = tm(now);
+ truncate(period, &mut time);
+ let mut labels = Vec::with_capacity(4);
+ for _ in 0..4 {
+ labels.push(pretty(period, &time));
+ decrement(period, &mut time);
+ }
+ labels.reverse();
+ labels
+}
+
+fn label(period: Period, timestamp: i64) -> String {
+ let mut time = tm(timestamp);
+ truncate(period, &mut time);
+ pretty(period, &time)
+}
+
+fn tm(timestamp: i64) -> libc::tm {
+ let mut result = std::mem::MaybeUninit::<libc::tm>::uninit();
+ // SAFETY: both pointers are valid for the duration of the call.
+ unsafe {
+ libc::gmtime_r(&timestamp, result.as_mut_ptr());
+ result.assume_init()
+ }
+}
+
+fn normalize(time: &mut libc::tm) {
+ // SAFETY: time points to an initialized libc::tm and timegm normalizes it in place.
+ unsafe { libc::timegm(time) };
+}
+
+fn truncate(period: Period, time: &mut libc::tm) {
+ match period {
+ Period::Week => {
+ time.tm_mday -= (time.tm_wday + 6) % 7;
+ }
+ Period::Month => time.tm_mday = 1,
+ Period::Quarter => {
+ time.tm_mday = 1;
+ time.tm_mon -= time.tm_mon % 3;
+ }
+ Period::Year => {
+ time.tm_mday = 1;
+ time.tm_mon = 0;
+ }
+ }
+ normalize(time);
+}
+
+fn decrement(period: Period, time: &mut libc::tm) {
+ match period {
+ Period::Week => time.tm_mday -= 7,
+ Period::Month => time.tm_mon -= 1,
+ Period::Quarter => time.tm_mon -= 3,
+ Period::Year => time.tm_year -= 1,
+ }
+ normalize(time);
+}
+
+fn pretty(period: Period, time: &libc::tm) -> String {
+ match period {
+ Period::Week => strftime("W%V %G", time),
+ Period::Month => {
+ const MONTHS: [&str; 12] = [
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ ];
+ format!("{} {}", MONTHS[time.tm_mon as usize], time.tm_year + 1900)
+ }
+ Period::Quarter => format!("Q{} {}", time.tm_mon / 3 + 1, time.tm_year + 1900),
+ Period::Year => (time.tm_year + 1900).to_string(),
+ }
+}
+
+fn strftime(format: &str, time: &libc::tm) -> String {
+ let format = std::ffi::CString::new(format).expect("static format has no NUL");
+ let mut buffer = [0_u8; 32];
+ // SAFETY: the output buffer, format string and tm pointers are valid.
+ let length = unsafe {
+ libc::strftime(
+ buffer.as_mut_ptr().cast::<libc::c_char>(),
+ buffer.len(),
+ format.as_ptr(),
+ time,
+ )
+ };
+ std::str::from_utf8(&buffer[..length])
+ .expect("strftime emits ASCII for this format")
+ .to_owned()
+}
diff --git a/crates/gilti/src/views/archive.rs b/crates/gilti/src/views/archive.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/views/archive.rs
@@ -0,0 +1,188 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use tokio::io::AsyncWriteExt;
+
+#[derive(Clone, Copy)]
+pub enum Format {
+ Tar,
+ TarGzip,
+ TarBzip2,
+ TarLzip,
+ TarXz,
+ TarZstd,
+ Zip,
+}
+
+impl Format {
+ pub fn parse(value: Option<&str>) -> Option<Self> {
+ match value.unwrap_or("tar.gz") {
+ "tar" => Some(Self::Tar),
+ "tar.gz" => Some(Self::TarGzip),
+ "tar.bz2" => Some(Self::TarBzip2),
+ "tar.lz" => Some(Self::TarLzip),
+ "tar.xz" => Some(Self::TarXz),
+ "tar.zst" => Some(Self::TarZstd),
+ "zip" => Some(Self::Zip),
+ _ => None,
+ }
+ }
+
+ fn extension(self) -> &'static str {
+ match self {
+ Self::Tar => "tar",
+ Self::TarGzip => "tar.gz",
+ Self::TarBzip2 => "tar.bz2",
+ Self::TarLzip => "tar.lz",
+ Self::TarXz => "tar.xz",
+ Self::TarZstd => "tar.zst",
+ Self::Zip => "zip",
+ }
+ }
+
+ fn content_type(self) -> &'static str {
+ match self {
+ Self::Tar => "application/x-tar; charset=UTF-8",
+ Self::TarGzip => "application/x-gzip; charset=UTF-8",
+ Self::TarBzip2 => "application/x-bzip2; charset=UTF-8",
+ Self::TarLzip => "application/x-lzip; charset=UTF-8",
+ Self::TarXz => "application/x-xz; charset=UTF-8",
+ Self::TarZstd => "application/x-zstd; charset=UTF-8",
+ Self::Zip => "application/x-zip; charset=UTF-8",
+ }
+ }
+
+ fn git_format(self) -> &'static str {
+ match self {
+ Self::TarGzip => "tar.gz",
+ Self::Zip => "zip",
+ _ => "tar",
+ }
+ }
+
+ fn compressor(self) -> Option<(&'static str, &'static [&'static str])> {
+ match self {
+ Self::TarBzip2 => Some(("/usr/bin/bzip2", &[])),
+ Self::TarLzip => Some(("/usr/bin/lzip", &[])),
+ Self::TarXz => Some(("/usr/bin/xz", &[])),
+ Self::TarZstd => Some(("/usr/bin/zstd", &["-T0"])),
+ _ => None,
+ }
+ }
+}
+
+pub async fn serve(
+ context: &super::shared::Context,
+ route: crate::router::RepoRoute<crate::router::RevisionPath>,
+ format: Format,
+ method: axum::http::Method,
+) -> axum::response::Response {
+ if method != axum::http::Method::GET && method != axum::http::Method::HEAD {
+ return super::method_not_allowed();
+ }
+ let repositories = context.repositories;
+ let name = route.repo.clone();
+ let revision = route.params.rev.clone();
+ let path = route.params.path.clone();
+ let model = tokio::task::spawn_blocking(move || {
+ crate::models::archive::Archive::load(
+ std::path::Path::new(repositories),
+ &name,
+ &revision,
+ path.as_deref(),
+ )
+ })
+ .await;
+ let model = match model {
+ Ok(Ok(model)) => model,
+ Ok(Err(error)) => return super::error(error),
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ let bytes = match generate(&model, route.params.path.as_deref(), format).await {
+ Ok(bytes) => bytes,
+ Err(error) => return super::error(crate::models::Error::Internal(error)),
+ };
+ let filename = format!("{}.{}", model.prefix, format.extension());
+ super::bytes_response(
+ format.content_type(),
+ Some(content_disposition(&filename)),
+ Some(format!("\"{}\"", model.oid)),
+ bytes,
+ &method,
+ )
+}
+
+async fn generate(
+ model: &crate::models::archive::Archive,
+ path: Option<&str>,
+ format: Format,
+) -> Result<Vec<u8>, String> {
+ let mut command = tokio::process::Command::new(crate::GIT);
+ command
+ .arg("--git-dir")
+ .arg(&model.repository_path)
+ .arg("archive")
+ .arg(format!("--format={}", format.git_format()))
+ .arg(format!("--prefix={}/", model.prefix))
+ .arg(&model.oid)
+ .env("GIT_CONFIG_GLOBAL", "/dev/null")
+ .env("GIT_CONFIG_NOSYSTEM", "1");
+ if let Some(path) = path {
+ command.arg("--").arg(format!(":(literal){path}"));
+ }
+ let output = command.output().await.map_err(|error| error.to_string())?;
+ if !output.status.success() {
+ return Err(format!(
+ "git archive failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ let Some((program, arguments)) = format.compressor() else {
+ return Ok(output.stdout);
+ };
+ let mut child = tokio::process::Command::new(program)
+ .args(arguments)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .map_err(|error| error.to_string())?;
+ let mut input = child.stdin.take().expect("compressor stdin is piped");
+ let bytes = output.stdout;
+ let writer = tokio::spawn(async move {
+ input.write_all(&bytes).await?;
+ input.shutdown().await
+ });
+ let output = child
+ .wait_with_output()
+ .await
+ .map_err(|error| error.to_string())?;
+ writer
+ .await
+ .map_err(|error| error.to_string())?
+ .map_err(|error| error.to_string())?;
+ if output.status.success() {
+ Ok(output.stdout)
+ } else {
+ Err(format!(
+ "archive compressor failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ ))
+ }
+}
+
+fn content_disposition(filename: &str) -> String {
+ let fallback = filename
+ .chars()
+ .map(|character| {
+ if character.is_ascii_alphanumeric() || matches!(character, '.' | '-' | '_') {
+ character
+ } else {
+ '_'
+ }
+ })
+ .collect::<String>();
+ let encoded =
+ percent_encoding::utf8_percent_encode(filename, percent_encoding::NON_ALPHANUMERIC);
+ format!("inline; filename=\"{fallback}\"; filename*=UTF-8''{encoded}")
+}
diff --git a/crates/gilti/src/views/diff.rs b/crates/gilti/src/views/diff.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/views/diff.rs
@@ -0,0 +1,456 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use maud::{Markup, html};
+
+#[derive(Clone, Copy, Eq, PartialEq)]
+pub enum Mode {
+ Unified,
+ SideBySide,
+ StatOnly,
+}
+
+#[derive(Clone, Copy)]
+pub struct Query {
+ pub context: u32,
+ pub ignore_whitespace: bool,
+ pub mode: Mode,
+}
+
+impl Query {
+ pub fn from_request(query: &crate::RequestQuery) -> Result<Self, ()> {
+ let context = query
+ .value("GILTI_QUERY_CONTEXT")
+ .map(str::parse)
+ .transpose()
+ .map_err(|_| ())?
+ .unwrap_or(3);
+ let context = if context == 0 { 3 } else { context };
+ if context > 40 {
+ return Err(());
+ }
+ let ignore_whitespace = match query.value("GILTI_QUERY_IGNOREWS") {
+ None | Some("0") => false,
+ Some("1") => true,
+ _ => return Err(()),
+ };
+ let mode = match query.value("GILTI_QUERY_DIFFTYPE") {
+ None | Some("0") => Mode::Unified,
+ Some("1") => Mode::SideBySide,
+ Some("2") => Mode::StatOnly,
+ _ => return Err(()),
+ };
+ Ok(Self {
+ context,
+ ignore_whitespace,
+ mode,
+ })
+ }
+
+ pub fn options(self) -> crate::models::diff::Options {
+ crate::models::diff::Options {
+ context: self.context,
+ ignore_whitespace: self.ignore_whitespace,
+ }
+ }
+}
+
+pub async fn serve(
+ context: &super::shared::Context,
+ route: crate::router::RepoRoute<crate::router::Comparison>,
+ query: Query,
+ raw: bool,
+ method: axum::http::Method,
+) -> axum::response::Response {
+ if method != axum::http::Method::GET && method != axum::http::Method::HEAD {
+ return super::method_not_allowed();
+ }
+ let repositories = context.repositories;
+ let name = route.repo.clone();
+ let old = route.params.old_rev.clone();
+ let new = route.params.new_rev.clone();
+ let path = route.params.path.clone();
+ let model = tokio::task::spawn_blocking(move || {
+ crate::models::diff::Diff::load(
+ std::path::Path::new(repositories),
+ &name,
+ Some(old),
+ new,
+ path,
+ query.options(),
+ )
+ })
+ .await;
+ let model = match model {
+ Ok(Ok(model)) => model,
+ Ok(Err(error)) => return super::error(error),
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ if raw {
+ return raw_response(context, &route, &model, query, &method).await;
+ }
+ let revision = model.new_revision.clone();
+ let content = content(&model, query, route.params.path.as_deref(), true);
+ super::shared::render(
+ context,
+ &model.repository,
+ &revision,
+ super::shared::Page::Diff,
+ content,
+ &method,
+ )
+}
+
+pub fn content(
+ model: &crate::models::diff::Diff,
+ query: Query,
+ path: Option<&str>,
+ controls: bool,
+) -> Markup {
+ html! {
+ @if controls { (content_controls(query)) }
+ div class="diffstat-header" {
+ "Diffstat"
+ @if let Some(path) = path { " (limited to '" (path) "')" }
+ }
+ table summary="diffstat" class="diffstat" {
+ @for file in &model.files { (file_stat(model, file)) }
+ }
+ div class="diffstat-summary" {
+ (model.files.len()) " files changed, " (model.additions) " insertions, " (model.deletions) " deletions"
+ }
+ @if query.mode != Mode::StatOnly {
+ @if query.mode == Mode::SideBySide {
+ table summary="ssdiff" class="ssdiff" {
+ @for file in &model.files { (side_file(model, file)) }
+ }
+ } @else {
+ table summary="diff" class="diff" { tr { td {
+ @for file in &model.files { (unified_file(model, file)) }
+ } } }
+ }
+ }
+ }
+}
+
+pub fn content_controls(query: Query) -> Markup {
+ html! { div class="cgit-panel" {
+ b { "diff options" }
+ form method="get" { table {
+ tr { td colspan="2" {} }
+ tr { td class="label" { "context:" } td class="ctrl" {
+ select name="context" onchange="this.form.submit();" {
+ @for value in [1_u32,2,3,4,5,6,7,8,9,10,15,20,25,30,35,40] {
+ option value=(value) selected[query.context == value] { (value) }
+ }
+ }
+ } }
+ tr { td class="label" { "space:" } td class="ctrl" {
+ select name="ignorews" onchange="this.form.submit();" {
+ option value="0" selected[!query.ignore_whitespace] { "include" }
+ option value="1" selected[query.ignore_whitespace] { "ignore" }
+ }
+ } }
+ tr { td class="label" { "mode:" } td class="ctrl" {
+ select name="dt" onchange="this.form.submit();" {
+ option value="0" selected[query.mode == Mode::Unified] { "unified" }
+ option value="1" selected[query.mode == Mode::SideBySide] { "ssdiff" }
+ option value="2" selected[query.mode == Mode::StatOnly] { "stat only" }
+ }
+ } }
+ tr { td {} td class="ctrl" { noscript { input type="submit" value="reload"; } } }
+ } }
+ } }
+}
+
+fn file_stat(model: &crate::models::diff::Diff, file: &crate::models::diff::File) -> Markup {
+ let path = if file.new_path.is_empty() {
+ &file.old_path
+ } else {
+ &file.new_path
+ };
+ let repo = super::shared::repository_url(&model.repository.name);
+ let old = super::shared::encode_path(model.old_revision.as_deref().unwrap_or("HEAD"));
+ let new = super::shared::encode_path(&model.new_revision);
+ let path_url = super::shared::encode_path(path);
+ let class = status_class(file.status);
+ html! { tr {
+ td class="mode" {
+ (super::tree::filemode(if file.new_mode == 0 { file.old_mode } else { file.new_mode }))
+ @if file.old_mode != 0 && file.new_mode != 0 && file.old_mode != file.new_mode {
+ span class="modechange" { "[" (super::tree::filemode(file.old_mode)) "]" }
+ }
+ }
+ td class=(class) {
+ a href=(format!("{repo}/+/diff/{old}..{new}/+/{path_url}")) { (path) }
+ @if matches!(file.status, git2::Delta::Renamed | git2::Delta::Copied) {
+ " (" @if file.status == git2::Delta::Copied { "copied" } @else { "renamed" }
+ " from " (&file.old_path) ")"
+ }
+ }
+ td class="right" {
+ @if file.binary { "bin" } @else { (file.additions + file.deletions) }
+ }
+ td class="graph" {
+ @if file.binary { (file.old_size) " -> " (file.new_size) " bytes" }
+ @else { "+" (file.additions) " −" (file.deletions) }
+ }
+ } }
+}
+
+fn unified_file(model: &crate::models::diff::Diff, file: &crate::models::diff::File) -> Markup {
+ html! {
+ (file_header(model, file))
+ @if file.binary { div class="ctx" { "Binary files differ" } }
+ @for hunk in &file.hunks {
+ div class="hunk" { (&hunk.header) }
+ @for line in &hunk.lines {
+ div class=(line_class(line.origin)) {
+ @if matches!(line.origin, '+' | '-' | ' ') { (line.origin) }
+ (&line.content)
+ }
+ }
+ }
+ }
+}
+
+fn side_file(model: &crate::models::diff::Diff, file: &crate::models::diff::File) -> Markup {
+ html! {
+ tr { td colspan="4" { (file_header(model, file)) } }
+ @if file.binary { tr { td colspan="4" { "Binary files differ" } } }
+ @for hunk in &file.hunks {
+ tr { td colspan="4" class="hunk" { (&hunk.header) } }
+ @for (old, new) in side_rows(hunk) { tr {
+ td class="lineno" { @if let Some(line) = old { (line.old_line.unwrap_or_default()) } }
+ td class=[old.map(|line| line_class(line.origin))] { (side_content(old, new, true)) }
+ td class="lineno" { @if let Some(line) = new { (line.new_line.unwrap_or_default()) } }
+ td class=[new.map(|line| line_class(line.origin))] { (side_content(new, old, false)) }
+ } }
+ }
+ }
+}
+
+fn side_rows(
+ hunk: &crate::models::diff::Hunk,
+) -> Vec<(
+ Option<&crate::models::diff::Line>,
+ Option<&crate::models::diff::Line>,
+)> {
+ let mut rows = Vec::new();
+ let mut removed = Vec::new();
+ let mut added = Vec::new();
+ let flush = |rows: &mut Vec<_>, removed: &mut Vec<_>, added: &mut Vec<_>| {
+ let count = removed.len().max(added.len());
+ for index in 0..count {
+ rows.push((removed.get(index).copied(), added.get(index).copied()));
+ }
+ removed.clear();
+ added.clear();
+ };
+ for line in &hunk.lines {
+ match line.origin {
+ '-' => removed.push(line),
+ '+' => added.push(line),
+ _ => {
+ flush(&mut rows, &mut removed, &mut added);
+ rows.push((Some(line), Some(line)));
+ }
+ }
+ }
+ flush(&mut rows, &mut removed, &mut added);
+ rows
+}
+
+fn side_content(
+ line: Option<&crate::models::diff::Line>,
+ other: Option<&crate::models::diff::Line>,
+ old: bool,
+) -> Markup {
+ let Some(line) = line else {
+ return html! {};
+ };
+ let paired_change =
+ other.is_some_and(|other| matches!((line.origin, other.origin), ('-', '+') | ('+', '-')));
+ if !paired_change {
+ return html! { (&line.content) };
+ }
+ let other = &other.expect("paired line exists").content;
+ let common = if old {
+ common_subsequence(&line.content, other)
+ } else {
+ common_subsequence(other, &line.content)
+ };
+ let segments = highlighted_segments(&line.content, &common);
+ let class = if old { "del" } else { "add" };
+ html! {
+ @for (changed, text) in segments {
+ @if changed { span class=(class) { (text) } } @else { (text) }
+ }
+ }
+}
+
+fn common_subsequence(old: &str, new: &str) -> Vec<char> {
+ let old = old.chars().collect::<Vec<_>>();
+ let new = new.chars().collect::<Vec<_>>();
+ if old.len() >= 200 || new.len() >= 200 {
+ return Vec::new();
+ }
+ let mut lengths = vec![vec![0_usize; new.len() + 1]; old.len() + 1];
+ for left in (0..old.len()).rev() {
+ for right in (0..new.len()).rev() {
+ lengths[left][right] = if old[left] == new[right] {
+ lengths[left + 1][right + 1] + 1
+ } else {
+ lengths[left + 1][right].max(lengths[left][right + 1])
+ };
+ }
+ }
+ let (mut left, mut right) = (0, 0);
+ let mut common = Vec::with_capacity(lengths[0][0]);
+ while left < old.len() && right < new.len() {
+ if old[left] == new[right] {
+ common.push(old[left]);
+ left += 1;
+ right += 1;
+ } else if lengths[left + 1][right] >= lengths[left][right + 1] {
+ left += 1;
+ } else {
+ right += 1;
+ }
+ }
+ common
+}
+
+fn highlighted_segments(value: &str, common: &[char]) -> Vec<(bool, String)> {
+ let mut common = common.iter().copied().peekable();
+ let mut segments = Vec::<(bool, String)>::new();
+ for character in value.chars() {
+ let changed = common.peek().copied() != Some(character);
+ if !changed {
+ common.next();
+ }
+ if let Some((last_changed, text)) = segments.last_mut()
+ && *last_changed == changed
+ {
+ text.push(character);
+ continue;
+ }
+ segments.push((changed, character.to_string()));
+ }
+ segments
+}
+
+fn file_header(model: &crate::models::diff::Diff, file: &crate::models::diff::File) -> Markup {
+ let old = if file.old_path.is_empty() {
+ "/dev/null"
+ } else {
+ &file.old_path
+ };
+ let new = if file.new_path.is_empty() {
+ "/dev/null"
+ } else {
+ &file.new_path
+ };
+ html! { div class="head" {
+ "diff --git a/" (old) " b/" (new)
+ @if file.old_mode == 0 { br; "new file mode " (format!("{:06o}", file.new_mode)) }
+ @if file.new_mode == 0 { br; "deleted file mode " (format!("{:06o}", file.old_mode)) }
+ @if let (Some(old_oid), Some(new_oid)) = (&file.old_oid, &file.new_oid) {
+ br; "index " (&old_oid[..old_oid.len().min(7)]) ".." (&new_oid[..new_oid.len().min(7)])
+ @if file.old_mode != 0 && file.new_mode != 0 {
+ " " (format!("{:06o}", file.old_mode))
+ @if file.old_mode != file.new_mode { ".." (format!("{:06o}", file.new_mode)) }
+ }
+ }
+ br; "--- " @if file.old_mode == 0 { "/dev/null" } @else { "a/" (old) }
+ br; "+++ " @if file.new_mode == 0 { "/dev/null" } @else { "b/" (new) }
+ @if model.old_oid.is_none() { "" }
+ } }
+}
+
+fn line_class(origin: char) -> &'static str {
+ match origin {
+ '+' => "add",
+ '-' => "del",
+ '@' => "hunk",
+ _ => "ctx",
+ }
+}
+
+fn status_class(status: git2::Delta) -> &'static str {
+ match status {
+ git2::Delta::Added => "add",
+ git2::Delta::Copied => "cpy",
+ git2::Delta::Deleted => "del",
+ git2::Delta::Modified => "upd",
+ git2::Delta::Renamed => "mov",
+ git2::Delta::Typechange => "typ",
+ git2::Delta::Conflicted => "stg",
+ _ => "unk",
+ }
+}
+
+async fn raw_response(
+ context: &super::shared::Context,
+ route: &crate::router::RepoRoute<crate::router::Comparison>,
+ model: &crate::models::diff::Diff,
+ query: Query,
+ method: &axum::http::Method,
+) -> axum::response::Response {
+ let repository = match crate::models::repository::path(
+ std::path::Path::new(context.repositories),
+ &route.repo,
+ ) {
+ Ok(repository) => repository,
+ Err(error) => return super::error(error),
+ };
+ let mut command = tokio::process::Command::new(crate::GIT);
+ command
+ .arg("--git-dir")
+ .arg(repository)
+ .arg("-c")
+ .arg("color.ui=false")
+ .arg("diff")
+ .arg("--no-ext-diff")
+ .arg("--no-textconv")
+ .arg("--no-renames")
+ .arg(format!("--unified={}", query.context));
+ if query.ignore_whitespace {
+ command.arg("--ignore-all-space");
+ }
+ command
+ .arg(model.old_oid.as_deref().unwrap_or(""))
+ .arg(&model.new_oid);
+ if let Some(path) = &route.params.path {
+ command.arg("--").arg(path);
+ }
+ let output = match command.output().await {
+ Ok(output) if output.status.success() => output.stdout,
+ Ok(output) => {
+ eprintln!(
+ "gilti: git diff failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ return super::error(crate::models::Error::Internal("git diff failed".to_owned()));
+ }
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ super::bytes_response("text/plain; charset=UTF-8", None, None, output, method)
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn highlights_side_by_side_character_changes() {
+ let common = super::common_subsequence("alpha", "aloha");
+ assert_eq!(common, ['a', 'l', 'h', 'a']);
+ assert_eq!(
+ super::highlighted_segments("alpha", &common),
+ [
+ (false, "al".to_owned()),
+ (true, "p".to_owned()),
+ (false, "ha".to_owned()),
+ ]
+ );
+ }
+}
diff --git a/crates/gilti/src/views/mod.rs b/crates/gilti/src/views/mod.rs
index 177a25b..3cb192d 100644
--- a/crates/gilti/src/views/mod.rs
+++ b/crates/gilti/src/views/mod.rs
@@ -2,13 +2,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod about;
+pub mod archive;
pub mod archive_signature;
pub mod blame;
+pub mod diff;
pub mod object;
pub mod overview;
+pub mod patch;
pub mod refs;
pub mod repositories;
+pub mod revision;
pub mod shared;
+pub mod stats;
pub mod tag;
pub mod tree;
@@ -20,6 +25,40 @@ fn method_not_allowed() -> axum::response::Response {
.expect("static response is valid")
}
+pub(crate) fn bad_request(message: &'static str) -> axum::response::Response {
+ axum::response::Response::builder()
+ .status(axum::http::StatusCode::BAD_REQUEST)
+ .header(axum::http::header::CONTENT_TYPE, "text/plain")
+ .body(axum::body::Body::from(message))
+ .expect("static response is valid")
+}
+
+fn bytes_response(
+ content_type: &'static str,
+ content_disposition: Option<String>,
+ etag: Option<String>,
+ bytes: Vec<u8>,
+ method: &axum::http::Method,
+) -> axum::response::Response {
+ let length = bytes.len();
+ let body = if method == axum::http::Method::HEAD {
+ axum::body::Body::empty()
+ } else {
+ axum::body::Body::from(bytes)
+ };
+ let mut response = axum::response::Response::builder()
+ .status(axum::http::StatusCode::OK)
+ .header(axum::http::header::CONTENT_TYPE, content_type)
+ .header(axum::http::header::CONTENT_LENGTH, length);
+ if let Some(value) = content_disposition {
+ response = response.header(axum::http::header::CONTENT_DISPOSITION, value);
+ }
+ if let Some(value) = etag {
+ response = response.header(axum::http::header::ETAG, value);
+ }
+ response.body(body).expect("byte response is valid")
+}
+
fn error(error: crate::models::Error) -> axum::response::Response {
let (status, message) = match error {
crate::models::Error::NotFound => (axum::http::StatusCode::NOT_FOUND, "not found\n"),
diff --git a/crates/gilti/src/views/patch.rs b/crates/gilti/src/views/patch.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/views/patch.rs
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pub async fn serve(
+ context: &super::shared::Context,
+ route: crate::router::RepoRoute<crate::router::Comparison>,
+ method: axum::http::Method,
+) -> axum::response::Response {
+ if method != axum::http::Method::GET && method != axum::http::Method::HEAD {
+ return super::method_not_allowed();
+ }
+ let repositories = context.repositories;
+ let name = route.repo.clone();
+ let old = route.params.old_rev.clone();
+ let new = route.params.new_rev.clone();
+ let model = tokio::task::spawn_blocking(move || {
+ crate::models::patch::Patch::load(std::path::Path::new(repositories), &name, &old, &new)
+ })
+ .await;
+ let model = match model {
+ Ok(Ok(model)) => model,
+ Ok(Err(error)) => return super::error(error),
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ let mut command = tokio::process::Command::new(crate::GIT);
+ command
+ .arg("--git-dir")
+ .arg(&model.repository_path)
+ .arg("-c")
+ .arg("color.ui=false")
+ .arg("format-patch")
+ .arg("--stdout")
+ .arg("--keep-subject")
+ .arg("--no-renames")
+ .arg("--signature=Gilti")
+ .arg(format!("{}..{}", model.old_oid, model.new_oid))
+ .env("GIT_CONFIG_GLOBAL", "/dev/null")
+ .env("GIT_CONFIG_NOSYSTEM", "1");
+ if let Some(path) = &route.params.path {
+ command.arg("--").arg(format!(":(literal){path}"));
+ }
+ let output = match command.output().await {
+ Ok(output) if output.status.success() => output.stdout,
+ Ok(output) => {
+ eprintln!(
+ "gilti: git format-patch failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ return super::error(crate::models::Error::Internal(
+ "git format-patch failed".to_owned(),
+ ));
+ }
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ let filename = format!("{}..{}.patch", model.old_oid, model.new_oid);
+ super::bytes_response(
+ "text/plain; charset=UTF-8",
+ Some(format!("inline; filename=\"{filename}\"")),
+ None,
+ output,
+ &method,
+ )
+}
diff --git a/crates/gilti/src/views/revision.rs b/crates/gilti/src/views/revision.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/views/revision.rs
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use maud::{Markup, html};
+
+pub async fn serve(
+ context: &super::shared::Context,
+ route: crate::router::RepoRoute<crate::router::Revision>,
+ query: super::diff::Query,
+ method: axum::http::Method,
+) -> axum::response::Response {
+ if method != axum::http::Method::GET && method != axum::http::Method::HEAD {
+ return super::method_not_allowed();
+ }
+ let repositories = context.repositories;
+ let name = route.repo.clone();
+ let revision = route.params.clone();
+ let model = tokio::task::spawn_blocking(move || {
+ crate::models::commit::Commit::load(
+ std::path::Path::new(repositories),
+ &name,
+ revision,
+ query.options(),
+ )
+ })
+ .await;
+ let model = match model {
+ Ok(Ok(model)) => model,
+ Ok(Err(error)) => return super::error(error),
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ let content = content(&model, query);
+ super::shared::render_titled(
+ context,
+ &model.repository,
+ &model.revision,
+ super::shared::Page::Revision,
+ Some(&model.subject),
+ content,
+ &method,
+ )
+}
+
+fn content(model: &crate::models::commit::Commit, query: super::diff::Query) -> Markup {
+ let repo = super::shared::repository_url(&model.repository.name);
+ let revision = super::shared::encode_path(&model.revision);
+ html! {
+ (super::diff::content_controls(query))
+ table summary="commit info" class="commit-info" {
+ tr { th { "author" } td { (&model.author.name) " <" (&model.author.email) ">" }
+ td class="right" { (timestamp(&model.author)) } }
+ tr { th { "committer" } td { (&model.committer.name) " <" (&model.committer.email) ">" }
+ td class="right" { (timestamp(&model.committer)) } }
+ tr { th { "commit" } td colspan="2" class="oid" {
+ a href=(format!("{repo}/+/{}", model.oid)) { (&model.oid) }
+ @let patch_old = model.parents.first().map_or("HEAD", String::as_str);
+ " (" a href=(format!("{repo}/+/patch/{patch_old}..{}", model.oid)) { "patch" } ")"
+ } }
+ tr { th { "tree" } td colspan="2" class="oid" {
+ a href=(format!("{repo}/+/{revision}/+/tree")) { (&model.tree) }
+ } }
+ @for parent in &model.parents {
+ tr { th { "parent" } td colspan="2" class="oid" {
+ a href=(format!("{repo}/+/{parent}")) { (parent) }
+ " (" a href=(format!("{repo}/+/diff/{parent}..{}", model.oid)) { "diff" } ")"
+ } }
+ }
+ tr { th { "download" } td colspan="2" class="oid" {
+ @for format in ["tar", "tar.gz", "tar.bz2", "tar.lz", "tar.xz", "tar.zst", "zip"] {
+ a href=(format!("{repo}/+/{revision}/+/archive?format={format}")) { (format) } br;
+ }
+ } }
+ }
+ div class="commit-subject" {
+ (&model.subject)
+ @for decoration in &model.decorations {
+ " ("
+ @if let Some(reference) = &decoration.reference {
+ @let reference = super::shared::encode_path(reference);
+ a href=(format!("{repo}/+/{reference}")) { (&decoration.label) }
+ } @else { (&decoration.label) }
+ ")"
+ }
+ }
+ div class="commit-msg" { (&model.message) }
+ @if let Some(notes) = &model.notes {
+ div class="notes-header" { "Notes" }
+ div class="notes" { (notes) }
+ div class="notes-footer" {}
+ }
+ @if let Some(diff) = &model.diff {
+ (super::diff::content(diff, query, None, false))
+ }
+ }
+}
+
+fn timestamp(identity: &crate::models::commit::Identity) -> String {
+ let adjusted = identity.timestamp + i64::from(identity.offset_minutes) * 60;
+ let mut value = std::mem::MaybeUninit::<libc::tm>::uninit();
+ // SAFETY: both pointers are valid for the duration of the call.
+ let value = unsafe {
+ libc::gmtime_r(&adjusted, value.as_mut_ptr());
+ value.assume_init()
+ };
+ let sign = if identity.offset_minutes < 0 {
+ '-'
+ } else {
+ '+'
+ };
+ let offset = identity.offset_minutes.unsigned_abs();
+ format!(
+ "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {sign}{:02}{:02}",
+ value.tm_year + 1900,
+ value.tm_mon + 1,
+ value.tm_mday,
+ value.tm_hour,
+ value.tm_min,
+ value.tm_sec,
+ offset / 60,
+ offset % 60
+ )
+}
diff --git a/crates/gilti/src/views/shared.rs b/crates/gilti/src/views/shared.rs
index 5e429c6..198ecf7 100644
--- a/crates/gilti/src/views/shared.rs
+++ b/crates/gilti/src/views/shared.rs
@@ -31,9 +31,29 @@ pub fn render(
content: Markup,
method: &axum::http::Method,
) -> axum::response::Response {
+ render_titled(context, repository, revision, active, None, content, method)
+}
+
+pub fn render_titled(
+ context: &Context,
+ repository: &crate::models::repository::Info,
+ revision: &str,
+ active: Page,
+ page_title: Option<&str>,
+ content: Markup,
+ method: &axum::http::Method,
+) -> axum::response::Response {
let repo = repository_url(&repository.name);
let rev = encode_path(revision);
- let title = format!("{} - {}", repository.name, repository.description);
+ let title = page_title.map_or_else(
+ || format!("{} - {}", repository.name, repository.description),
+ |page_title| {
+ format!(
+ "{page_title} - {} - {}",
+ repository.name, repository.description
+ )
+ },
+ );
let document = html! {
(DOCTYPE)
html lang="en" {
@@ -54,7 +74,9 @@ pub fn render(
tr { td class="sub" { (&repository.description) } }
}
table class="tabs" { tr { td {
- (tab(&format!("{repo}/+/about"), "about", active == Page::About))
+ @if repository.has_readme {
+ (tab(&format!("{repo}/+/about"), "about", active == Page::About))
+ }
(tab(&repo, "summary", active == Page::Summary))
(tab(&format!("{repo}/+/refs"), "refs", active == Page::Refs))
(tab(&format!("{repo}/+/{rev}/+/log"), "log", active == Page::Log))
diff --git a/crates/gilti/src/views/stats.rs b/crates/gilti/src/views/stats.rs
new file mode 100644
--- /dev/null
+++ b/crates/gilti/src/views/stats.rs
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+use maud::{Markup, html};
+
+pub struct Query {
+ pub period: crate::models::stats::Period,
+ pub code: &'static str,
+ pub top: Option<usize>,
+}
+
+pub enum QueryError {
+ BadRequest,
+ NotFound,
+}
+
+impl Query {
+ pub fn from_request(query: &crate::RequestQuery) -> Result<Self, QueryError> {
+ let (period, code) = match query.value("GILTI_QUERY_PERIOD") {
+ None | Some("w" | "week") => (crate::models::stats::Period::Week, "w"),
+ Some("m" | "month") => (crate::models::stats::Period::Month, "m"),
+ Some("q" | "quarter") => (crate::models::stats::Period::Quarter, "q"),
+ Some("y" | "year") => (crate::models::stats::Period::Year, "y"),
+ Some(_) => return Err(QueryError::NotFound),
+ };
+ let top = match query.value("GILTI_QUERY_OFFSET") {
+ None | Some("0") => Some(10),
+ Some("-1") => None,
+ Some(value) => Some(
+ value
+ .parse::<usize>()
+ .ok()
+ .filter(|value| *value > 0)
+ .ok_or(QueryError::BadRequest)?,
+ ),
+ };
+ Ok(Self { period, code, top })
+ }
+}
+
+pub async fn serve(
+ context: &super::shared::Context,
+ route: crate::router::RepoRoute<()>,
+ query: Query,
+ method: axum::http::Method,
+) -> axum::response::Response {
+ if method != axum::http::Method::GET && method != axum::http::Method::HEAD {
+ return super::method_not_allowed();
+ }
+ let repositories = context.repositories;
+ let name = route.repo;
+ let model = tokio::task::spawn_blocking(move || {
+ crate::models::stats::Stats::load(std::path::Path::new(repositories), &name, query.period)
+ })
+ .await;
+ let model = match model {
+ Ok(Ok(model)) => model,
+ Ok(Err(error)) => return super::error(error),
+ Err(error) => return super::error(crate::models::Error::Internal(error.to_string())),
+ };
+ let content = content(&model, &query);
+ super::shared::render(
+ context,
+ &model.repository,
+ "HEAD",
+ super::shared::Page::Stats,
+ content,
+ &method,
+ )
+}
+
+fn content(model: &crate::models::stats::Stats, query: &Query) -> Markup {
+ let shown = query
+ .top
+ .unwrap_or(model.authors.len())
+ .min(model.authors.len());
+ html! {
+ div class="cgit-panel" {
+ b { "stat options" }
+ form method="get" { table {
+ tr { td colspan="2" {} }
+ tr { td class="label" { "Period:" } td class="ctrl" {
+ select name="period" onchange="this.form.submit();" {
+ @for (code, name) in [("w","week"),("m","month"),("q","quarter"),("y","year")] {
+ option value=(code) selected[query.code == code] { (name) }
+ }
+ }
+ } }
+ tr { td class="label" { "Authors:" } td class="ctrl" {
+ select name="ofs" onchange="this.form.submit();" {
+ @for value in [10_usize,25,50,100] {
+ option value=(value) selected[query.top == Some(value)] { (value) }
+ }
+ option value="-1" selected[query.top.is_none()] { "all" }
+ }
+ } }
+ tr { td {} td class="ctrl" { noscript { input type="submit" value="Reload"; } } }
+ } }
+ }
+ h2 { "Commits per author per " (query.period.name()) }
+ table class="stats" {
+ tr { th { "Author" } @for label in &model.labels { th { (label) } } th { "Total" } }
+ @for author in &model.authors[..shown] {
+ tr { td class="left" { (&author.name) }
+ @for count in &author.counts { td { (count) } }
+ td class="sum" { (author.total) }
+ }
+ }
+ @if shown < model.authors.len() {
+ @let others = &model.authors[shown..];
+ tr { td class="left" { "Others (" (others.len()) ")" }
+ @for index in 0..4 { td { (others.iter().map(|author| author.counts[index]).sum::<usize>()) } }
+ td class="sum" { (others.iter().map(|author| author.total).sum::<usize>()) }
+ }
+ }
+ tr { td class="total" { "Total" }
+ @for total in &model.totals { td class="sum" { (total) } }
+ td class="sum" { (model.totals.iter().sum::<usize>()) }
+ }
+ }
+ }
+}
diff --git a/crates/gilti/src/views/tree.rs b/crates/gilti/src/views/tree.rs
index 503f182..c988d67 100644
--- a/crates/gilti/src/views/tree.rs
+++ b/crates/gilti/src/views/tree.rs
@@ -103,7 +103,7 @@ fn html_content(model: &crate::models::tree::Tree) -> Markup {
}
}
-fn filemode(mode: u32) -> String {
+pub(super) fn filemode(mode: u32) -> String {
let mut value = String::with_capacity(10);
value.push(match mode {
0o040000 => 'd',
diff --git a/tests/smoke.sh b/tests/smoke.sh
index 460d79a..e883e20 100755
--- a/tests/smoke.sh
+++ b/tests/smoke.sh
@@ -247,6 +247,22 @@ until curl -fsS "http://127.0.0.1:$http_port/" | grep -q 'testing'; do
done
curl -fsS "http://127.0.0.1:$http_port/testing" | grep -q 'Initial commit'
curl -fsS "http://127.0.0.1:$http_port/testing/+/HEAD/+/tree/README%2emd" | grep -q 'Testing'
+old_commit=$(git -C "$work/testing-clone" rev-parse HEAD^)
+new_commit=$(git -C "$work/testing-clone" rev-parse HEAD)
+curl -fsS "http://127.0.0.1:$http_port/testing/+/$new_commit" | grep -q 'Push with second key'
+curl -fsS "http://127.0.0.1:$http_port/testing/+/stats" | grep -q 'Gilti smoke test'
+curl -fsS "http://127.0.0.1:$http_port/testing/+/diff/$old_commit..$new_commit?format=raw" |
+ grep -q '^+Second key can push\.'
+curl -fsS "http://127.0.0.1:$http_port/testing/+/patch/$old_commit..$new_commit/+/README%2emd" |
+ grep -q '^Subject: Push with second key'
+for format in tar tar.gz tar.bz2 tar.lz tar.xz tar.zst zip; do
+ curl -fsS "http://127.0.0.1:$http_port/testing/+/HEAD/+/archive?format=$format" \
+ -o "$work/testing.$format"
+ [ -s "$work/testing.$format" ] || {
+ echo "empty $format archive" >&2
+ exit 1
+ }
+done
summary_status=$(curl -sS -o /dev/null -w '%{http_code}:%{redirect_url}' \
"http://127.0.0.1:$http_port/testing/+/summary")
[ "$summary_status" = "308:http://127.0.0.1:$http_port/testing" ] || {
@@ -264,7 +280,7 @@ printf '%s' "$lfs_response" | grep -q '"code":404' || {
echo 'unexpected LFS batch response' >&2
exit 1
}
-cache_url="http://127.0.0.1:$http_port/testing"
+cache_url="http://127.0.0.1:$http_port/testing/+/HEAD/+/log"
curl -fsS -D "$work/cache-1.headers" -o /dev/null "$cache_url"
sleep 2
curl -fsS -D "$work/cache-2.headers" -o /dev/null "$cache_url"