summaryrefslogtreecommitdiff
path: root/cgit/ui-summary.c
diff options
context:
space:
mode:
Diffstat (limited to 'cgit/ui-summary.c')
-rw-r--r--cgit/ui-summary.c418
1 files changed, 418 insertions, 0 deletions
diff --git a/cgit/ui-summary.c b/cgit/ui-summary.c
new file mode 100644
index 0000000000..2c3db14a9e
--- /dev/null
+++ b/cgit/ui-summary.c
@@ -0,0 +1,418 @@
+/* ui-summary.c: functions for generating repo summary page
+ *
+ * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
+ * Copyright (C) 2026 Project Tick
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-summary.h"
+#include "html.h"
+#include "ui-blob.h"
+#include "ui-log.h"
+#include "ui-plain.h"
+#include "ui-refs.h"
+#include "ui-shared.h"
+
+static int urls;
+#define MAX_METADATA_BYTES (64 * 1024)
+
+static char *trim_whitespace(const char *start, const char *end)
+{
+ while (start < end && isspace((unsigned char)*start))
+ start++;
+ while (end > start && isspace((unsigned char)end[-1]))
+ end--;
+ if (end <= start)
+ return NULL;
+ return xstrndup(start, end - start);
+}
+
+static char *find_spdx_identifier(const char *buf, size_t size)
+{
+ static const char *keys[] = {
+ "SPDX-License-Identifier:",
+ "SPDX-License-Expression:",
+ };
+ const char *line = buf;
+ const char *end = buf + size;
+
+ while (line && line < end) {
+ const char *eol = strchrnul(line, '\n');
+ size_t len = eol - line;
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(keys); i++) {
+ const char *value;
+
+ if (len < strlen(keys[i]))
+ continue;
+ if (!skip_prefix(line, keys[i], &value))
+ continue;
+ return trim_whitespace(value, eol);
+ }
+
+ if (!*eol)
+ break;
+ line = eol + 1;
+ }
+ return NULL;
+}
+
+static char *detect_spdx_for_path(const char *path)
+{
+ char *buf = NULL;
+ unsigned long size = 0;
+ char *spdx = NULL;
+
+ if (!path || !ctx.qry.head)
+ return NULL;
+ if (cgit_ref_read_file(path, ctx.qry.head, 1, &buf, &size))
+ return NULL;
+ if (size > MAX_METADATA_BYTES)
+ goto out;
+ spdx = find_spdx_identifier(buf, size);
+
+out:
+ free(buf);
+ return spdx;
+}
+
+static char *find_first_repo_path(const char **candidates, size_t nr)
+{
+ size_t i;
+ const char *ref = ctx.qry.head;
+
+ if (!ref)
+ return NULL;
+
+ for (i = 0; i < nr; i++) {
+ if (cgit_ref_path_exists(candidates[i], ref, 1))
+ return xstrdup(candidates[i]);
+ }
+ return NULL;
+}
+
+static void print_repo_file_link(const char *path)
+{
+ if (!path)
+ return;
+ cgit_tree_link(path, NULL, "ls-blob", ctx.qry.head, ctx.qry.head, path);
+}
+
+static void print_repo_metadata(void)
+{
+ static const char *license_candidates[] = {
+ "LICENSE",
+ "LICENSE.md",
+ "LICENSE.txt",
+ "LICENCE",
+ "LICENCE.md",
+ "LICENCE.txt",
+ "COPYING",
+ "COPYING.md",
+ "COPYING.txt",
+ };
+ static const char *codeowners_candidates[] = {
+ "CODEOWNERS",
+ ".github/CODEOWNERS",
+ "docs/CODEOWNERS",
+ ".gitlab/CODEOWNERS",
+ };
+ static const char *maintainers_candidates[] = {
+ "MAINTAINERS",
+ "MAINTAINERS.md",
+ "MAINTAINERS.txt",
+ "MAINTAINER",
+ "MAINTAINER.md",
+ "MAINTAINER.txt",
+ };
+ char *license_path = find_first_repo_path(license_candidates,
+ ARRAY_SIZE(license_candidates));
+ char *license_spdx = license_path ? detect_spdx_for_path(license_path) : NULL;
+ char *codeowners_path = find_first_repo_path(codeowners_candidates,
+ ARRAY_SIZE(codeowners_candidates));
+ char *maintainers_path = find_first_repo_path(maintainers_candidates,
+ ARRAY_SIZE(maintainers_candidates));
+
+ if (!license_path && !codeowners_path && !maintainers_path)
+ goto cleanup;
+
+ html("<div class='summary-metadata'>");
+ html("<table summary='repository metadata' class='list nowrap' style='width: auto'>");
+ html("<tr class='nohover'>");
+ html("<th class='left' colspan='2'>Metadata</th>");
+ html("</tr>\n");
+
+ if (license_path) {
+ html("<tr><td class='left'>License</td><td class='left'>");
+ if (license_spdx) {
+ html_txt(license_spdx);
+ html(" (");
+ print_repo_file_link(license_path);
+ html(")");
+ } else {
+ print_repo_file_link(license_path);
+ }
+ html("</td></tr>\n");
+ }
+
+ if (codeowners_path) {
+ html("<tr><td class='left'>Codeowners</td><td class='left'>");
+ print_repo_file_link(codeowners_path);
+ html("</td></tr>\n");
+ }
+
+ if (maintainers_path) {
+ html("<tr><td class='left'>Maintainers</td><td class='left'>");
+ print_repo_file_link(maintainers_path);
+ html("</td></tr>\n");
+ }
+
+ html("</table>");
+ html("</div>");
+
+cleanup:
+ free(license_spdx);
+ free(license_path);
+ free(codeowners_path);
+ free(maintainers_path);
+}
+
+static unsigned long ref_age(const struct refinfo *ref)
+{
+ if (!ref || !ref->object)
+ return 0;
+ switch (ref->object->type) {
+ case OBJ_TAG:
+ return ref->tag ? ref->tag->tagger_date : 0;
+ case OBJ_COMMIT:
+ return ref->commit ? ref->commit->committer_date : 0;
+ default:
+ return 0;
+ }
+}
+
+static int cmp_ref_age_desc(const void *a, const void *b)
+{
+ const struct refinfo *r1 = *(const struct refinfo **)a;
+ const struct refinfo *r2 = *(const struct refinfo **)b;
+ unsigned long age1 = ref_age(r1);
+ unsigned long age2 = ref_age(r2);
+
+ if (age1 == age2)
+ return 0;
+ return (age1 > age2) ? -1 : 1;
+}
+
+static char *get_latest_tag_name(void)
+{
+ struct reflist list;
+ char *result = NULL;
+
+ memset(&list, 0, sizeof(list));
+ refs_for_each_tag_ref(get_main_ref_store(the_repository),
+ cgit_refs_cb, &list);
+ if (!list.count)
+ goto cleanup;
+ qsort(list.refs, list.count, sizeof(*list.refs), cmp_ref_age_desc);
+ result = xstrdup(list.refs[0]->refname);
+
+cleanup:
+ cgit_free_reflist_inner(&list);
+ return result;
+}
+
+static void print_repo_badges(void)
+{
+ struct string_list_item *item;
+
+ for_each_string_list_item(item, &ctx.repo->badges) {
+ html("<span class='badge'>");
+ if (item->util) {
+ html("<a href='");
+ html_attr((const char *)item->util);
+ html("'>");
+ }
+ html("<img class='badge' alt='badge' src='");
+ html_attr(item->string);
+ html("'/>");
+ if (item->util)
+ html("</a>");
+ html("</span>");
+ }
+}
+
+static void print_repo_overview(void)
+{
+ char *latest_tag = get_latest_tag_name();
+ int has_badges = ctx.repo->badges.nr > 0;
+
+ if (!ctx.repo->defbranch && !latest_tag && !has_badges)
+ return;
+
+ html("<div class='summary-overview'>");
+ html("<table summary='repository overview' class='list nowrap' style='width: auto'>");
+ html("<tr class='nohover'><th class='left' colspan='2'>Overview</th></tr>\n");
+
+ if (ctx.repo->defbranch) {
+ html("<tr><td class='left'>Default branch</td><td class='left'>");
+ cgit_log_link(ctx.repo->defbranch, NULL, NULL,
+ ctx.repo->defbranch, NULL, NULL,
+ 0, NULL, NULL, 0, 0);
+ html("</td></tr>\n");
+ }
+
+ if (latest_tag) {
+ html("<tr><td class='left'>Latest tag</td><td class='left'>");
+ cgit_tag_link(latest_tag, NULL, NULL, latest_tag);
+ html("</td></tr>\n");
+ }
+
+ if (has_badges) {
+ html("<tr><td class='left'>Badges</td><td class='left'>");
+ print_repo_badges();
+ html("</td></tr>\n");
+ }
+
+ html("</table>");
+ html("</div>");
+
+ free(latest_tag);
+}
+
+static void print_url(const char *url)
+{
+ int columns = 3;
+
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ if (urls++ == 0) {
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ htmlf("<tr class='nohover'><th class='left' colspan='%d'>Clone</th></tr>\n", columns);
+ }
+
+ htmlf("<tr><td colspan='%d'><a rel='vcs-git' href='", columns);
+ html_url_path(url);
+ html("' title='");
+ html_attr(ctx.repo->name);
+ html(" Git repository'>");
+ html_txt(url);
+ html("</a></td></tr>\n");
+}
+
+void cgit_print_summary(void)
+{
+ int columns = 3;
+
+ if (ctx.repo->enable_log_filecount)
+ columns++;
+ if (ctx.repo->enable_log_linecount)
+ columns++;
+
+ cgit_print_layout_start();
+ print_repo_overview();
+ print_repo_metadata();
+ html("<table summary='repository info' class='list nowrap'>");
+ cgit_print_branches(ctx.cfg.summary_branches);
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ cgit_print_tags(ctx.cfg.summary_tags);
+ if (ctx.cfg.summary_log > 0) {
+ htmlf("<tr class='nohover'><td colspan='%d'>&nbsp;</td></tr>", columns);
+ cgit_print_log(ctx.qry.head, 0, ctx.cfg.summary_log, NULL,
+ NULL, NULL, 0, 0, 0);
+ }
+ urls = 0;
+ cgit_add_clone_urls(print_url);
+ html("</table>");
+ cgit_print_layout_end();
+}
+
+/* The caller must free the return value. */
+static char* append_readme_path(const char *filename, const char *ref, const char *path)
+{
+ char *file, *base_dir, *full_path, *resolved_base = NULL, *resolved_full = NULL;
+ /* If a subpath is specified for the about page, make it relative
+ * to the directory containing the configured readme. */
+
+ file = xstrdup(filename);
+ base_dir = dirname(file);
+ if (!strcmp(base_dir, ".") || !strcmp(base_dir, "..")) {
+ if (!ref) {
+ free(file);
+ return NULL;
+ }
+ full_path = xstrdup(path);
+ } else
+ full_path = fmtalloc("%s/%s", base_dir, path);
+
+ if (!ref) {
+ resolved_base = realpath(base_dir, NULL);
+ resolved_full = realpath(full_path, NULL);
+ if (!resolved_base || !resolved_full || !starts_with(resolved_full, resolved_base)) {
+ free(full_path);
+ full_path = NULL;
+ }
+ }
+
+ free(file);
+ free(resolved_base);
+ free(resolved_full);
+
+ return full_path;
+}
+
+void cgit_print_repo_readme(const char *path)
+{
+ char *filename, *ref, *mimetype;
+ int free_filename = 0;
+
+ mimetype = get_mimetype_for_filename(path);
+ if (mimetype && (!strncmp(mimetype, "image/", 6) || !strncmp(mimetype, "video/", 6))) {
+ ctx.page.mimetype = mimetype;
+ ctx.page.charset = NULL;
+ cgit_print_plain();
+ free(mimetype);
+ return;
+ }
+ free(mimetype);
+
+ cgit_print_layout_start();
+ if (ctx.repo->readme.nr == 0)
+ goto done;
+
+ filename = ctx.repo->readme.items[0].string;
+ ref = ctx.repo->readme.items[0].util;
+
+ if (path) {
+ free_filename = 1;
+ filename = append_readme_path(filename, ref, path);
+ if (!filename)
+ goto done;
+ }
+
+ /* Print the calculated readme, either from the git repo or from the
+ * filesystem, while applying the about-filter.
+ */
+ html("<div id='summary'>");
+ cgit_open_filter(ctx.repo->about_filter, filename);
+ if (ref)
+ cgit_print_file(filename, ref, 1);
+ else
+ html_include(filename);
+ cgit_close_filter(ctx.repo->about_filter);
+
+ html("</div>");
+ if (free_filename)
+ free(filename);
+
+done:
+ cgit_print_layout_end();
+}