diff options
Diffstat
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, ¬es, 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(¬es); - 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(¬es_ref, "refs/notes/signatures/%s", - f->suffix + 1); - - init_notes(tree, notes_ref.buf, combine_notes_ignore, 0); - strbuf_release(¬es_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(×tamp, 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" |
