| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | AUTHORS | 12 | ||||
| -rw-r--r-- | Makefile | 21 | ||||
| -rw-r--r-- | README | 99 | ||||
| -rw-r--r-- | README.md | 95 | ||||
| -rw-r--r-- | cache.c | 2 | ||||
| -rw-r--r-- | cgit.c | 178 | ||||
| -rw-r--r-- | cgit.css | 407 | ||||
| -rw-r--r-- | cgit.h | 30 | ||||
| -rw-r--r-- | cgit.mk | 1 | ||||
| -rw-r--r-- | cgit.svg | 5 | ||||
| -rw-r--r-- | cgitrc.5.txt | 120 | ||||
| -rw-r--r-- | cmd.c | 11 | ||||
| -rw-r--r-- | favicon.svg | 5 | ||||
| -rw-r--r-- | filter.c | 16 | ||||
| -rwxr-xr-x | filters/ci-jenkins.sh | 58 | ||||
| -rwxr-xr-x | filters/html-converters/md2html | 84 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.py | 45 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.sh | 14 | ||||
| -rwxr-xr-x | gen-version.sh | 18 | ||||
| m--------- | git | 0 | ||||
| -rw-r--r-- | parsing.c | 10 | ||||
| -rw-r--r-- | scan-tree.c | 43 | ||||
| -rw-r--r-- | scan-tree.h | 4 | ||||
| -rw-r--r-- | shared.c | 41 | ||||
| -rw-r--r-- | solarized.css | 140 | ||||
| -rwxr-xr-x | tests/t0106-diff.sh | 56 | ||||
| -rw-r--r-- | tests/t0112-ci.sh | 127 | ||||
| -rw-r--r-- | tests/t0113-ci-lua.sh | 78 | ||||
| -rw-r--r-- | tests/t0114-broken-repo.sh | 71 | ||||
| -rw-r--r-- | ui-blame.c | 6 | ||||
| -rw-r--r-- | ui-blob.c | 14 | ||||
| -rw-r--r-- | ui-ci.c | 152 | ||||
| -rw-r--r-- | ui-ci.h | 7 | ||||
| -rw-r--r-- | ui-clone.c | 37 | ||||
| -rw-r--r-- | ui-diff.c | 68 | ||||
| -rw-r--r-- | ui-log.c | 5 | ||||
| -rw-r--r-- | ui-plain.c | 6 | ||||
| -rw-r--r-- | ui-repolist.c | 2 | ||||
| -rw-r--r-- | ui-shared.c | 127 | ||||
| -rw-r--r-- | ui-shared.h | 3 | ||||
| -rw-r--r-- | ui-snapshot.c | 2 | ||||
| -rw-r--r-- | ui-tag.c | 2 | ||||
| -rw-r--r-- | ui-tree.c | 8 |
44 files changed, 1775 insertions, 456 deletions
@@ -10,3 +10,4 @@ cgitrc.5.pdf cgitrc.5.xml *.o *.d +__pycache__/ @@ -1,13 +1,5 @@ Maintainer: - Jason A. Donenfeld <Jason@zx2c4.com> + Saya Andy <saya.andy@posteo.com> Contributors: - Jason A. Donenfeld <Jason@zx2c4.com> - Lukas Fleischer <cgit@cryptocrack.de> - Johan Herland <johan@herland.net> - Lars Hjemli <hjemli@gmail.com> - Ferry Huberts <ferry.huberts@pelagic.nl> - John Keeping <john@keeping.me.uk> - -Previous Maintainer: - Lars Hjemli <hjemli@gmail.com> + Saya Andy <saya.andy@posteo.com> @@ -1,6 +1,6 @@ all:: -CGIT_VERSION = v1.2.3 +CGIT_VERSION = v1.3.1 CGIT_SCRIPT_NAME = cgit.cgi CGIT_SCRIPT_PATH = /var/www/htdocs/cgit CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH) @@ -14,7 +14,7 @@ htmldir = $(docdir) pdfdir = $(docdir) mandir = $(prefix)/share/man SHA1_HEADER = <openssl/sha.h> -GIT_VER = 2.50.1 +GIT_VER = 2.54.0 GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz INSTALL = install COPYTREE = cp -r @@ -87,9 +87,12 @@ install: all $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH) $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css + $(INSTALL) -m 0644 solarized.css $(DESTDIR)$(CGIT_DATA_PATH)/solarized.css $(INSTALL) -m 0644 cgit.js $(DESTDIR)$(CGIT_DATA_PATH)/cgit.js $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png + $(INSTALL) -m 0644 cgit.svg $(DESTDIR)$(CGIT_DATA_PATH)/cgit.svg $(INSTALL) -m 0644 favicon.ico $(DESTDIR)$(CGIT_DATA_PATH)/favicon.ico + $(INSTALL) -m 0644 favicon.svg $(DESTDIR)$(CGIT_DATA_PATH)/favicon.svg $(INSTALL) -m 0644 robots.txt $(DESTDIR)$(CGIT_DATA_PATH)/robots.txt $(INSTALL) -m 0755 -d $(DESTDIR)$(filterdir) $(COPYTREE) filters/* $(DESTDIR)$(filterdir) @@ -108,11 +111,23 @@ install-pdf: doc-pdf $(INSTALL) -m 0755 -d $(DESTDIR)$(pdfdir) $(INSTALL) -m 0644 $(DOC_PDF) $(DESTDIR)$(pdfdir) +define rm_f +rm -f $(1) + +endef + uninstall: rm -f $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css + rm -f $(DESTDIR)$(CGIT_DATA_PATH)/solarized.css + rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.js rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png + rm -f $(DESTDIR)$(CGIT_DATA_PATH)/cgit.svg rm -f $(DESTDIR)$(CGIT_DATA_PATH)/favicon.ico + rm -f $(DESTDIR)$(CGIT_DATA_PATH)/favicon.svg + rm -f $(DESTDIR)$(CGIT_DATA_PATH)/robots.txt + $(foreach file,$(patsubst filters/%,%,$(shell find filters/ ! -type d)), \ + $(call rm_f,$(DESTDIR)$(filterdir)/$(file))) uninstall-doc: uninstall-man uninstall-html uninstall-pdf @@ -149,7 +164,7 @@ $(DOC_PDF): %.pdf : %.txt clean: clean-doc $(RM) cgit VERSION CGIT-CFLAGS *.o tags - $(RM) -r .deps + $(RM) -r .depend cleanall: clean $(MAKE) -C git clean @@ -1,99 +0,0 @@ -cgit - CGI for Git -================== - -This is an attempt to create a fast web interface for the Git SCM, using a -built-in cache to decrease server I/O pressure. - -Installation ------------- - -Building cgit involves building a proper version of Git. How to do this -depends on how you obtained the cgit sources: - -a) If you're working in a cloned cgit repository, you first need to -initialize and update the Git submodule: - - $ git submodule init # register the Git submodule in .git/config - $ $EDITOR .git/config # if you want to specify a different url for git - $ git submodule update # clone/fetch and checkout correct git version - -b) If you're building from a cgit tarball, you can download a proper git -version like this: - - $ make get-git - -When either a) or b) has been performed, you can build and install cgit like -this: - - $ make - $ sudo make install - -This will install `cgit.cgi` and `cgit.css` into `/var/www/htdocs/cgit`. You -can configure this location (and a few other things) by providing a `cgit.conf` -file (see the Makefile for details). - -If you'd like to compile without Lua support, you may use: - - $ make NO_LUA=1 - -And if you'd like to specify a Lua implementation, you may use: - - $ make LUA_PKGCONFIG=lua5.1 - -If this is not specified, the Lua implementation will be auto-detected, -preferring LuaJIT if many are present. Acceptable values are generally "lua", -"luajit", "lua5.1", and "lua5.2". - - -Dependencies ------------- - -* libzip -* libcrypto (OpenSSL) -* libssl (OpenSSL) -* optional: luajit or lua, most reliably used when pkg-config is available - -Apache configuration --------------------- - -A new `Directory` section must probably be added for cgit, possibly something -like this: - - <Directory "/var/www/htdocs/cgit/"> - AllowOverride None - Options +ExecCGI - Order allow,deny - Allow from all - </Directory> - - -Runtime configuration ---------------------- - -The file `/etc/cgitrc` is read by cgit before handling a request. In addition -to runtime parameters, this file may also contain a list of repositories -displayed by cgit (see `cgitrc.5.txt` for further details). - -The cache ---------- - -When cgit is invoked it looks for a cache file matching the request and -returns it to the client. If no such cache file exists (or if it has expired), -the content for the request is written into the proper cache file before the -file is returned. - -If the cache file has expired but cgit is unable to obtain a lock for it, the -stale cache file is returned to the client. This is done to favour page -throughput over page freshness. - -The generated content contains the complete response to the client, including -the HTTP headers `Modified` and `Expires`. - -Online presence ---------------- - -* The cgit homepage is hosted by cgit at <https://git.zx2c4.com/cgit/about/> - -* Patches, bug reports, discussions and support should go to the cgit - mailing list: <cgit@lists.zx2c4.com>. To sign up, visit - <https://lists.zx2c4.com/mailman/listinfo/cgit> diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e13a37 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +## cgitext – CGI for Git extension by Saya Andy + +This is a custom expansion to cgit. Its most prominent changes by now are: + +* CI/CD tab, linking to an external service on ref pages by using template. The Jenkins plugin is added to support a scenario when a ref lacks a pipeline. + +* Theming now relies on CSS variables set in cgit.css, to allow easier custom additions and implicit filter theming. + +* Allow to use multiple favicons and of MIME type different from x-icon (f.ex. svg/png/webp). + +* Use vector-based format for favicon and cgit logo and make it default. + +* Include optional solarized.css addon theme. + +* Allow user to pick variable refs for diff in web ui. + +The original cgit project is hosted at [git.zx2c4.com](https://git.zx2c4.com/cgit/about/) + +## cgit – CGI for Git + +This is an attempt to create a fast web interface for the Git SCM, using a built-in cache to decrease server I/O pressure. + +### Installation + +Building cgit involves building a proper version of Git. How to do this depends on how you obtained the cgit sources: + +1. If you're working in a cloned cgit repository, you first need to initialize and update the Git submodule: + + $ git submodule init # register the Git submodule in .git/config + $ $EDITOR .git/config # if you want to specify a different url for git + $ git submodule update # clone/fetch and checkout correct git version + +2. If you're building from a cgit tarball, you can download a proper git version like this: + + $ make get-git + +When either `1.` or `2.` has been performed, you can build and install cgit like +this: +``` +make +sudo make install +``` + +This will install `cgit.cgi` and `cgit.css` into `/var/www/htdocs/cgit`. You can configure this location (and a few other things) by providing a `cgit.conf` file (see the Makefile for details). + +If you'd like to compile without Lua support, you may use: +``` +make NO_LUA=1 +``` + +And if you'd like to specify a Lua implementation, you may use: +``` +make LUA_PKGCONFIG=lua5.1 +``` + +If this is not specified, the Lua implementation will be auto-detected, preferring LuaJIT if many are present. Acceptable values are generally "lua", "luajit", "lua5.1", and "lua5.2". + + +### Dependencies + +* libzip +* libcrypto (OpenSSL) +* libssl (OpenSSL) +* optional: luajit or lua, most reliably used when pkg-config is available + +### Apache configuration + +A new `Directory` section must probably be added for cgit, possibly something like this: + +``` +<Directory "/var/www/htdocs/cgit/"> + AllowOverride None + Options +ExecCGI + Order allow,deny + Allow from all +</Directory> +``` + +### Runtime configuration + +The file `/etc/cgitrc` is read by cgit before handling a request. In addition to runtime parameters, this file may also contain a list of repositories displayed by cgit (see `cgitrc.5.txt` for further details). + +### The cache + +When cgit is invoked it looks for a cache file matching the request and returns it to the client. If no such cache file exists (or if it has expired), the content for the request is written into the proper cache file before the file is returned. + +If the cache file has expired but cgit is unable to obtain a lock for it, the stale cache file is returned to the client. This is done to favour page throughput over page freshness. + +The generated content contains the complete response to the client, including the HTTP headers `Modified` and `Expires`. + +### Online presence + +* The cgit homepage is hosted by cgit at [git.zx2c4.com](https://git.zx2c4.com/cgit/about/) + +* Patches, bug reports, discussions and support should go to the cgit mailing list: [cgit@lists.zx2c4.com](mailto:cgit@lists.zx2c4.com). To sign up, visit [lists.zx2c4.com](https://lists.zx2c4.com/mailman/listinfo/cgit) @@ -185,6 +185,8 @@ static int lock_slot(struct cache_slot *slot) slot->lock_fd = -1; return saved_errno; } + if (ftruncate(slot->lock_fd, 0) < 0) + return errno; if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0) return errno; return 0; @@ -1,6 +1,11 @@ /* cgit.c: cgi for the git scm * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Always check there is git repo on repo.path + * Allow to include multiple favicons * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -41,31 +46,47 @@ static void add_mimetype(const char *name, const char *value) static void process_cached_repolist(const char *path); -static void repo_config(struct cgit_repo *repo, const char *name, const char *value) +void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *value) { const char *path; struct string_list_item *item; if (!strcmp(name, "name")) - repo->name = xstrdup(value); + repo->name = strdup_first_line(value); else if (!strcmp(name, "clone-url")) - repo->clone_url = xstrdup(value); + repo->clone_url = strdup_first_line(value); else if (!strcmp(name, "desc")) - repo->desc = xstrdup(value); + repo->desc = strdup_first_line(value); else if (!strcmp(name, "owner")) - repo->owner = xstrdup(value); + repo->owner = strdup_first_line(value); else if (!strcmp(name, "homepage")) - repo->homepage = xstrdup(value); + repo->homepage = strdup_first_line(value); + else if (!strcmp(name, "ci-url")) { + repo->ci_url = strdup_first_line(value); + /* An explicit repo.ci-url would otherwise never be used + * when the more specific urls are inherited from the + * global settings, so discard those. + */ + if (repo->ci_branch_url == ctx.cfg.ci_branch_url) + repo->ci_branch_url = NULL; + if (repo->ci_tag_url == ctx.cfg.ci_tag_url) + repo->ci_tag_url = NULL; + } else if (!strcmp(name, "ci-branch-url")) + repo->ci_branch_url = strdup_first_line(value); + else if (!strcmp(name, "ci-tag-url")) + repo->ci_tag_url = strdup_first_line(value); else if (!strcmp(name, "defbranch")) - repo->defbranch = xstrdup(value); + repo->defbranch = strdup_first_line(value); else if (!strcmp(name, "extra-head-content")) - repo->extra_head_content = xstrdup(value); + repo->extra_head_content = strdup_first_line(value); else if (!strcmp(name, "snapshots")) repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); else if (!strcmp(name, "enable-blame")) repo->enable_blame = atoi(value); else if (!strcmp(name, "enable-commit-graph")) repo->enable_commit_graph = atoi(value); + else if (!strcmp(name, "enable-follow-links")) + repo->enable_follow_links = atoi(value); else if (!strcmp(name, "enable-log-filecount")) repo->enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) @@ -89,22 +110,22 @@ static void repo_config(struct cgit_repo *repo, const char *name, const char *va } else if (!strcmp(name, "max-stats")) repo->max_stats = cgit_find_stats_period(value, NULL); else if (!strcmp(name, "module-link")) - repo->module_link= xstrdup(value); + repo->module_link= strdup_first_line(value); else if (skip_prefix(name, "module-link.", &path)) { - item = string_list_append(&repo->submodules, xstrdup(path)); - item->util = xstrdup(value); + item = string_list_append(&repo->submodules, strdup_first_line(path)); + item->util = strdup_first_line(value); } else if (!strcmp(name, "section")) - repo->section = xstrdup(value); + repo->section = strdup_first_line(value); else if (!strcmp(name, "snapshot-prefix")) - repo->snapshot_prefix = xstrdup(value); + repo->snapshot_prefix = strdup_first_line(value); else if (!strcmp(name, "readme") && value != NULL) { if (repo->readme.items == ctx.cfg.readme.items) memset(&repo->readme, 0, sizeof(repo->readme)); - string_list_append(&repo->readme, xstrdup(value)); + string_list_append(&repo->readme, strdup_first_line(value)); } else if (!strcmp(name, "logo") && value != NULL) - repo->logo = xstrdup(value); + repo->logo = strdup_first_line(value); else if (!strcmp(name, "logo-link") && value != NULL) - repo->logo_link = xstrdup(value); + repo->logo_link = strdup_first_line(value); else if (!strcmp(name, "hide")) repo->hide = atoi(value); else if (!strcmp(name, "ignore")) @@ -120,6 +141,8 @@ static void repo_config(struct cgit_repo *repo, const char *name, const char *va repo->email_filter = cgit_new_filter(value, EMAIL); else if (!strcmp(name, "owner-filter")) repo->owner_filter = cgit_new_filter(value, OWNER); + else if (!strcmp(name, "ci-filter")) + repo->ci_filter = cgit_new_filter(value, CI); } } @@ -128,41 +151,47 @@ static void config_cb(const char *name, const char *value) const char *arg; if (!strcmp(name, "section")) - ctx.cfg.section = xstrdup(value); + ctx.cfg.section = strdup_first_line(value); else if (!strcmp(name, "repo.url")) ctx.repo = cgit_add_repo(value); else if (ctx.repo && !strcmp(name, "repo.path")) ctx.repo->path = trim_end(value, '/'); else if (ctx.repo && skip_prefix(name, "repo.", &arg)) - repo_config(ctx.repo, arg, value); + cgit_repo_config(ctx.repo, arg, value); else if (!strcmp(name, "readme")) - string_list_append(&ctx.cfg.readme, xstrdup(value)); + string_list_append(&ctx.cfg.readme, strdup_first_line(value)); else if (!strcmp(name, "root-title")) - ctx.cfg.root_title = xstrdup(value); + ctx.cfg.root_title = strdup_first_line(value); else if (!strcmp(name, "root-desc")) - ctx.cfg.root_desc = xstrdup(value); + ctx.cfg.root_desc = strdup_first_line(value); else if (!strcmp(name, "root-readme")) - ctx.cfg.root_readme = xstrdup(value); + ctx.cfg.root_readme = strdup_first_line(value); else if (!strcmp(name, "css")) - string_list_append(&ctx.cfg.css, xstrdup(value)); + string_list_append(&ctx.cfg.css, strdup_first_line(value)); else if (!strcmp(name, "js")) - string_list_append(&ctx.cfg.js, xstrdup(value)); + string_list_append(&ctx.cfg.js, strdup_first_line(value)); else if (!strcmp(name, "favicon")) - ctx.cfg.favicon = xstrdup(value); + string_list_append(&ctx.cfg.favicon, strdup_first_line(value)); else if (!strcmp(name, "footer")) - ctx.cfg.footer = xstrdup(value); + ctx.cfg.footer = strdup_first_line(value); else if (!strcmp(name, "head-include")) - ctx.cfg.head_include = xstrdup(value); + ctx.cfg.head_include = strdup_first_line(value); else if (!strcmp(name, "header")) - ctx.cfg.header = xstrdup(value); + ctx.cfg.header = strdup_first_line(value); else if (!strcmp(name, "logo")) - ctx.cfg.logo = xstrdup(value); + ctx.cfg.logo = strdup_first_line(value); else if (!strcmp(name, "logo-link")) - ctx.cfg.logo_link = xstrdup(value); + ctx.cfg.logo_link = strdup_first_line(value); else if (!strcmp(name, "module-link")) - ctx.cfg.module_link = xstrdup(value); + ctx.cfg.module_link = strdup_first_line(value); + else if (!strcmp(name, "ci-url")) + ctx.cfg.ci_url = strdup_first_line(value); + else if (!strcmp(name, "ci-branch-url")) + ctx.cfg.ci_branch_url = strdup_first_line(value); + else if (!strcmp(name, "ci-tag-url")) + ctx.cfg.ci_tag_url = strdup_first_line(value); else if (!strcmp(name, "strict-export")) - ctx.cfg.strict_export = xstrdup(value); + ctx.cfg.strict_export = strdup_first_line(value); else if (!strcmp(name, "virtual-root")) ctx.cfg.virtual_root = ensure_end(value, '/'); else if (!strcmp(name, "noplainemail")) @@ -204,7 +233,7 @@ static void config_cb(const char *name, const char *value) else if (!strcmp(name, "cache-size")) ctx.cfg.cache_size = atoi(value); else if (!strcmp(name, "cache-root")) - ctx.cfg.cache_root = xstrdup(expand_macros(value)); + ctx.cfg.cache_root = strdup_first_line(expand_macros(value)); else if (!strcmp(name, "cache-root-ttl")) ctx.cfg.cache_root_ttl = atoi(value); else if (!strcmp(name, "cache-repo-ttl")) @@ -231,6 +260,8 @@ static void config_cb(const char *name, const char *value) ctx.cfg.owner_filter = cgit_new_filter(value, OWNER); else if (!strcmp(name, "auth-filter")) ctx.cfg.auth_filter = cgit_new_filter(value, AUTH); + else if (!strcmp(name, "ci-filter")) + ctx.cfg.ci_filter = cgit_new_filter(value, CI); else if (!strcmp(name, "embedded")) ctx.cfg.embedded = atoi(value); else if (!strcmp(name, "max-atom-items")) @@ -248,21 +279,21 @@ static void config_cb(const char *name, const char *value) } else if (!strcmp(name, "max-commit-count")) ctx.cfg.max_commit_count = atoi(value); else if (!strcmp(name, "project-list")) - ctx.cfg.project_list = xstrdup(expand_macros(value)); + ctx.cfg.project_list = strdup_first_line(expand_macros(value)); else if (!strcmp(name, "scan-path")) if (ctx.cfg.cache_size) process_cached_repolist(expand_macros(value)); else if (ctx.cfg.project_list) scan_projects(expand_macros(value), - ctx.cfg.project_list, repo_config); + ctx.cfg.project_list); else - scan_tree(expand_macros(value), repo_config); + scan_tree(expand_macros(value)); else if (!strcmp(name, "scan-hidden-path")) ctx.cfg.scan_hidden_path = atoi(value); else if (!strcmp(name, "section-from-path")) ctx.cfg.section_from_path = atoi(value); else if (!strcmp(name, "repository-sort")) - ctx.cfg.repository_sort = xstrdup(value); + ctx.cfg.repository_sort = strdup_first_line(value); else if (!strcmp(name, "section-sort")) ctx.cfg.section_sort = atoi(value); else if (!strcmp(name, "source-filter")) @@ -276,19 +307,19 @@ static void config_cb(const char *name, const char *value) else if (!strcmp(name, "side-by-side-diffs")) ctx.cfg.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED; else if (!strcmp(name, "agefile")) - ctx.cfg.agefile = xstrdup(value); + ctx.cfg.agefile = strdup_first_line(value); else if (!strcmp(name, "mimetype-file")) - ctx.cfg.mimetype_file = xstrdup(value); + ctx.cfg.mimetype_file = strdup_first_line(value); else if (!strcmp(name, "renamelimit")) ctx.cfg.renamelimit = atoi(value); else if (!strcmp(name, "remove-suffix")) ctx.cfg.remove_suffix = atoi(value); else if (!strcmp(name, "robots")) - ctx.cfg.robots = xstrdup(value); + ctx.cfg.robots = strdup_first_line(value); else if (!strcmp(name, "clone-prefix")) - ctx.cfg.clone_prefix = xstrdup(value); + ctx.cfg.clone_prefix = strdup_first_line(value); else if (!strcmp(name, "clone-url")) - ctx.cfg.clone_url = xstrdup(value); + ctx.cfg.clone_url = strdup_first_line(value); else if (!strcmp(name, "local-time")) ctx.cfg.local_time = atoi(value); else if (!strcmp(name, "commit-sort")) { @@ -382,8 +413,7 @@ static void prepare_context(void) ctx.cfg.case_sensitive_sort = 1; ctx.cfg.branch_sort = 0; ctx.cfg.commit_sort = 0; - ctx.cfg.logo = "/cgit.png"; - ctx.cfg.favicon = "/favicon.ico"; + ctx.cfg.logo = "/cgit.svg"; ctx.cfg.local_time = 0; ctx.cfg.enable_http_clone = 1; ctx.cfg.enable_index_owner = 1; @@ -448,16 +478,15 @@ struct refmatch { int match; }; -static int find_current_ref(const char *refname, const char *referent UNUSED, - const struct object_id *oid, int flags, void *cb_data) +static int find_current_ref(const struct reference *ref, void *cb_data) { struct refmatch *info; info = (struct refmatch *)cb_data; - if (!strcmp(refname, info->req_ref)) + if (!strcmp(ref->name, info->req_ref)) info->match = 1; if (!info->first_ref) - info->first_ref = xstrdup(refname); + info->first_ref = xstrdup(ref->name); return info->match; } @@ -586,9 +615,12 @@ static void prepare_repo_env(int *nongit) /* Setup the git directory and initialize the notes system. Both of these * load local configuration from the git repository, so we do them both while - * the HOME variables are unset. */ + * the HOME variables are unset. Initializing the notes system requires an + * actual repository, as it would otherwise dereference the not yet known + * hash algorithm. */ setup_git_directory_gently(nongit); - load_display_notes(NULL); + if (!*nongit) + load_display_notes(NULL); } static int prepare_repo_cmd(int nongit) @@ -785,15 +817,6 @@ static char *build_snapshot_setting(int bitmap) return strbuf_detach(&result, NULL); } -static char *get_first_line(char *txt) -{ - char *t = xstrdup(txt); - char *p = strchr(t, '\n'); - if (p) - *p = '\0'; - return t; -} - static void print_repo(FILE *f, struct cgit_repo *repo) { struct string_list_item *item; @@ -802,11 +825,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.path=%s\n", repo->path); if (repo->owner) fprintf(f, "repo.owner=%s\n", repo->owner); - if (repo->desc) { - char *tmp = get_first_line(repo->desc); - fprintf(f, "repo.desc=%s\n", tmp); - free(tmp); - } + if (repo->desc) + fprintf(f, "repo.desc=%s\n", repo->desc); for_each_string_list_item(item, &repo->readme) { if (item->util) fprintf(f, "repo.readme=%s:%s\n", (char *)item->util, item->string); @@ -823,12 +843,20 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.section=%s\n", repo->section); if (repo->homepage) fprintf(f, "repo.homepage=%s\n", repo->homepage); + if (repo->ci_url) + fprintf(f, "repo.ci-url=%s\n", repo->ci_url); + if (repo->ci_branch_url) + fprintf(f, "repo.ci-branch-url=%s\n", repo->ci_branch_url); + if (repo->ci_tag_url) + fprintf(f, "repo.ci-tag-url=%s\n", repo->ci_tag_url); if (repo->clone_url) fprintf(f, "repo.clone-url=%s\n", repo->clone_url); fprintf(f, "repo.enable-blame=%d\n", repo->enable_blame); fprintf(f, "repo.enable-commit-graph=%d\n", repo->enable_commit_graph); + fprintf(f, "repo.enable-follow-links=%d\n", + repo->enable_follow_links); fprintf(f, "repo.enable-log-filecount=%d\n", repo->enable_log_filecount); fprintf(f, "repo.enable-log-linecount=%d\n", @@ -843,6 +871,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter="); if (repo->owner_filter && repo->owner_filter != ctx.cfg.owner_filter) cgit_fprintf_filter(repo->owner_filter, f, "repo.owner-filter="); + if (repo->ci_filter && repo->ci_filter != ctx.cfg.ci_filter) + cgit_fprintf_filter(repo->ci_filter, f, "repo.ci-filter="); if (repo->snapshots != ctx.cfg.snapshots) { char *tmp = build_snapshot_setting(repo->snapshots); fprintf(f, "repo.snapshots=%s\n", tmp ? tmp : ""); @@ -905,9 +935,9 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) } idx = cgit_repolist.count; if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list, repo_config); + scan_projects(path, ctx.cfg.project_list); else - scan_tree(path, repo_config); + scan_tree(path); print_repolist(f, &cgit_repolist, idx); if (rename(locked_rc.buf, cached_rc)) fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n", @@ -937,10 +967,9 @@ static void process_cached_repolist(const char *path) */ if (generate_cached_repolist(path, cached_rc.buf)) { if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list, - repo_config); + scan_projects(path, ctx.cfg.project_list); else - scan_tree(path, repo_config); + scan_tree(path); } goto out; } @@ -972,7 +1001,7 @@ static void cgit_parse_args(int argc, const char **argv) for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "--version")) { - printf("CGit %s | https://git.zx2c4.com/cgit/\n\nCompiled in features:\n", CGIT_VERSION); + printf("CGit %s | https://sayag.it/cgitext.git | https://git.zx2c4.com/cgit/\n\nCompiled in features:\n", CGIT_VERSION); #ifdef NO_LUA printf("[-] "); #else @@ -1021,7 +1050,7 @@ static void cgit_parse_args(int argc, const char **argv) */ ctx.cfg.snapshots = 0xFF; scan++; - scan_tree(arg, repo_config); + scan_tree(arg); } } if (scan) { @@ -1055,6 +1084,12 @@ static int calc_ttl(void) return ctx.cfg.cache_repo_ttl; } +static NORETURN void cgit_die_routine(const char *msg, va_list params) +{ + cgit_vprint_error_page(400, "Bad request", msg, params); + exit(0); +} + int cmd_main(int argc, const char **argv) { const char *path; @@ -1062,6 +1097,7 @@ int cmd_main(int argc, const char **argv) cgit_init_filters(); atexit(cgit_cleanup_filters); + set_die_routine(cgit_die_routine); prepare_context(); cgit_repolist.length = 0; @@ -1,15 +1,88 @@ div#cgit { + --bg: white; + --bg-zebra: #f7f7f7; + --bg-hl: #eee; + --bg-hl2: #ccc; + --fg: #333; + --fg-strong: #000; + --fg-mid: #444; + --fg-mute: #777; + --fg-dim: #888; + --fg-faint: #bbb; + --fg-faintest: #ccc; + --fg-on-tint: black; + --fg-linenum: gray; + --border: #ccc; + --border-mid: #aaa; + --border-dark: #777; + --border-strong: black; + + --red: red; + --red-mid: #c00; + --red-dark: #800; + --green: green; + --green-mid: #080; + --green-dark: #040; + --blue: blue; + --blue-dark: #009; + + --graph-col1: #a00; + --graph-col2: #0a0; + --graph-col3: #aa0; + --graph-col4: #00a; + --graph-col5: #a0a; + --graph-col6: #0aa; + + --graph-add-bg: #5c5; + --graph-rem-bg: #c55; + + --notes-bg: #ffd; + --notes-border: #ee9; + --add-bg: #cfc; + --add-bg-dark: #aca; + --del-bg: #fcc; + --del-bg-dark: #caa; + --changed-bg: #ffc; + --changed-bg-dark: #cca; + --hunk-bg: #ccf; + + --deco-branch-bg: #8f8; + --deco-branch-border: #070; + --deco-tag-bg: #ff8; + --deco-tag-border: #770; + --deco-tag-annotated-bg: #fc8; + --deco-tag-annotated-border: #770; + --deco-remote-bg: #ccf; + --deco-remote-border: #007; + --deco-other-bg: #f88; + --deco-other-border: #700; + + --syn-keyword: #000; + --syn-keyword-alt: #0057ae; + --syn-type: #010181; + --syn-string: #bf0303; + --syn-string-alt: #818100; + --syn-escape: #f0f; + --syn-comment: #838183; + --syn-number: #b07e00; + --syn-preproc: #008200; + --syn-operator: #000; + --syn-interpolation: #0057ae; + --syn-linenum: #555; + --syn-error: #bf0303; + --syn-mark-bg: #ffb; + padding: 0em; margin: 0em; font-family: sans-serif; font-size: 10pt; - color: #333; - background: white; + color: var(--fg); + background: var(--bg); padding: 4px; } div#cgit a { - color: blue; + color: var(--blue); text-decoration: none; } @@ -38,7 +111,7 @@ div#cgit table#header td.main { } div#cgit table#header td.main a { - color: #000; + color: var(--fg-strong); } div#cgit table#header td.form { @@ -56,13 +129,13 @@ div#cgit table#header td.form select { } div#cgit table#header td.sub { - color: #777; - border-top: solid 1px #ccc; + color: var(--fg-mute); + border-top: solid 1px var(--border); padding-left: 10px; } div#cgit table.tabs { - border-bottom: solid 3px #ccc; + border-bottom: solid 3px var(--border); border-collapse: collapse; margin-top: 2em; margin-bottom: 0px; @@ -76,13 +149,13 @@ div#cgit table.tabs td { div#cgit table.tabs td a { padding: 2px 0.75em; - color: #777; + color: var(--fg-mute); font-size: 110%; } div#cgit table.tabs td a.active { - color: #000; - background-color: #ccc; + color: var(--fg-strong); + background-color: var(--bg-hl2); } div#cgit table.tabs a[href^="http://"]:after, div#cgit table.tabs a[href^="https://"]:after { @@ -109,14 +182,14 @@ div#cgit table.tabs td.form select { div#cgit div.path { margin: 0px; padding: 5px 2em 2px 2em; - color: #000; - background-color: #eee; + color: var(--fg-strong); + background-color: var(--bg-hl); } div#cgit div.content { margin: 0px; padding: 2em; - border-bottom: solid 3px #ccc; + border-bottom: solid 3px var(--border); } @@ -127,46 +200,46 @@ div#cgit table.list { } div#cgit table.list tr { - background: white; + background: var(--bg); } div#cgit table.list tr.logheader { - background: #eee; + background: var(--bg-hl); } div#cgit table.list tr:nth-child(even) { - background: #f7f7f7; + background: var(--bg-zebra); } div#cgit table.list tr:nth-child(odd) { - background: white; + background: var(--bg); } div#cgit table.list tr:hover { - background: #eee; + background: var(--bg-hl); } div#cgit table.list tr.nohover { - background: white; + background: var(--bg); } div#cgit table.list tr.nohover:hover { - background: white; + background: var(--bg); } div#cgit table.list tr.nohover-highlight:hover:nth-child(even) { - background: #f7f7f7; + background: var(--bg-zebra); } div#cgit table.list tr.nohover-highlight:hover:nth-child(odd) { - background: white; + background: var(--bg); } div#cgit table.list th { font-weight: bold; - /* color: #888; - border-top: dashed 1px #888; - border-bottom: dashed 1px #888; + /* color: var(--fg-dim); + border-top: dashed 1px var(--fg-dim); + border-bottom: dashed 1px var(--fg-dim); */ padding: 0.1em 0.5em 0.05em 0.5em; vertical-align: baseline; @@ -183,27 +256,27 @@ div#cgit table.list td.commitgraph { } div#cgit table.list td.commitgraph .column1 { - color: #a00; + color: var(--graph-col1); } div#cgit table.list td.commitgraph .column2 { - color: #0a0; + color: var(--graph-col2); } div#cgit table.list td.commitgraph .column3 { - color: #aa0; + color: var(--graph-col3); } div#cgit table.list td.commitgraph .column4 { - color: #00a; + color: var(--graph-col4); } div#cgit table.list td.commitgraph .column5 { - color: #a0a; + color: var(--graph-col5); } div#cgit table.list td.commitgraph .column6 { - color: #0aa; + color: var(--graph-col6); } div#cgit table.list td.logsubject { @@ -218,16 +291,16 @@ div#cgit table.list td.logmsg { } div#cgit table.list td a { - color: black; + color: var(--fg-strong); } div#cgit table.list td a.ls-dir { font-weight: bold; - color: #00f; + color: var(--blue); } div#cgit table.list td a:hover { - color: #00f; + color: var(--blue); } div#cgit img { @@ -261,21 +334,21 @@ div#cgit div#summary { div#cgit table#downloads { float: right; border-collapse: collapse; - border: solid 1px #777; + border: solid 1px var(--border-dark); margin-left: 0.5em; margin-bottom: 0.5em; } div#cgit table#downloads th { - background-color: #ccc; + background-color: var(--bg-hl2); } div#cgit div#blob { - border: solid 1px black; + border: solid 1px var(--border-strong); } div#cgit div.error { - color: red; + color: var(--red); font-weight: bold; margin: 1em 2em; } @@ -297,21 +370,21 @@ div#cgit td.ls-mode { div#cgit table.blob { margin-top: 0.5em; - border-top: solid 1px black; + border-top: solid 1px var(--border-strong); } div#cgit table.blob td.hashes, div#cgit table.blob td.lines { margin: 0; padding: 0 0 0 0.5em; vertical-align: top; - color: black; + color: var(--fg-strong); } div#cgit table.blob td.linenumbers { margin: 0; padding: 0 0.5em 0 0.5em; vertical-align: top; text-align: right; - border-right: 1px solid gray; + border-right: 1px solid var(--fg-linenum); } div#cgit table.blob pre { @@ -320,14 +393,19 @@ div#cgit table.blob pre { div#cgit table.blob td.linenumbers a, div#cgit table.ssdiff td.lineno a { - color: gray; + color: var(--fg-linenum); text-align: right; text-decoration: none; } div#cgit table.blob td.linenumbers a:hover, div#cgit table.ssdiff td.lineno a:hover { - color: black; + color: var(--fg-strong); +} + +div#cgit table.blob td.linenumbers a:target:before { + color: var(--red); + content: "\2BA9"; } div#cgit table.blame td.hashes, @@ -346,11 +424,11 @@ div#cgit table.blame td.linenumbers div.alt { } div#cgit table.blame div.alt:nth-child(even) { - background: #eee; + background: var(--bg-hl); } div#cgit table.blame div.alt:nth-child(odd) { - background: white; + background: var(--bg); } div#cgit table.blame td.lines > div { @@ -369,20 +447,20 @@ div#cgit table.blame .oid { div#cgit table.bin-blob { margin-top: 0.5em; - border: solid 1px black; + border: solid 1px var(--border-strong); } div#cgit table.bin-blob th { font-family: monospace; white-space: pre; - border: solid 1px #777; + border: solid 1px var(--border-dark); padding: 0.5em 1em; } div#cgit table.bin-blob td { font-family: monospace; white-space: pre; - border-left: solid 1px #777; + border-left: solid 1px var(--border-dark); padding: 0em 1em; } @@ -402,8 +480,8 @@ div#cgit div.cgit-panel { div#cgit div.cgit-panel table { border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; + border: solid 1px var(--border-mid); + background-color: var(--bg-hl); } div#cgit div.cgit-panel th { @@ -454,8 +532,8 @@ div#cgit div.notes-header { div#cgit div.notes { white-space: pre; font-family: monospace; - border: solid 1px #ee9; - background-color: #ffd; + border: solid 1px var(--notes-border); + background-color: var(--notes-bg); padding: 0.3em 2em 0.3em 1em; float: left; } @@ -471,8 +549,8 @@ div#cgit div.diffstat-header { div#cgit table.diffstat { border-collapse: collapse; - border: solid 1px #aaa; - background-color: #eee; + border: solid 1px var(--border-mid); + background-color: var(--bg-hl); } div#cgit table.diffstat th { @@ -495,19 +573,19 @@ div#cgit table.diffstat td.mode { div#cgit table.diffstat td span.modechange { padding-left: 1em; - color: red; + color: var(--red); } div#cgit table.diffstat td.add a { - color: green; + color: var(--green); } div#cgit table.diffstat td.del a { - color: red; + color: var(--red); } div#cgit table.diffstat td.upd a { - color: blue; + color: var(--blue); } div#cgit table.diffstat td.graph { @@ -526,15 +604,15 @@ div#cgit table.diffstat td.graph td { } div#cgit table.diffstat td.graph td.add { - background-color: #5c5; + background-color: var(--graph-add-bg); } div#cgit table.diffstat td.graph td.rem { - background-color: #c55; + background-color: var(--graph-rem-bg); } div#cgit div.diffstat-summary { - color: #888; + color: var(--fg-dim); padding-top: 0.5em; } @@ -550,19 +628,19 @@ div#cgit table.diff td { div#cgit table.diff td div.head { font-weight: bold; margin-top: 1em; - color: black; + color: var(--fg-strong); } div#cgit table.diff td div.hunk { - color: #009; + color: var(--blue-dark); } div#cgit table.diff td div.add { - color: green; + color: var(--green); } div#cgit table.diff td div.del { - color: red; + color: var(--red); } div#cgit .oid { @@ -580,7 +658,7 @@ div#cgit .right { div#cgit table.list td.reposection { font-style: italic; - color: #888; + color: var(--fg-dim); } div#cgit a.button { @@ -617,7 +695,7 @@ div#cgit ul.pager li { } div#cgit ul.pager a { - color: #777; + color: var(--fg-mute); } div#cgit ul.pager .current { @@ -626,46 +704,46 @@ div#cgit ul.pager .current { div#cgit span.age-mins { font-weight: bold; - color: #080; + color: var(--green-mid); } div#cgit span.age-hours { - color: #080; + color: var(--green-mid); } div#cgit span.age-days { - color: #040; + color: var(--green-dark); } div#cgit span.age-weeks { - color: #444; + color: var(--fg-mid); } div#cgit span.age-months { - color: #888; + color: var(--fg-dim); } div#cgit span.age-years { - color: #bbb; + color: var(--fg-faint); } div#cgit span.insertions { - color: #080; + color: var(--green-mid); } div#cgit span.deletions { - color: #800; + color: var(--red-dark); } div#cgit div.footer { margin-top: 0.5em; text-align: center; font-size: 80%; - color: #ccc; + color: var(--fg-faintest); } div#cgit div.footer a { - color: #ccc; + color: var(--fg-faintest); text-decoration: none; } @@ -674,43 +752,48 @@ div#cgit div.footer a:hover { } div#cgit a.branch-deco { - color: #000; + color: var(--fg-strong); margin: 0px 0.5em; padding: 0px 0.25em; - background-color: #88ff88; - border: solid 1px #007700; + background-color: var(--deco-branch-bg); + border: solid 1px var(--deco-branch-border); + border-radius: 2px; } div#cgit a.tag-deco { - color: #000; + color: var(--fg-strong); margin: 0px 0.5em; padding: 0px 0.25em; - background-color: #ffff88; - border: solid 1px #777700; + background-color: var(--deco-tag-bg); + border: solid 1px var(--deco-tag-border); + border-radius: 2px; } div#cgit a.tag-annotated-deco { - color: #000; + color: var(--fg-strong); margin: 0px 0.5em; padding: 0px 0.25em; - background-color: #ffcc88; - border: solid 1px #777700; + background-color: var(--deco-tag-annotated-bg); + border: solid 1px var(--deco-tag-annotated-border); + border-radius: 2px; } div#cgit a.remote-deco { - color: #000; + color: var(--fg-strong); margin: 0px 0.5em; padding: 0px 0.25em; - background-color: #ccccff; - border: solid 1px #000077; + background-color: var(--deco-remote-bg); + border: solid 1px var(--deco-remote-border); + border-radius: 2px; } div#cgit a.deco { - color: #000; + color: var(--fg-strong); margin: 0px 0.5em; padding: 0px 0.25em; - background-color: #ff8888; - border: solid 1px #770000; + background-color: var(--deco-other-bg); + border: solid 1px var(--deco-other-border); + border-radius: 2px; } div#cgit div.commit-subject a.branch-deco, @@ -723,21 +806,21 @@ div#cgit div.commit-subject a.deco { } div#cgit table.stats { - border: solid 1px black; + border: solid 1px var(--border-strong); border-collapse: collapse; } div#cgit table.stats th { text-align: left; padding: 1px 0.5em; - background-color: #eee; - border: solid 1px black; + background-color: var(--bg-hl); + border: solid 1px var(--border-strong); } div#cgit table.stats td { text-align: right; padding: 1px 0.5em; - border: solid 1px black; + border: solid 1px var(--border-strong); } div#cgit table.stats td.total { @@ -746,9 +829,9 @@ div#cgit table.stats td.total { } div#cgit table.stats td.sum { - color: #c00; + color: var(--red-mid); font-weight: bold; -/* background-color: #eee; */ +/* background-color: var(--bg-hl); */ } div#cgit table.stats td.left { @@ -757,14 +840,14 @@ div#cgit table.stats td.left { div#cgit table.vgraph { border-collapse: separate; - border: solid 1px black; + border: solid 1px var(--border-strong); height: 200px; } div#cgit table.vgraph th { - background-color: #eee; + background-color: var(--bg-hl); font-weight: bold; - border: solid 1px white; + border: solid 1px var(--bg); padding: 1px 0.5em; } @@ -774,18 +857,18 @@ div#cgit table.vgraph td { } div#cgit table.vgraph div.bar { - background-color: #eee; + background-color: var(--bg-hl); } div#cgit table.hgraph { - border: solid 1px black; + border: solid 1px var(--border-strong); width: 800px; } div#cgit table.hgraph th { - background-color: #eee; + background-color: var(--bg-hl); font-weight: bold; - border: solid 1px black; + border: solid 1px var(--border-strong); padding: 1px 0.5em; } @@ -795,7 +878,7 @@ div#cgit table.hgraph td { } div#cgit table.hgraph div.bar { - background-color: #eee; + background-color: var(--bg-hl); height: 1em; } @@ -808,83 +891,83 @@ div#cgit table.ssdiff td { font-family: monospace; white-space: pre; padding: 1px 4px 1px 4px; - border-left: solid 1px #aaa; - border-right: solid 1px #aaa; + border-left: solid 1px var(--border-mid); + border-right: solid 1px var(--border-mid); } div#cgit table.ssdiff td.add { - color: black; - background: #cfc; + color: var(--fg-on-tint); + background: var(--add-bg); min-width: 50%; } div#cgit table.ssdiff td.add_dark { - color: black; - background: #aca; + color: var(--fg-on-tint); + background: var(--add-bg-dark); min-width: 50%; } div#cgit table.ssdiff span.add { - background: #cfc; + background: var(--add-bg); font-weight: bold; } div#cgit table.ssdiff td.del { - color: black; - background: #fcc; + color: var(--fg-on-tint); + background: var(--del-bg); min-width: 50%; } div#cgit table.ssdiff td.del_dark { - color: black; - background: #caa; + color: var(--fg-on-tint); + background: var(--del-bg-dark); min-width: 50%; } div#cgit table.ssdiff span.del { - background: #fcc; + background: var(--del-bg); font-weight: bold; } div#cgit table.ssdiff td.changed { - color: black; - background: #ffc; + color: var(--fg-on-tint); + background: var(--changed-bg); min-width: 50%; } div#cgit table.ssdiff td.changed_dark { - color: black; - background: #cca; + color: var(--fg-on-tint); + background: var(--changed-bg-dark); min-width: 50%; } div#cgit table.ssdiff td.lineno { - color: black; - background: #eee; + color: var(--fg-on-tint); + background: var(--bg-hl); text-align: right; width: 3em; min-width: 3em; } div#cgit table.ssdiff td.hunk { - color: black; - background: #ccf; - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; + color: var(--fg-on-tint); + background: var(--hunk-bg); + border-top: solid 1px var(--border-mid); + border-bottom: solid 1px var(--border-mid); } div#cgit table.ssdiff td.head { - border-top: solid 1px #aaa; - border-bottom: solid 1px #aaa; + border-top: solid 1px var(--border-mid); + border-bottom: solid 1px var(--border-mid); } div#cgit table.ssdiff td.head div.head { font-weight: bold; - color: black; + color: var(--fg-on-tint); } div#cgit table.ssdiff td.foot { - border-top: solid 1px #aaa; + border-top: solid 1px var(--border-mid); border-left: none; border-right: none; border-bottom: none; @@ -897,3 +980,63 @@ div#cgit table.ssdiff td.space { div#cgit table.ssdiff td.space div { min-height: 3em; } + +div#cgit .hl.kwa, +div#cgit .hl.kwc { + color: var(--syn-keyword); + font-weight: bold; +} + +div#cgit .hl.kwb { + color: var(--syn-keyword-alt); +} + +div#cgit .hl.kwd { + color: var(--syn-type); +} + +div#cgit .hl.str { + color: var(--syn-string); +} + +div#cgit .hl.dstr, +div#cgit .hl.pps { + color: var(--syn-string-alt); +} + +div#cgit .hl.esc { + color: var(--syn-escape); +} + +div#cgit .hl.com, +div#cgit .hl.slc { + color: var(--syn-comment); + font-style: italic; +} + +div#cgit .hl.num { + color: var(--syn-number); +} + +div#cgit .hl.dir, +div#cgit .hl.ppc { + color: var(--syn-preproc); +} + +div#cgit .hl.opt, +div#cgit .hl.sym { + color: var(--syn-operator); +} + +div#cgit .hl.ipl { + color: var(--syn-interpolation); +} + +div#cgit .hl.lin, +div#cgit .hl.line { + color: var(--syn-linenum); +} + +div#cgit .hl.mark { + background-color: var(--syn-mark-bg); +} @@ -17,7 +17,7 @@ #include <notes.h> #include <object.h> #include <object-name.h> -#include <object-store.h> +#include <odb.h> #include <path.h> #include <refs.h> #include <revision.h> @@ -63,7 +63,7 @@ typedef enum { } diff_type; typedef enum { - ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER + ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER, CI } filter_type; struct cgit_filter { @@ -90,6 +90,9 @@ struct cgit_repo { char *extra_head_content; char *owner; char *homepage; + char *ci_url; + char *ci_branch_url; + char *ci_tag_url; char *defbranch; char *module_link; struct string_list readme; @@ -101,6 +104,7 @@ struct cgit_repo { int snapshots; int enable_blame; int enable_commit_graph; + int enable_follow_links; int enable_log_filecount; int enable_log_linecount; int enable_remote_branches; @@ -115,14 +119,12 @@ struct cgit_repo { struct cgit_filter *source_filter; struct cgit_filter *email_filter; struct cgit_filter *owner_filter; + struct cgit_filter *ci_filter; struct string_list submodules; int hide; int ignore; }; -typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name, - const char *value); - struct cgit_repolist { int length; int count; @@ -198,9 +200,12 @@ struct cgit_query { struct cgit_config { char *agefile; char *cache_root; + char *ci_url; + char *ci_branch_url; + char *ci_tag_url; char *clone_prefix; char *clone_url; - char *favicon; + struct string_list favicon; char *footer; char *head_include; char *header; @@ -276,6 +281,7 @@ struct cgit_config { struct cgit_filter *email_filter; struct cgit_filter *owner_filter; struct cgit_filter *auth_filter; + struct cgit_filter *ci_filter; }; struct cgit_page { @@ -333,7 +339,10 @@ 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); extern struct cgit_repo *cgit_get_repoinfo(const char *url); -extern void cgit_repo_config_cb(const char *name, const char *value); +extern void cgit_repo_config(struct cgit_repo *repo, const char *name, + const char *value); + +extern int cgit_have_repository(void); extern int chk_zero(int result, char *msg); extern int chk_positive(int result, char *msg); @@ -346,8 +355,7 @@ extern void strbuf_ensure_end(struct strbuf *sb, char c); extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); extern void cgit_free_reflist_inner(struct reflist *list); -extern int cgit_refs_cb(const char *refname, const char *referent UNUSED, - const struct object_id *oid, int flags, void *cb_data); +extern int cgit_refs_cb(const struct reference *ref, void *cb_data); extern void cgit_free_commitinfo(struct commitinfo *info); extern void cgit_free_taginfo(struct taginfo *info); @@ -395,7 +403,9 @@ extern void cgit_init_filters(void); extern void cgit_prepare_repo_env(struct cgit_repo * repo); -extern int readfile(const char *path, char **buf, size_t *size); +extern int read_first_line(const char *path, char **buf, size_t *size); + +extern char *strdup_first_line(const char *txt); extern char *expand_macros(const char *txt); @@ -79,6 +79,7 @@ CGIT_OBJ_NAMES += shared.o CGIT_OBJ_NAMES += ui-atom.o CGIT_OBJ_NAMES += ui-blame.o CGIT_OBJ_NAMES += ui-blob.o +CGIT_OBJ_NAMES += ui-ci.o CGIT_OBJ_NAMES += ui-clone.o CGIT_OBJ_NAMES += ui-commit.o CGIT_OBJ_NAMES += ui-diff.o diff --git a/cgit.svg b/cgit.svg new file mode 100644 index 0000000..43fee2c --- /dev/null +++ b/cgit.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="96" height="64" viewBox="0 0 96 64" fill="none" stroke="#a00" stroke-width="5"> + <title>cgit</title> + <path d="M31.5 27.5A9 9 0 0 0 22.5 18.5h-5A9 9 0 0 0 8.5 27.5V38a9 9 0 0 0 9 9h5a9 9 0 0 0 9-9M31.5 38v12.5a8 8 0 0 1-8 8H17a8 8 0 0 1-8-8M46.5 3v5m0 8v33M62.5 9v29.75a7.75 7.75 0 0 0 15.5 0V37"/> + <path stroke-width="4" d="M56 18h25"/> +</svg> diff --git a/cgitrc.5.txt b/cgitrc.5.txt index 6f3e952..527233d 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -100,6 +100,28 @@ cache-static-ttl:: version of repository pages accessed with a fixed SHA1. See also: "CACHE". Default value: -1". +ci-branch-url:: + Url template used by the "ci" tab when a branch is being viewed. If + unspecified, `ci-url` is used instead. Default value: none. See also: + "CI URL EXPANSION". + +ci-filter:: + Specifies a command which decides whether the "ci" tab is shown for + the ref currently being viewed, which is useful when only some refs + have a pipeline. Default value: none. See also: "FILTER API". + +ci-tag-url:: + Url template used by the "ci" tab when a tag is being viewed. If + unspecified, `ci-url' is used instead. Default value: none. See also: + "CI URL EXPANSION". + +ci-url:: + Url template used by the "ci" tab for refs which are not covered by + `ci-branch-url` or `ci-tag-url`. When at least one of the three ci url + settings is specified, cgit adds a "ci" tab which redirects to the + expanded url for the ref currently being viewed. Default value: none. + See also: "CI URL EXPANSION". + clone-prefix:: Space-separated list of common prefixes which, when combined with a repository url, generates valid clone urls for the repository. This @@ -218,9 +240,14 @@ enable-tree-linenumbers:: for plaintext blobs printed in the tree view. Default value: "1". favicon:: - Url used as link to a shortcut icon for cgit. It is suggested to use - the value "/favicon.ico" since certain browsers will ignore other - values. Default value: "/favicon.ico". + Url used as link to a shortcut icon for cgit. It is suggested to + always include the value "/favicon.ico" since certain browsers will + ignore other values. May be given multiple times, each favicon URL + path is added in the head section of the document in turn, letting + the browser pick the icon it prefers. The mimetype of each icon is + deduced from the extension of its url ("ico", "png", "svg", "gif", + "jpg", "jpeg", "webp" and "avif" are recognized) and emitted as a + "type" attribute. Default value: "/favicon.svg", "/favicon.ico". footer:: The content of the file specified with this option will be included @@ -250,7 +277,7 @@ local-time:: logo:: Url which specifies the source of an image which will be used as a logo - on all cgit pages. Default value: "/cgit.png". + on all cgit pages. Default value: "/cgit.svg". logo-link:: Url loaded when clicking on the cgit logo image. If unspecified the @@ -468,6 +495,22 @@ repo.branch-sort:: list, and when set to "name" enables ordering by branch name. Default value: "name". +repo.ci-branch-url:: + Override the global setting `ci-branch-url' for this repository. + Default value: <ci-branch-url>. See also: "CI URL EXPANSION". + +repo.ci-filter:: + Override the default ci-filter. Default value: none. See also: + "enable-filter-overrides". See also: "FILTER API". + +repo.ci-tag-url:: + Override the global setting `ci-tag-url' for this repository. Default + value: <ci-tag-url>. See also: "CI URL EXPANSION". + +repo.ci-url:: + Override the global setting `ci-url' for this repository. Default + value: <ci-url>. See also: "CI URL EXPANSION". + repo.clone-url:: A list of space-separated urls which can be used to clone this repo. Default value: none. See also: "MACRO EXPANSION". @@ -503,6 +546,10 @@ repo.enable-commit-graph:: A flag which can be used to disable the global setting `enable-commit-graph'. Default value: none. +repo.enable-follow-links:: + A flag which can be used to disable the global setting + `enable-follow-links'. Default value: none. + repo.enable-html-serving:: A flag which can be used to override the global setting `enable-html-serving`. Default value: none. @@ -706,6 +753,20 @@ auth filter:: Please see `filters/simple-authentication.lua` for a clear example script that may be modified. +ci filter:: + This filter is given three parameters: the name of the branch or tag + being viewed, the string "branch" or "tag" to say which of the two it + is, and the ci url that the "ci" tab would redirect to. It decides + whether that tab is shown at all, by returning zero from the exit code + / close function to show it and non-zero to hide it. Nothing is written + to its standard input, and it must not write to standard output, since + that would land in the middle of the page being rendered. + + The filter is consulted while rendering every page of the repository, + not only ref pages, so a filter which contacts the ci system should + cache its verdict and use a short timeout. Please see + `filters/ci-jenkins.sh` for an example. + commit filter:: This filter is given no arguments. The commit message text that is to be filtered is available on standard input and the filtered text is @@ -772,6 +833,46 @@ the environment variables defined in "FILTER API": - repo.clone-url +CI URL EXPANSION +---------------- +The options `ci-url', `ci-branch-url' and `ci-tag-url' (and their +`repo.'-prefixed counterparts) are url templates in which the following +tokens are substituted when the "ci" tab is followed: + +$ref:: + The name of the branch or tag currently being viewed. + +$repo:: + The url of the repository, i.e. the value of `repo.url'. + +$slug:: + $repo with any ".git" suffix removed and all slashes replaced by + dashes, e.g. "sayauz/web.git" becomes "sayauz-web". + +Two consecutive dollar signs produce a single literal one, and any other +"$" is left untouched. Tokens are substituted verbatim, so the resulting +url must already be properly encoded. + +When a tag is being viewed, `ci-tag-url' is used; for anything else +`ci-branch-url' is used. Whichever of the two applies falls back to +`ci-url' when it is unset, and the "ci" tab yields "404 Not found" when +neither is set for the current ref. This makes it possible to point at a +Jenkins multibranch pipeline, where branches and tags live at different +locations: + + ci-branch-url=https://ci.example.org/job/$slug/job/$ref + ci-tag-url=https://ci.example.org/job/$slug/view/tags/job/$ref + +Setting `repo.ci-url' discards the values which the repository would +otherwise inherit from the global `ci-branch-url' and `ci-tag-url', so +that a single repository can be pointed at a different ci system without +having to override both of them. + +Note that cgit itself never contacts the ci system, so a tab shown for a +ref which has no pipeline only reveals that after being followed. Use +`ci-filter' to suppress the tab in that case. + + CACHE ----- @@ -820,8 +921,15 @@ cache-size=1000 # Specify some default clone urls using macro expansion clone-url=git://foo.org/$CGIT_REPO_URL git@foo.org:$CGIT_REPO_URL + # Specify the css url css=/css/cgit.css +css=/css/solarized.css + + +# Add a "ci" tab pointing at a Jenkins multibranch pipeline +ci-branch-url=https://ci.foo.org/job/$slug/job/$ref +ci-tag-url=https://ci.foo.org/job/$slug/view/tags/job/$ref # Show owner on index page @@ -856,7 +964,8 @@ enable-log-linecount=1 branch-sort=age -# Add a cgit favicon +# Add cgit favicons +favicon=/favicon.svg favicon=/favicon.ico @@ -1016,3 +1125,4 @@ AUTHOR ------ Lars Hjemli <hjemli@gmail.com> Jason A. Donenfeld <Jason@zx2c4.com> +Saya Andy <saya.andy@posteo.com> @@ -1,6 +1,10 @@ /* cmd.c: the cgit command dispatcher * * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Include ci ui element * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -13,6 +17,7 @@ #include "ui-atom.h" #include "ui-blame.h" #include "ui-blob.h" +#include "ui-ci.h" #include "ui-clone.h" #include "ui-commit.h" #include "ui-diff.h" @@ -77,6 +82,11 @@ static void blob_fn(void) cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0); } +static void ci_fn(void) +{ + cgit_print_ci(); +} + static void commit_fn(void) { cgit_print_commit(ctx.qry.oid, ctx.qry.path); @@ -175,6 +185,7 @@ struct cgit_cmd *cgit_get_cmd(void) def_cmd(about, 0, 0, 0), def_cmd(blame, 1, 1, 0), def_cmd(blob, 1, 0, 0), + def_cmd(ci, 1, 0, 0), def_cmd(commit, 1, 1, 0), def_cmd(diff, 1, 1, 0), def_cmd(info, 1, 0, 1), diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..6be3df4 --- /dev/null +++ b/favicon.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52" fill="none" stroke-width="6"> + <title>cgit</title> + <g stroke="#f33"><path id="g" d="M37.5 15A9 9 0 0 0 28.5 6h-5A9 9 0 0 0 14.5 15v10.5a9 9 0 0 0 9 9h5a9 9 0 0 0 9-9M37.5 25.5V38a8 8 0 0 1-8 8H23a8 8 0 0 1-8-8"/></g> + <use href="#g" stroke="#a00" stroke-width="4"/> +</svg> @@ -1,6 +1,10 @@ /* filter.c: filter framework functions * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Integrate ci filter * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -30,12 +34,14 @@ void cgit_cleanup_filters(void) reap_filter(ctx.cfg.email_filter); reap_filter(ctx.cfg.owner_filter); reap_filter(ctx.cfg.auth_filter); + reap_filter(ctx.cfg.ci_filter); for (i = 0; i < cgit_repolist.count; ++i) { reap_filter(cgit_repolist.repos[i].about_filter); reap_filter(cgit_repolist.repos[i].commit_filter); reap_filter(cgit_repolist.repos[i].source_filter); reap_filter(cgit_repolist.repos[i].email_filter); reap_filter(cgit_repolist.repos[i].owner_filter); + reap_filter(cgit_repolist.repos[i].ci_filter); } } @@ -114,7 +120,7 @@ static struct cgit_filter *new_exec_filter(const char *cmd, int argument_count) f = xmalloc(sizeof(*f)); /* We leave argv for now and assign it below. */ - cgit_exec_filter_init(f, xstrdup(cmd), NULL); + cgit_exec_filter_init(f, strdup_first_line(cmd), NULL); f->base.argument_count = argument_count; args_size = (2 + argument_count) * sizeof(char *); f->argv = xmalloc(args_size); @@ -356,7 +362,7 @@ static struct cgit_filter *new_lua_filter(const char *cmd, int argument_count) filter->base.fprintfp = fprintf_lua_filter; filter->base.cleanup = cleanup_lua_filter; filter->base.argument_count = argument_count; - filter->script_file = xstrdup(cmd); + filter->script_file = strdup_first_line(cmd); return &filter->base; } @@ -402,7 +408,7 @@ static const struct { struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) { - char *colon; + const char *colon; int i; size_t len; int argument_count; @@ -424,6 +430,10 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) argument_count = 12; break; + case CI: + argument_count = 3; + break; + case EMAIL: argument_count = 2; break; diff --git a/filters/ci-jenkins.sh b/filters/ci-jenkins.sh new file mode 100755 index 0000000..842af21 --- /dev/null +++ b/filters/ci-jenkins.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# This script may be used with the ci-filter or repo.ci-filter setting in +# cgitrc to hide the "ci" tab for refs which have no pipeline on a Jenkins +# instance. +# +# Arguments: +# $1 the name of the branch or tag being viewed +# $2 "branch" or "tag" +# $3 the ci url which the "ci" tab would redirect to +# +# Exit with a zero status to show the tab, non-zero to hide it. This script +# must not write anything to standard output, as that would end up in the +# middle of the page cgit is rendering. +# +# The filter is consulted while rendering every repository page, so the +# verdict is cached on disk to keep Jenkins from being hammered, and the +# probe is given a short timeout so that an unreachable Jenkins degrades +# into a missing tab rather than a hanging web server. +# +# Set CI_NETRC to a netrc(5) file if the Jenkins instance requires +# authentication; without it a private job answers 403 and the tab is +# hidden even though the pipeline exists. + +CI_CACHE_DIR="${CI_CACHE_DIR:-/var/cache/cgit/ci-filter}" +CI_CACHE_TTL_MINUTES="${CI_CACHE_TTL_MINUTES:-5}" +CI_TIMEOUT="${CI_TIMEOUT:-2}" + +url="$3" +test -n "$url" || exit 1 + +# Jenkins job pages are often not readable anonymously, so query the REST +# API rather than the page the tab points at. +probe="$url/api/json?tree=name" + +key="$(printf '%s' "$url" | cksum | tr -cd '0-9')" +cache="$CI_CACHE_DIR/$key" + +mkdir -p "$CI_CACHE_DIR" 2>/dev/null + +if test -f "$cache" && + test -z "$(find "$cache" -mmin "+$CI_CACHE_TTL_MINUTES" 2>/dev/null)" +then + exit "$(cat "$cache")" +fi + +status=0 +curl --silent --fail --head --output /dev/null \ + --max-time "$CI_TIMEOUT" \ + ${CI_NETRC:+--netrc-file "$CI_NETRC"} \ + "$probe" >/dev/null 2>&1 || status=1 + +if test -d "$CI_CACHE_DIR" +then + printf '%s\n' "$status" >"$cache.$$" 2>/dev/null && + mv "$cache.$$" "$cache" 2>/dev/null +fi + +exit "$status" diff --git a/filters/html-converters/md2html b/filters/html-converters/md2html index 59f43a8..4f6ad37 100755 --- a/filters/html-converters/md2html +++ b/filters/html-converters/md2html @@ -2,8 +2,46 @@ import markdown import sys import io -from pygments.formatters import HtmlFormatter from markdown.extensions.toc import TocExtension + +# Pygments' token classes, mapped onto the --syn-* custom properties that +# cgit.css defines. The fallbacks are the palette those properties default to, +# so that the output stays readable under a stylesheet without them. +SYNTAX_COLORS = ( + (('k', 'kc', 'kd', 'kn', 'kr'), '--syn-keyword', '#000', + 'font-weight: bold'), + (('kp',), '--syn-keyword', '#000', None), + (('kt',), '--syn-type', '#010181', None), + (('nc', 'nf', 'nn', 'ne'), '--syn-keyword-alt', '#0057ae', None), + (('s', 'sa', 'sb', 'sc', 'dl', 'sd', 's2', 'sh', 'sx', 'sr', 's1', 'ss'), + '--syn-string', '#bf0303', None), + (('se',), '--syn-escape', '#f0f', None), + (('si',), '--syn-interpolation', '#0057ae', None), + (('c', 'ch', 'cm', 'c1', 'cs'), '--syn-comment', '#838183', + 'font-style: italic'), + (('cp', 'cpf'), '--syn-preproc', '#008200', None), + (('m', 'mb', 'mf', 'mh', 'mi', 'mo', 'il'), '--syn-number', '#b07e00', + None), + (('o', 'ow', 'p'), '--syn-operator', '#000', None), + (('err',), '--syn-error', '#bf0303', None), + (('gd',), '--red', 'red', None), + (('gi',), '--green', 'green', None), + (('lineno', 'linenos'), '--syn-linenum', '#555', None), +) + + +def style_defs(prefix='.highlight'): + rules = [] + for classes, prop, fallback, extra in SYNTAX_COLORS: + selector = ', '.join('%s .%s' % (prefix, c) for c in classes) + decls = 'color: var(%s, %s)' % (prop, fallback) + if extra: + decls += '; ' + extra + rules.append('%s { %s }' % (selector, decls)) + rules.append('%s .hll { background-color: var(--syn-mark-bg, #ffb) }' + % prefix) + return '\n'.join(rules) + '\n' + sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') sys.stdout.write(''' @@ -20,7 +58,7 @@ sys.stdout.write(''' margin-bottom: 0 !important; } .markdown-body a.absent { - color: #c00; + color: var(--red-mid, #c00); } .markdown-body a.anchor { display: block; @@ -42,7 +80,7 @@ sys.stdout.write(''' } .markdown-body h1 .mini-icon-link, .markdown-body h2 .mini-icon-link, .markdown-body h3 .mini-icon-link, .markdown-body h4 .mini-icon-link, .markdown-body h5 .mini-icon-link, .markdown-body h6 .mini-icon-link { display: none; - color: #000; + color: var(--fg-strong, #000); } .markdown-body h1:hover a.anchor, .markdown-body h2:hover a.anchor, .markdown-body h3:hover a.anchor, .markdown-body h4:hover a.anchor, .markdown-body h5:hover a.anchor, .markdown-body h6:hover a.anchor { text-decoration: none; @@ -55,19 +93,19 @@ sys.stdout.write(''' display: inline-block; } div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div#cgit .markdown-body h3 a.toclink, div#cgit .markdown-body h4 a.toclink, div#cgit .markdown-body h5 a.toclink, div#cgit .markdown-body h6 a.toclink { - color: black; + color: var(--fg-strong, #000); } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { font-size: inherit; } .markdown-body h1 { font-size: 28px; - color: #000; + color: var(--fg-strong, #000); } .markdown-body h2 { font-size: 24px; - border-bottom: 1px solid #ccc; - color: #000; + border-bottom: 1px solid var(--border, #ccc); + color: var(--fg-strong, #000); } .markdown-body h3 { font-size: 18px; @@ -79,14 +117,14 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# font-size: 14px; } .markdown-body h6 { - color: #777; + color: var(--fg-mute, #777); font-size: 14px; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre { margin: 15px 0; } .markdown-body hr { - border: 2px solid #ccc; + border: 2px solid var(--border, #ccc); } .markdown-body>h2:first-child, .markdown-body>h1:first-child, .markdown-body>h1:first-child+h2, .markdown-body>h3:first-child, .markdown-body>h4:first-child, .markdown-body>h5:first-child, .markdown-body>h6:first-child { margin-top: 0; @@ -148,9 +186,9 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# margin-bottom: 0px; } .markdown-body blockquote { - border-left: 4px solid #DDD; + border-left: 4px solid var(--border, #ddd); padding: 0 15px; - color: #777; + color: var(--fg-mute, #777); } .markdown-body blockquote>:first-child { margin-top: 0px; @@ -162,15 +200,15 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# font-weight: bold; } .markdown-body table th, .markdown-body table td { - border: 1px solid #ccc; + border: 1px solid var(--border, #ccc); padding: 6px 13px; } .markdown-body table tr { - border-top: 1px solid #ccc; - background-color: #fff; + border-top: 1px solid var(--border, #ccc); + background-color: var(--bg, #fff); } .markdown-body table tr:nth-child(2n) { - background-color: #f8f8f8; + background-color: var(--bg-zebra, #f8f8f8); } .markdown-body img { max-width: 100%; @@ -182,7 +220,7 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# overflow: hidden; } .markdown-body span.frame>span { - border: 1px solid #ddd; + border: 1px solid var(--border, #ddd); display: block; float: left; overflow: hidden; @@ -196,7 +234,7 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# } .markdown-body span.frame span span { clear: both; - color: #333; + color: var(--fg, #333); display: block; padding: 5px 0 0; } @@ -254,8 +292,8 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# .markdown-body code, .markdown-body tt { margin: 0 2px; padding: 0px 5px; - border: 1px solid #eaeaea; - background-color: #f8f8f8; + border: 1px solid var(--border, #eaeaea); + background-color: var(--bg-zebra, #f8f8f8); border-radius: 3px; } .markdown-body code { @@ -269,8 +307,8 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# background: transparent; } .markdown-body .highlight pre, .markdown-body pre { - background-color: #f8f8f8; - border: 1px solid #ccc; + background-color: var(--bg-zebra, #f8f8f8); + border: 1px solid var(--border, #ccc); font-size: 13px; line-height: 19px; overflow: auto; @@ -284,9 +322,9 @@ div#cgit .markdown-body h1 a.toclink, div#cgit .markdown-body h2 a.toclink, div# border: none; } ''') -sys.stdout.write(HtmlFormatter(style='pastie').get_style_defs('.highlight')) +sys.stdout.write(style_defs('.highlight')) sys.stdout.write(''' -</style> +</style> ''') sys.stdout.write("<div class='markdown-body'>") sys.stdout.flush() diff --git a/filters/syntax-highlighting.py b/filters/syntax-highlighting.py index e912594..d757e2d 100755 --- a/filters/syntax-highlighting.py +++ b/filters/syntax-highlighting.py @@ -30,11 +30,50 @@ from pygments.lexers import guess_lexer_for_filename from pygments.formatters import HtmlFormatter +# Pygments' token classes, mapped onto the --syn-* custom properties that +# cgit.css defines. The fallbacks are the palette those properties default to, +# so that the output stays readable under a stylesheet without them. +SYNTAX_COLORS = ( + (('k', 'kc', 'kd', 'kn', 'kr'), '--syn-keyword', '#000', + 'font-weight: bold'), + (('kp',), '--syn-keyword', '#000', None), + (('kt',), '--syn-type', '#010181', None), + (('nc', 'nf', 'nn', 'ne'), '--syn-keyword-alt', '#0057ae', None), + (('s', 'sa', 'sb', 'sc', 'dl', 'sd', 's2', 'sh', 'sx', 'sr', 's1', 'ss'), + '--syn-string', '#bf0303', None), + (('se',), '--syn-escape', '#f0f', None), + (('si',), '--syn-interpolation', '#0057ae', None), + (('c', 'ch', 'cm', 'c1', 'cs'), '--syn-comment', '#838183', + 'font-style: italic'), + (('cp', 'cpf'), '--syn-preproc', '#008200', None), + (('m', 'mb', 'mf', 'mh', 'mi', 'mo', 'il'), '--syn-number', '#b07e00', + None), + (('o', 'ow', 'p'), '--syn-operator', '#000', None), + (('err',), '--syn-error', '#bf0303', None), + (('gd',), '--red', 'red', None), + (('gi',), '--green', 'green', None), + (('lineno', 'linenos'), '--syn-linenum', '#555', None), +) + + +def style_defs(prefix='.highlight'): + rules = [] + for classes, prop, fallback, extra in SYNTAX_COLORS: + selector = ', '.join('%s .%s' % (prefix, c) for c in classes) + decls = 'color: var(%s, %s)' % (prop, fallback) + if extra: + decls += '; ' + extra + rules.append('%s { %s }' % (selector, decls)) + rules.append('%s .hll { background-color: var(--syn-mark-bg, #ffb) }' + % prefix) + return '\n'.join(rules) + '\n' + + sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8', errors='replace') sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') data = sys.stdin.read() filename = sys.argv[1] -formatter = HtmlFormatter(style='pastie', nobackground=True) +formatter = HtmlFormatter(nobackground=True) try: lexer = guess_lexer_for_filename(filename, data) @@ -48,8 +87,8 @@ except TypeError: lexer = TextLexer() # highlight! :-) -# printout pygments' css definitions as well +# printout the css definitions for pygments' token classes as well sys.stdout.write('<style>') -sys.stdout.write(formatter.get_style_defs('.highlight')) +sys.stdout.write(style_defs('.highlight')) sys.stdout.write('</style>') sys.stdout.write(highlight(data, lexer, formatter, outfile=None)) diff --git a/filters/syntax-highlighting.sh b/filters/syntax-highlighting.sh index 840bc34..bb8d60c 100755 --- a/filters/syntax-highlighting.sh +++ b/filters/syntax-highlighting.sh @@ -8,8 +8,10 @@ # might have to use an external call to sed instead. # # Note: the highlight command (http://www.andre-simon.de/) uses css for syntax -# highlighting, so you'll probably want something like the following included -# in your css file: +# highlighting. cgit.css defines these classes in terms of its --syn-* custom +# properties, so nothing has to be added when using the default stylesheet. +# The palette below, from highlight 3.13, is what those properties default to; +# a custom stylesheet wanting the same colors can use it directly: # # Style definition file generated by highlight 2.4.8, http://www.andre-simon.de/ # @@ -114,8 +116,12 @@ EXTENSION="${BASENAME##*.}" # Version 2 can be found (for example) on EPEL 5, while version 3 can be # found (for example) on EPEL 6. # +# Note that -I (--include-style) is not used: the stylesheet it embeds in the +# output would override the .hl rules in cgit.css and with them any theming +# done through the --syn-* custom properties. +# # This is for version 2 -exec highlight --force -f -I -X -S "$EXTENSION" 2>/dev/null +exec highlight --force -f -X -S "$EXTENSION" 2>/dev/null # This is for version 3 -#exec highlight --force -f -I -O xhtml -S "$EXTENSION" 2>/dev/null +#exec highlight --force -f -O xhtml -S "$EXTENSION" 2>/dev/null diff --git a/gen-version.sh b/gen-version.sh index 80cf49a..b493cf5 100755 --- a/gen-version.sh +++ b/gen-version.sh @@ -3,10 +3,24 @@ # Get version-info specified in Makefile V=$1 -# Use `git describe` to get current version if we're inside a git repo +# When building from a git repository, derive the version from the most recent +# release tag instead. The result follows Semantic Versioning 2.0.0, using the +# number of commits made since that tag as build metadata, so seven commits +# after the v1.3.1 tag gives "v1.3.1+7", while a build of the tag itself gives +# plain "v1.3.1". Release tags are expected to be named vMAJOR.MINOR.PATCH. if test "$(git rev-parse --git-dir 2>/dev/null)" = '.git' then - V=$(git describe --abbrev=4 HEAD 2>/dev/null) + tag=$(git describe --abbrev=0 --match 'v[0-9]*' HEAD 2>/dev/null) + if test -n "$tag" + then + ahead=$(git rev-list --count "$tag..HEAD" 2>/dev/null) + if test -n "$ahead" && test "$ahead" -gt 0 + then + V="$tag+$ahead" + else + V="$tag" + fi + fi fi new="CGIT_VERSION = $V" diff --git a/git b/git -Subproject d82adb61ba2fd11d8f2587fca1b6bd7925ce404 +Subproject 94f057755b7941b321fd11fec1b2e3ca5313a4e @@ -19,7 +19,7 @@ */ void cgit_parse_url(const char *url) { - char *c, *cmd, *p; + char *c, *cmd, *p, *buf; struct cgit_repo *repo; if (!url || url[0] == '\0') @@ -32,11 +32,12 @@ void cgit_parse_url(const char *url) return; } + buf = xstrdup(url); cmd = NULL; - c = strchr(url, '/'); + c = strchr(buf, '/'); while (c) { c[0] = '\0'; - repo = cgit_get_repoinfo(url); + repo = cgit_get_repoinfo(buf); if (repo) { ctx.repo = repo; cmd = c; @@ -56,6 +57,7 @@ void cgit_parse_url(const char *url) if (cmd[1]) ctx.qry.page = xstrdup(cmd + 1); } + free(buf); } static char *substr(const char *head, const char *tail) @@ -200,7 +202,7 @@ struct taginfo *cgit_parse_tag(struct tag *tag) const char *p; struct taginfo *ret = NULL; - data = repo_read_object_file(the_repository, &tag->object.oid, &type, &size); + data = odb_read_object(the_repository->objects, &tag->object.oid, &type, &size); if (!data || type != OBJ_TAG) goto cleanup; diff --git a/scan-tree.c b/scan-tree.c index 84da86e..c120efe 100644 --- a/scan-tree.c +++ b/scan-tree.c @@ -47,27 +47,27 @@ out: } static struct cgit_repo *repo; -static repo_config_fn config_fn; static void scan_tree_repo_config(const char *name, const char *value) { - config_fn(repo, name, value); + cgit_repo_config(repo, name, value); } -static int gitconfig_config(const char *key, const char *value, const struct config_context *, void *cb) +static int gitconfig_config(const char *key, const char *value, + const __attribute__((unused)) struct config_context *ctx, void *cb) { const char *name; if (!strcmp(key, "gitweb.owner")) - config_fn(repo, "owner", value); + cgit_repo_config(repo, "owner", value); else if (!strcmp(key, "gitweb.description")) - config_fn(repo, "desc", value); + cgit_repo_config(repo, "desc", value); else if (!strcmp(key, "gitweb.category")) - config_fn(repo, "section", value); + cgit_repo_config(repo, "section", value); else if (!strcmp(key, "gitweb.homepage")) - config_fn(repo, "homepage", value); + cgit_repo_config(repo, "homepage", value); else if (skip_prefix(key, "cgit.", &name)) - config_fn(repo, name, value); + cgit_repo_config(repo, name, value); return 0; } @@ -79,7 +79,7 @@ static char *xstrrchr(char *s, char *from, int c) return from < s ? NULL : from; } -static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) +static void add_repo(const char *base, struct strbuf *path) { struct stat st; struct passwd *pwd; @@ -121,7 +121,6 @@ static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) strbuf_setlen(&rel, rel.len - 1); repo = cgit_add_repo(rel.buf); - config_fn = fn; if (ctx.cfg.enable_git_config) { strbuf_addstr(path, "config"); git_config_from_file(gitconfig_config, path->buf, NULL); @@ -134,7 +133,7 @@ static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) strip_suffix_mem(repo->url, &urllen, "/"); repo->url[urllen] = '\0'; } - repo->path = xstrdup(path->buf); + repo->path = strdup_first_line(path->buf); while (!repo->owner) { if ((pwd = getpwuid(st.st_uid)) == NULL) { fprintf(stderr, "Error reading owner-info for %s: %s (%d)\n", @@ -144,13 +143,13 @@ static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) if (pwd->pw_gecos) if ((p = strchr(pwd->pw_gecos, ','))) *p = '\0'; - repo->owner = xstrdup(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); + repo->owner = strdup_first_line(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); } if (repo->desc == cgit_default_repo_desc || !repo->desc) { strbuf_addstr(path, "description"); if (!stat(path->buf, &st)) - readfile(path->buf, &repo->desc, &size); + read_first_line(path->buf, &repo->desc, &size); strbuf_setlen(path, pathlen); } @@ -167,7 +166,7 @@ static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) } if (slash && !n) { *slash = '\0'; - repo->section = xstrdup(rel.buf); + repo->section = strdup_first_line(rel.buf); *slash = '/'; if (starts_with(repo->name, repo->section)) { repo->name += strlen(repo->section); @@ -184,7 +183,7 @@ static void add_repo(const char *base, struct strbuf *path, repo_config_fn fn) strbuf_release(&rel); } -static void scan_path(const char *base, const char *path, repo_config_fn fn) +static void scan_path(const char *base, const char *path) { DIR *dir = opendir(path); struct dirent *ent; @@ -200,12 +199,12 @@ static void scan_path(const char *base, const char *path, repo_config_fn fn) strbuf_add(&pathbuf, path, strlen(path)); if (is_git_dir(pathbuf.buf)) { - add_repo(base, &pathbuf, fn); + add_repo(base, &pathbuf); goto end; } strbuf_addstr(&pathbuf, "/.git"); if (is_git_dir(pathbuf.buf)) { - add_repo(base, &pathbuf, fn); + add_repo(base, &pathbuf); goto end; } /* @@ -230,14 +229,14 @@ static void scan_path(const char *base, const char *path, repo_config_fn fn) continue; } if (S_ISDIR(st.st_mode)) - scan_path(base, pathbuf.buf, fn); + scan_path(base, pathbuf.buf); } end: strbuf_release(&pathbuf); closedir(dir); } -void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn) +void scan_projects(const char *path, const char *projectsfile) { struct strbuf line = STRBUF_INIT; FILE *projects; @@ -254,7 +253,7 @@ void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn continue; strbuf_insert(&line, 0, "/", 1); strbuf_insert(&line, 0, path, strlen(path)); - scan_path(path, line.buf, fn); + scan_path(path, line.buf); } if ((err = ferror(projects))) { fprintf(stderr, "Error reading from projectsfile %s: %s (%d)\n", @@ -264,7 +263,7 @@ void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn strbuf_release(&line); } -void scan_tree(const char *path, repo_config_fn fn) +void scan_tree(const char *path) { - scan_path(path, path, fn); + scan_path(path, path); } diff --git a/scan-tree.h b/scan-tree.h index 1afbd4b..def0a7a 100644 --- a/scan-tree.h +++ b/scan-tree.h @@ -1,2 +1,2 @@ -extern void scan_projects(const char *path, const char *projectsfile, repo_config_fn fn); -extern void scan_tree(const char *path, repo_config_fn fn); +extern void scan_projects(const char *path, const char *projectsfile); +extern void scan_tree(const char *path); @@ -1,6 +1,11 @@ /* shared.c: global vars + some callback functions * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Add cgit_have_repository() + * Add ci filter configuration * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -13,6 +18,11 @@ struct cgit_repolist cgit_repolist; struct cgit_context ctx; +int cgit_have_repository(void) +{ + return startup_info->have_repository; +} + int chk_zero(int result, char *msg) { if (result != 0) @@ -52,16 +62,21 @@ struct cgit_repo *cgit_add_repo(const char *url) ret = &cgit_repolist.repos[cgit_repolist.count-1]; memset(ret, 0, sizeof(struct cgit_repo)); ret->url = trim_end(url, '/'); + *strchrnul(ret->url, '\n') = '\0'; ret->name = ret->url; ret->path = NULL; ret->desc = cgit_default_repo_desc; ret->extra_head_content = NULL; ret->owner = NULL; ret->homepage = NULL; + ret->ci_url = ctx.cfg.ci_url; + ret->ci_branch_url = ctx.cfg.ci_branch_url; + ret->ci_tag_url = ctx.cfg.ci_tag_url; ret->section = ctx.cfg.section; ret->snapshots = ctx.cfg.snapshots; ret->enable_blame = ctx.cfg.enable_blame; 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; @@ -78,6 +93,7 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->source_filter = ctx.cfg.source_filter; ret->email_filter = ctx.cfg.email_filter; ret->owner_filter = ctx.cfg.owner_filter; + ret->ci_filter = ctx.cfg.ci_filter; ret->clone_url = ctx.cfg.clone_url; ret->submodules.strdup_strings = 1; ret->hide = ret->ignore = 0; @@ -212,11 +228,10 @@ void cgit_free_reflist_inner(struct reflist *list) free(list->refs); } -int cgit_refs_cb(const char *refname, const char *referent UNUSED, - const struct object_id *oid, int flags, void *cb_data) +int cgit_refs_cb(const struct reference *ref, void *cb_data) { struct reflist *list = (struct reflist *)cb_data; - struct refinfo *info = cgit_mk_refinfo(refname, oid); + struct refinfo *info = cgit_mk_refinfo(ref->name, ref->oid); if (info) cgit_add_ref(list, info); @@ -243,7 +258,7 @@ static int load_mmfile(mmfile_t *file, const struct object_id *oid) file->ptr = (char *)""; file->size = 0; } else { - file->ptr = repo_read_object_file(the_repository, oid, &type, + file->ptr = odb_read_object(the_repository->objects, oid, &type, (unsigned long *)&file->size); } return 1; @@ -395,7 +410,7 @@ int cgit_parse_snapshots_mask(const char *str) if (strcmp(str, "all") == 0) return INT_MAX; - string_list_split(&tokens, str, ' ', -1); + string_list_split(&tokens, str, " ", -1); string_list_remove_empty_items(&tokens, 0); for_each_string_list_item(item, &tokens) { @@ -440,9 +455,10 @@ void cgit_prepare_repo_env(struct cgit_repo * repo) } /* Read the content of the specified file into a newly allocated buffer, - * zeroterminate the buffer and return 0 on success, errno otherwise. + * zeroterminate the buffer, truncate at a new line, and return 0 on success, + * errno otherwise. */ -int readfile(const char *path, char **buf, size_t *size) +int read_first_line(const char *path, char **buf, size_t *size) { int fd, e; struct stat st; @@ -463,10 +479,18 @@ int readfile(const char *path, char **buf, size_t *size) *size = read_in_full(fd, *buf, st.st_size); e = errno; (*buf)[*size] = '\0'; + *strchrnul(*buf, '\n') = '\0'; close(fd); return (*size == st.st_size ? 0 : e); } +char *strdup_first_line(const char *txt) +{ + char *t = xstrdup(txt); + *strchrnul(t, '\n') = '\0'; + return t; +} + static int is_token_char(char c) { return isalnum(c) || c == '_'; @@ -541,7 +565,8 @@ char *expand_macros(const char *txt) char *get_mimetype_for_filename(const char *filename) { - char *ext, *mimetype, line[1024]; + const char *ext; + char *mimetype, line[1024]; struct string_list list = STRING_LIST_INIT_NODUP; int i; FILE *file; diff --git a/solarized.css b/solarized.css new file mode 100644 index 0000000..2e6e3d4 --- /dev/null +++ b/solarized.css @@ -0,0 +1,140 @@ +/* + * Solarized theme (Ethan Schoonover) with automatic light/dark selection + * based on the prefers-color-scheme media query. + * + * Must be listed after cgit.css, e.g. + * + * css=/cgit.css + * css=/solarized.css + */ + +:root { + --sol-base03: #002b36; + --sol-base02: #073642; + --sol-base01: #586e75; + --sol-base00: #657b83; + --sol-base0: #839496; + --sol-base1: #93a1a1; + --sol-base2: #eee8d5; + --sol-base3: #fdf6e3; + + --sol-yellow: #b58900; + --sol-orange: #cb4b16; + --sol-red: #dc322f; + --sol-magenta: #d33682; + --sol-violet: #6c71c4; + --sol-blue: #268bd2; + --sol-cyan: #2aa198; + --sol-green: #859900; + + --sol-canvas: var(--sol-base3); + --sol-panel: var(--sol-base2); + --sol-body: var(--sol-base00); + --sol-emph: var(--sol-base01); + --sol-muted: var(--sol-base0); + --sol-secondary: var(--sol-base1); + --sol-shade: var(--sol-base03); + --sol-page: var(--sol-panel); + + --sol-tint-soft: 18%; + --sol-tint: 25%; + --sol-tint-strong: 45%; + + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --sol-canvas: var(--sol-base03); + --sol-panel: var(--sol-base02); + --sol-body: var(--sol-base0); + --sol-emph: var(--sol-base1); + --sol-muted: var(--sol-base00); + --sol-secondary: var(--sol-base01); + --sol-shade: var(--sol-base3); + + --sol-tint-soft: 30%; + --sol-tint: 35%; + --sol-tint-strong: 55%; + } +} + +body:has(> div#cgit) { + margin: 0; + background-color: var(--sol-page); +} + +div#cgit { + --bg: var(--sol-canvas); + --bg-zebra: color-mix(in srgb, var(--sol-panel) 40%, var(--sol-canvas)); + --bg-hl: var(--sol-panel); + --bg-hl2: color-mix(in srgb, var(--sol-panel) 85%, var(--sol-shade)); + --fg: var(--sol-body); + --fg-strong: var(--sol-emph); + --fg-mid: var(--sol-body); + --fg-mute: var(--sol-muted); + --fg-dim: var(--sol-secondary); + --fg-faint: color-mix(in srgb, var(--sol-secondary) 65%, var(--sol-canvas)); + --fg-faintest: color-mix(in srgb, var(--sol-secondary) 45%, var(--sol-canvas)); + --fg-on-tint: var(--sol-emph); + --fg-linenum: var(--sol-secondary); + --border: var(--sol-panel); + --border-mid: var(--sol-secondary); + --border-dark: var(--sol-muted); + --border-strong: var(--sol-emph); + + --red: var(--sol-red); + --red-mid: color-mix(in srgb, var(--sol-red) 75%, var(--sol-shade)); + --red-dark: color-mix(in srgb, var(--sol-red) 55%, var(--sol-shade)); + --green: var(--sol-green); + --green-mid: color-mix(in srgb, var(--sol-green) 70%, var(--sol-shade)); + --green-dark: color-mix(in srgb, var(--sol-green) 50%, var(--sol-shade)); + --blue: var(--sol-blue); + --blue-dark: color-mix(in srgb, var(--sol-blue) 70%, var(--sol-shade)); + + --graph-col1: var(--sol-red); + --graph-col2: var(--sol-green); + --graph-col3: var(--sol-yellow); + --graph-col4: var(--sol-blue); + --graph-col5: var(--sol-magenta); + --graph-col6: var(--sol-cyan); + + --graph-add-bg: var(--sol-green); + --graph-rem-bg: var(--sol-red); + + --notes-bg: color-mix(in srgb, var(--sol-yellow) var(--sol-tint-soft), var(--sol-canvas)); + --notes-border: var(--sol-yellow); + --add-bg: color-mix(in srgb, var(--sol-green) var(--sol-tint), var(--sol-canvas)); + --add-bg-dark: color-mix(in srgb, var(--sol-green) var(--sol-tint-strong), var(--sol-canvas)); + --del-bg: color-mix(in srgb, var(--sol-red) var(--sol-tint), var(--sol-canvas)); + --del-bg-dark: color-mix(in srgb, var(--sol-red) var(--sol-tint-strong), var(--sol-canvas)); + --changed-bg: color-mix(in srgb, var(--sol-yellow) var(--sol-tint), var(--sol-canvas)); + --changed-bg-dark: color-mix(in srgb, var(--sol-yellow) var(--sol-tint-strong), var(--sol-canvas)); + --hunk-bg: color-mix(in srgb, var(--sol-blue) var(--sol-tint), var(--sol-canvas)); + + --deco-branch-bg: color-mix(in srgb, var(--sol-green) var(--sol-tint-soft), var(--sol-canvas)); + --deco-branch-border: var(--sol-green); + --deco-tag-bg: color-mix(in srgb, var(--sol-yellow) var(--sol-tint-soft), var(--sol-canvas)); + --deco-tag-border: var(--sol-yellow); + --deco-tag-annotated-bg: color-mix(in srgb, var(--sol-orange) var(--sol-tint-soft), var(--sol-canvas)); + --deco-tag-annotated-border: var(--sol-yellow); + --deco-remote-bg: color-mix(in srgb, var(--sol-violet) var(--sol-tint-soft), var(--sol-canvas)); + --deco-remote-border: var(--sol-violet); + --deco-other-bg: color-mix(in srgb, var(--sol-red) var(--sol-tint-soft), var(--sol-canvas)); + --deco-other-border: var(--sol-red); + + --syn-keyword: var(--sol-emph); + --syn-keyword-alt: var(--sol-blue); + --syn-type: var(--blue-dark); + --syn-string: var(--sol-red); + --syn-string-alt: var(--sol-green); + --syn-escape: var(--sol-magenta); + --syn-comment: var(--sol-secondary); + --syn-number: var(--sol-yellow); + --syn-preproc: var(--green-mid); + --syn-operator: var(--sol-emph); + --syn-interpolation: var(--sol-blue); + --syn-linenum: var(--sol-secondary); + --syn-error: var(--sol-red); + --syn-mark-bg: color-mix(in srgb, var(--sol-yellow) var(--sol-tint), var(--sol-canvas)); +} diff --git a/tests/t0106-diff.sh b/tests/t0106-diff.sh index 82b645e..0c167d4 100755 --- a/tests/t0106-diff.sh +++ b/tests/t0106-diff.sh @@ -16,4 +16,60 @@ test_expect_success 'find added line' ' grep "<div class=.add.>+5</div>" tmp ' +test_expect_success 'ref picker: from: select present' ' + grep "<select name=.id2." tmp +' + +test_expect_success 'ref picker: to: select present' ' + grep "<select name=.id." tmp +' + +test_expect_success 'ref picker: from: label present' ' + grep "from:" tmp +' + +test_expect_success 'ref picker: to: label present' ' + grep "to:" tmp +' + +test_expect_success 'ref picker: master branch listed' ' + grep "master" tmp +' + +test_expect_success 'ref picker: auto-submit on change' ' + grep "onchange=.this.form.submit()." tmp +' + +test_expect_success 'compare two refs: foo/diff?id=master&id2=HEAD' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp +' + +test_expect_success 'diff with explicit refs shows diffstat' ' + grep "diffstat" tmp +' + +test_expect_success 'diff with oid2 parameter uses it for old ref' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp && + grep "from:" tmp +' + +test_expect_success 'ref picker: to: shows master when id=master' ' + cgit_query "url=foo/diff&id=master" >tmp && + grep "master" tmp +' + +test_expect_success 'ref picker: from: empty when no id2' ' + cgit_query "url=foo/diff" >tmp && + grep "<select name=.id2." tmp +' + +test_expect_success 'compare initial commit with HEAD' ' + root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) && + cgit_query "url=foo/diff&id=HEAD&id2=$root" >tmp +' + +test_expect_success 'diff with HEAD and root shows content' ' + grep "diffstat" tmp +' + test_done diff --git a/tests/t0112-ci.sh b/tests/t0112-ci.sh new file mode 100644 index 0000000..33609d6 --- /dev/null +++ b/tests/t0112-ci.sh @@ -0,0 +1,127 @@ +#!/bin/sh + +test_description='Check content on ci page' +. ./setup.sh + +# The ci settings are only inherited by repositories which are declared +# after them, which is used below to get a repository with no ci url and +# one with a tag-only ci url. +cat >cgitrc.ci <<EOF +virtual-root=/ +cache-root=$PWD/cache +cache-size=0 +enable-filter-overrides=1 + +repo.url=tagonly +repo.path=$PWD/repos/foo/.git +repo.ci-tag-url=https://ci.example.org/tagonly/\$ref + +ci-branch-url=https://ci.example.org/job/\$slug/job/\$ref +ci-tag-url=https://ci.example.org/job/\$slug/view/tags/job/\$ref + +repo.url=foo +repo.path=$PWD/repos/foo/.git + +repo.url=sayauz/web.git +repo.path=$PWD/repos/foo/.git + +repo.url=override +repo.path=$PWD/repos/foo/.git +repo.ci-url=https://ci.example.org/override/\$repo/\$\$/\$ref + +repo.url=filtered +repo.path=$PWD/repos/foo/.git +repo.ci-filter=exec:$PWD/ci-filter.sh +EOF + +# Records its arguments and only accepts tags. +cat >ci-filter.sh <<EOF +#!/bin/sh +printf '%s|%s|%s\n' "\$1" "\$2" "\$3" >>"$PWD/ci-filter.log" +test "\$2" = tag +EOF +chmod +x ci-filter.sh + +git --git-dir="$PWD/repos/foo/.git" tag v0.17.3.2 master + +cgit_ci() +{ + CGIT_CONFIG="$PWD/cgitrc.ci" QUERY_STRING="url=$1" cgit +} + +test_expect_success 'redirect to the branch ci url' ' + cgit_ci "foo/ci&h=master" >tmp && + grep "^Status: 302 Found$" tmp && + grep "^Location: https://ci.example.org/job/foo/job/master$" tmp +' + +test_expect_success 'redirect to the tag ci url' ' + cgit_ci "foo/ci&h=v0.17.3.2" >tmp && + grep "^Status: 302 Found$" tmp && + grep "^Location: https://ci.example.org/job/foo/view/tags/job/v0.17.3.2$" tmp +' + +test_expect_success 'redirect to the default branch ci url' ' + cgit_ci "foo/ci" >tmp && + grep "^Location: https://ci.example.org/job/foo/job/master$" tmp +' + +test_expect_success '$slug strips ".git" and replaces slashes' ' + cgit_ci "sayauz/web.git/ci&h=v0.17.3.2" >tmp && + grep "^Location: https://ci.example.org/job/sayauz-web/view/tags/job/v0.17.3.2$" tmp +' + +test_expect_success 'repo.ci-url overrides and expands $repo and $$' ' + cgit_ci "override/ci&h=master" >tmp && + grep -F "Location: https://ci.example.org/override/override/\$/master" tmp +' + +test_expect_success 'repo.ci-tag-url does not apply to branches' ' + cgit_ci "tagonly/ci&h=v0.17.3.2" >tmp && + grep "^Location: https://ci.example.org/tagonly/v0.17.3.2$" tmp && + cgit_ci "tagonly/ci&h=master" >tmp && + grep "^Status: 404 Not found$" tmp +' + +test_expect_success 'find ci tab' ' + cgit_ci "foo/refs&h=master" >tmp && + grep "href=./foo/ci/.>ci</a>" tmp +' + +test_expect_success 'no ci tab without a ci url' ' + cgit_url "foo/refs" >tmp && + ! grep ">ci</a>" tmp +' + +test_expect_success 'ci filter accepts a tag' ' + rm -f ci-filter.log && + cgit_ci "filtered/ci&h=v0.17.3.2" >tmp && + grep "^Location: https://ci.example.org/job/filtered/view/tags/job/v0.17.3.2$" tmp && + echo "v0.17.3.2|tag|https://ci.example.org/job/filtered/view/tags/job/v0.17.3.2" >expected && + test_cmp expected ci-filter.log +' + +test_expect_success 'ci filter rejects a branch' ' + rm -f ci-filter.log && + cgit_ci "filtered/ci&h=master" >tmp && + grep "^Status: 404 Not found$" tmp && + echo "master|branch|https://ci.example.org/job/filtered/job/master" >expected && + test_cmp expected ci-filter.log +' + +test_expect_success "ci filter hides the tab and is consulted once" ' + rm -f ci-filter.log && + cgit_ci "filtered/refs&h=master" >tmp && + ! grep ">ci</a>" tmp && + test_line_count = 1 ci-filter.log && + cgit_ci "filtered/refs&h=v0.17.3.2" >tmp && + grep "href=./filtered/ci/?h=v0.17.3.2.>ci</a>" tmp +' + +test_expect_success 'ci filter is not consulted without a ci url' ' + rm -f ci-filter.log && + cgit_url "foo/refs" >tmp && + test_path_is_missing ci-filter.log +' + +test_done diff --git a/tests/t0113-ci-lua.sh b/tests/t0113-ci-lua.sh new file mode 100644 index 0000000..1e095b1 --- /dev/null +++ b/tests/t0113-ci-lua.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +test_description='Check ci filter written in Lua' +. ./setup.sh + +if test $CGIT_HAS_LUA -ne 1 +then + skip_all='Skipping Lua ci filter tests: Lua support not compiled in' + test_done + exit +fi + +cat >cgitrc.cilua <<EOF +virtual-root=/ +cache-root=$PWD/cache +cache-size=0 + +ci-url=https://ci.example.org/job/\$slug/job/\$ref +ci-filter=lua:$PWD/ci-filter.lua + +repo.url=foo +repo.path=$PWD/repos/foo/.git +EOF + +# Accepts tags only, and records its arguments. +cat >ci-filter.lua <<EOF +function filter_open(ref, refkind, url) + local log = io.open("$PWD/ci-filter.log", "a") + log:write(ref .. "|" .. refkind .. "|" .. url .. "\n") + log:close() + accepted = (refkind == "tag") +end + +function filter_write(buffer) +end + +function filter_close() + if accepted then + return 0 + end + return 1 +end +EOF + +git --git-dir="$PWD/repos/foo/.git" tag v1.0 master + +cgit_ci() +{ + CGIT_CONFIG="$PWD/cgitrc.cilua" QUERY_STRING="url=$1" cgit +} + +test_expect_success 'Lua ci filter accepts a tag' ' + rm -f ci-filter.log && + cgit_ci "foo/ci&h=v1.0" >tmp && + grep "^Location: https://ci.example.org/job/foo/job/v1.0$" tmp && + echo "v1.0|tag|https://ci.example.org/job/foo/job/v1.0" >expected && + test_cmp expected ci-filter.log +' + +test_expect_success 'Lua ci filter rejects a branch' ' + cgit_ci "foo/ci&h=master" >tmp && + grep "^Status: 404 Not found$" tmp +' + +test_expect_success 'Lua ci filter controls the tab' ' + cgit_ci "foo/refs&h=v1.0" >tmp && + grep "href=./foo/ci/?h=v1.0.>ci</a>" tmp && + cgit_ci "foo/refs&h=master" >tmp && + ! grep ">ci</a>" tmp +' + +test_expect_success 'page is still rendered correctly around the filter' ' + cgit_ci "foo/refs&h=v1.0" >tmp && + grep "</html>" tmp && + ! grep "|tag|" tmp +' + +test_done diff --git a/tests/t0114-broken-repo.sh b/tests/t0114-broken-repo.sh new file mode 100644 index 0000000..61367e6 --- /dev/null +++ b/tests/t0114-broken-repo.sh @@ -0,0 +1,71 @@ +#!/bin/sh + +test_description='Check that a misconfigured repo.path is reported' +. ./setup.sh + +mkdir -p plaindir + +cat >cgitrc.broken <<EOF +virtual-root=/ +cache-root=$PWD/cache +cache-size=0 + +ci-branch-url=https://ci.example.org/job/\$slug/job/\$ref + +repo.url=missing +repo.path=$PWD/no/such/repo.git + +repo.url=notarepo +repo.path=$PWD/plaindir +EOF + +# cgit used to spin forever on these requests, so give it a deadline and +# report a failure rather than hanging the test suite. +cgit_broken() +{ + CGIT_CONFIG="$PWD/cgitrc.broken" QUERY_STRING="url=$1" perl -e ' + my $pid = fork(); + if ($pid == 0) { + exec("cgit") or die "exec: $!"; + } + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + alarm(20); + waitpid($pid, 0); + alarm(0); + }; + if ($@) { + kill "KILL", $pid; + print STDERR "cgit did not terminate\n"; + exit 1; + } + ' </dev/null +} + +test_expect_success 'report a repo.path that does not exist' ' + cgit_broken "missing/&h=master" >tmp && + grep "Failed to open missing" tmp +' + +test_expect_success 'report a repo.path that is not a repository' ' + cgit_broken "notarepo/&h=master" >tmp && + grep "Failed to open notarepo" tmp +' + +test_expect_success 'report an unknown page for a broken repo' ' + cgit_broken "missing/nosuchpage/&h=master" >tmp && + grep "^Status: 404 Not found$" tmp && + grep "Invalid request" tmp +' + +test_expect_success 'no ci tab for a broken repo' ' + cgit_broken "missing/nosuchpage/&h=master" >tmp && + ! grep ">ci</a>" tmp +' + +test_expect_success 'report a broken repo on the ci page' ' + cgit_broken "missing/ci/&h=master" >tmp && + grep "Failed to open missing" tmp +' + +test_done @@ -121,14 +121,14 @@ static void print_object(const struct object_id *oid, const char *path, struct blame_origin *o; struct blame_entry *ent = NULL; - type = oid_object_info(the_repository, oid, &size); + type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { cgit_print_error_page(404, "Not found", "Bad object name: %s", oid_to_hex(oid)); return; } - buf = repo_read_object_file(the_repository, oid, &type, &size); + buf = odb_read_object(the_repository->objects, oid, &type, &size); if (!buf) { cgit_print_error_page(500, "Internal server error", "Error reading object %s", oid_to_hex(oid)); @@ -263,7 +263,7 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base, static int basedir_len(const char *path) { - char *p = strrchr(path, '/'); + const char *p = strrchr(path, '/'); if (p) return p - path + 1; return 0; @@ -56,7 +56,7 @@ int cgit_ref_path_exists(const char *path, const char *ref, int file_only) if (repo_get_oid(the_repository, ref, &oid)) goto done; - if (oid_object_info(the_repository, &oid, &size) != OBJ_COMMIT) + if (odb_read_object_info(the_repository->objects, &oid, &size) != OBJ_COMMIT) goto done; read_tree(the_repository, repo_get_commit_tree(the_repository, lookup_commit_reference(the_repository, &oid)), @@ -91,18 +91,18 @@ int cgit_print_file(char *path, const char *head, int file_only) if (repo_get_oid(the_repository, head, &oid)) return -1; - type = oid_object_info(the_repository, &oid, &size); + type = odb_read_object_info(the_repository->objects, &oid, &size); if (type == OBJ_COMMIT) { commit = lookup_commit_reference(the_repository, &oid); read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk_tree_ctx); if (!walk_tree_ctx.found_path) return -1; - type = oid_object_info(the_repository, &oid, &size); + type = odb_read_object_info(the_repository->objects, &oid, &size); } if (type == OBJ_BAD) return -1; - buf = repo_read_object_file(the_repository, &oid, &type, &size); + buf = odb_read_object(the_repository->objects, &oid, &type, &size); if (!buf) return -1; buf[size] = '\0'; @@ -147,13 +147,13 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl } } - type = oid_object_info(the_repository, &oid, &size); + type = odb_read_object_info(the_repository->objects, &oid, &size); if ((!hex) && type == OBJ_COMMIT && path) { commit = lookup_commit_reference(the_repository, &oid); read_tree(the_repository, repo_get_commit_tree(the_repository, commit), &paths, walk_tree, &walk_tree_ctx); - type = oid_object_info(the_repository, &oid, &size); + type = odb_read_object_info(the_repository->objects, &oid, &size); } if (type == OBJ_BAD) { @@ -162,7 +162,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl return; } - buf = repo_read_object_file(the_repository, &oid, &type, &size); + buf = odb_read_object(the_repository->objects, &oid, &type, &size); if (!buf) { cgit_print_error_page(500, "Internal server error", "Error reading object %s", hex); @@ -0,0 +1,152 @@ +/* ui-ci.c: redirect to an external CI system + * + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Licensed under GNU General Public License v2 + * (see COPYING for full license text) + */ + +#define USE_THE_REPOSITORY_VARIABLE + +#include "cgit.h" +#include "ui-ci.h" +#include "html.h" +#include "ui-shared.h" + +/* Memoized outcome of resolving the ci url for this request: -1 while + * unresolved, 0 when there is no ci url for the current ref, and 1 when + * ci_url holds the url to redirect to. + */ +static int ci_resolved = -1; +static char *ci_url; + +static int ref_is_tag(const char *ref) +{ + struct strbuf fullref = STRBUF_INIT; + struct object_id oid; + int is_tag; + + strbuf_addf(&fullref, "refs/tags/%s", ref); + is_tag = !repo_get_oid(the_repository, fullref.buf, &oid); + strbuf_release(&fullref); + return is_tag; +} + +/* Append the repository url with any ".git" suffix removed and all + * slashes replaced by dashes, e.g. "sayauz/web.git" becomes + * "sayauz-web". + */ +static void add_repo_slug(struct strbuf *buf, const char *url) +{ + size_t len, i; + + if (!strip_suffix(url, ".git", &len)) + len = strlen(url); + for (i = 0; i < len; i++) + strbuf_addch(buf, url[i] == '/' ? '-' : url[i]); +} + +static char *expand_ci_url(const char *template) +{ + const char *p, *rest; + struct strbuf buf = STRBUF_INIT; + + for (p = template; *p; p++) { + if (*p != '$') { + strbuf_addch(&buf, *p); + continue; + } + if (skip_prefix(p, "$ref", &rest)) + strbuf_addstr(&buf, ctx.qry.head); + else if (skip_prefix(p, "$repo", &rest)) + strbuf_addstr(&buf, ctx.repo->url); + else if (skip_prefix(p, "$slug", &rest)) + add_repo_slug(&buf, ctx.repo->url); + else if (skip_prefix(p, "$$", &rest)) + strbuf_addch(&buf, '$'); + else { + strbuf_addch(&buf, '$'); + continue; + } + p = rest - 1; + } + return strbuf_detach(&buf, NULL); +} + +/* Ask the ci filter whether the pipeline denoted by url actually exists. + * The filter must not write to stdout, and signals "yes" by exiting with + * a zero status. + */ +static int ci_filter_accepts(const char *refkind, const char *url) +{ + struct cgit_filter *filter = ctx.repo->ci_filter; + + if (!filter) + return 1; + if (cgit_open_filter(filter, ctx.qry.head, refkind, url)) + return 0; + return !cgit_close_filter(filter); +} + +static void resolve_ci_url(void) +{ + const char *template, *refkind; + + if (ci_resolved != -1) + return; + ci_resolved = 0; + + if (!ctx.repo || !ctx.qry.head || !cgit_have_repository()) + return; + + if (ref_is_tag(ctx.qry.head)) { + refkind = "tag"; + template = ctx.repo->ci_tag_url; + } else { + refkind = "branch"; + template = ctx.repo->ci_branch_url; + } + if (!template) + template = ctx.repo->ci_url; + if (!template) + return; + + ci_url = expand_ci_url(template); + if (!ci_filter_accepts(refkind, ci_url)) { + free(ci_url); + ci_url = NULL; + return; + } + ci_resolved = 1; +} + +int cgit_ci_available(void) +{ + resolve_ci_url(); + return ci_resolved == 1; +} + +void cgit_print_ci(void) +{ + resolve_ci_url(); + + if (ci_resolved != 1) { + cgit_print_error_page(404, "Not found", + "No ci url available for %s", + ctx.qry.head); + return; + } + + /* The expanded template is emitted verbatim rather than through + * cgit_redirect(), which percent-encodes characters such as '?', + * '=' and '%' that are meaningful in a ci url. Refuse anything + * which could be used to smuggle in extra response headers. + */ + if (strpbrk(ci_url, "\r\n")) { + cgit_print_error_page(500, "Internal server error", + "Malformed ci url"); + return; + } + + htmlf("Status: 302 Found\nLocation: %s\n\n", ci_url); +} @@ -0,0 +1,7 @@ +#ifndef UI_CI_H +#define UI_CI_H + +extern int cgit_ci_available(void); +extern void cgit_print_ci(void); + +#endif /* UI_CI_H */ @@ -14,42 +14,45 @@ #include "html.h" #include "ui-shared.h" #include "packfile.h" -#include "object-store.h" -static int print_ref_info(const char *refname, const char *referent UNUSED, - const struct object_id *oid, int flags, void *cb_data) +static int print_ref_info(const struct reference *ref, void *cb_data) { struct object *obj; - if (!(obj = parse_object(the_repository, oid))) + if (!(obj = parse_object(the_repository, ref->oid))) return 0; - htmlf("%s\t%s\n", oid_to_hex(oid), refname); + htmlf("%s\t%s\n", oid_to_hex(ref->oid), ref->name); if (obj->type == OBJ_TAG) { - if (!(obj = deref_tag(the_repository, obj, refname, 0))) + if (!(obj = deref_tag(the_repository, obj, ref->name, 0))) return 0; - htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), refname); + htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), ref->name); } return 0; } static void print_pack_info(void) { - struct packed_git *pack; + struct odb_source *source; char *offset; ctx.page.mimetype = "text/plain"; ctx.page.filename = "objects/info/packs"; cgit_print_http_headers(); - reprepare_packed_git(the_repository); - for (pack = get_packed_git(the_repository); pack; pack = pack->next) { - if (pack->pack_local) { - offset = strrchr(pack->pack_name, '/'); - if (offset && offset[1] != '\0') - ++offset; - else - offset = pack->pack_name; - htmlf("P %s\n", offset); + odb_reprepare(the_repository->objects); + for (source = the_repository->objects->sources; source; source = source->next) { + struct odb_source_files *files = odb_source_files_downcast(source); + struct packfile_list_entry *e; + for (e = files->packed->packs.head; e; e = e->next) { + struct packed_git *p = e->pack; + if (p->pack_local) { + offset = strrchr(p->pack_name, '/'); + if (offset && offset[1] != '\0') + ++offset; + else + offset = p->pack_name; + htmlf("P %s\n", offset); + } } } } @@ -1,6 +1,10 @@ /* ui-diff.c: show diff between two blobs * * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Select variable refs from web ui for diff * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -336,6 +340,32 @@ static void filepair_cb(struct diff_filepair *pair) cgit_ssdiff_footer(); } +static int print_ref_option(const struct reference *ref, void *cb_data) +{ + const char **sel = (const char **)cb_data; + + html_option(ref->name, ref->name, sel[0]); + return 0; +} + +static void cgit_print_ref_select(const char *name, const char *selected) +{ + /* + * Offer every branch and tag so that any two refs can be compared. + */ + html("<select name='"); + html_attr(name); + html("' onchange='this.form.submit();'>"); + refs_for_each_branch_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + if (ctx.repo->enable_remote_branches) + refs_for_each_remote_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + refs_for_each_tag_ref(get_main_ref_store(the_repository), + print_ref_option, (void *)&selected); + html("</select>"); +} + void cgit_print_diff_ctrls(void) { int i, curr; @@ -343,9 +373,43 @@ void cgit_print_diff_ctrls(void) html("<div class='cgit-panel'>"); html("<b>diff options</b>"); html("<form method='get'>"); - cgit_add_hidden_formfields(1, 0, ctx.qry.page); + /* + * Emit the same hidden fields as cgit_add_hidden_formfields(), except + * id/id2, which are carried by the ref-picker selects below. + */ + if (!ctx.cfg.virtual_root) { + struct strbuf url = STRBUF_INIT; + strbuf_addf(&url, "%s/%s", ctx.qry.repo, ctx.qry.page); + if (ctx.qry.vpath) + strbuf_addf(&url, "/%s", ctx.qry.vpath); + html_hidden("url", url.buf); + strbuf_release(&url); + } + if (ctx.qry.head && ctx.repo->defbranch && + strcmp(ctx.qry.head, ctx.repo->defbranch)) + html_hidden("h", ctx.qry.head); + if (ctx.qry.showmsg) + html_hidden("showmsg", "1"); + html("<table>"); html("<tr><td colspan='2'/></tr>"); + + /* ref pickers: compare any two refs (branches/tags) */ + { + const char *new_ref = ctx.qry.oid ? ctx.qry.oid : ctx.qry.head; + const char *old_ref = ctx.qry.oid2; + + html("<tr>"); + html("<td class='label'>from:</td>"); + html("<td class='ctrl'>"); + cgit_print_ref_select("id2", old_ref); + html("</td></tr><tr>"); + html("<td class='label'>to:</td>"); + html("<td class='ctrl'>"); + cgit_print_ref_select("id", new_ref); + html("</td></tr>"); + } + html("<tr>"); html("<td class='label'>context:</td>"); html("<td class='ctrl'>"); @@ -395,7 +459,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev, * 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.cfg.enable_follow_links) { + if (ctx.qry.follow && ctx.repo->enable_follow_links) { current_prefix = prefix; prefix = ""; } else { @@ -83,7 +83,7 @@ void show_commit_decorations(struct commit *commit) break; case DECORATION_REF_TAG: if (!refs_read_ref(get_main_ref_store(the_repository), deco->name, &oid_tag) && - !peel_iterated_oid(the_repository, &oid_tag, &peeled)) + !peel_object(the_repository, &oid_tag, &peeled, PEEL_OBJECT_VERIFY_TAGGED_OBJECT_TYPE)) is_annotated = !oideq(&oid_tag, &peeled); cgit_tag_link(buf, NULL, is_annotated ? "tag-annotated-deco" : "tag-deco", buf); break; @@ -167,6 +167,7 @@ static int show_commit(struct commit *commit, struct rev_info *revs) revs->diffopt.output_format = DIFF_FORMAT_CALLBACK; revs->diffopt.format_callback = cgit_diff_tree_cb; revs->diffopt.format_callback_data = handle_rename; + revs->diffopt.no_free = 1; diff_flush(&revs->diffopt); revs->diffopt.output_format = saved_fmt; revs->diffopt.flags = saved_flags; @@ -406,7 +407,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern } } - if (!path || !ctx.cfg.enable_follow_links) { + if (!path || !ctx.repo->enable_follow_links) { /* * If we don't have a path, "follow" is a no-op so make sure * the variable is set to false to avoid needing to check @@ -24,13 +24,13 @@ static int print_object(const struct object_id *oid, const char *path) char *buf, *mimetype; unsigned long size; - type = oid_object_info(the_repository, oid, &size); + type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { cgit_print_error_page(404, "Not found", "Not found"); return 0; } - buf = repo_read_object_file(the_repository, oid, &type, &size); + buf = odb_read_object(the_repository->objects, oid, &type, &size); if (!buf) { cgit_print_error_page(404, "Not found", "Not found"); return 0; @@ -157,7 +157,7 @@ static int walk_tree(const struct object_id *oid, struct strbuf *base, static int basedir_len(const char *path) { - char *p = strrchr(path, '/'); + const char *p = strrchr(path, '/'); if (p) return p - path + 1; return 0; diff --git a/ui-repolist.c b/ui-repolist.c index d12e3dd..1b224cf 100644 --- a/ui-repolist.c +++ b/ui-repolist.c @@ -18,7 +18,7 @@ static time_t read_agefile(const char *path) char *buf = NULL; struct strbuf date_buf = STRBUF_INIT; - if (readfile(path, &buf, &size)) { + if (read_first_line(path, &buf, &size)) { free(buf); return 0; } diff --git a/ui-shared.c b/ui-shared.c index 4250b89..686fde0 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -1,6 +1,14 @@ /* ui-shared.c: common web output functions * * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com> + * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com> + * + * Modified 2026 by Saya Andy: + * Implement ci link tab + * Check repo existence + * Replace memrchr() for macOS compatibility + * Allow multiple favicons of different mime types to be used + * Include favicon.svg as default * * Licensed under GNU General Public License v2 * (see COPYING for full license text) @@ -12,6 +20,7 @@ #include "ui-shared.h" #include "cmd.h" #include "html.h" +#include "ui-ci.h" #include "version.h" static const char cgit_doctype[] = @@ -342,6 +351,12 @@ void cgit_tag_link(const char *name, const char *title, const char *class, reporevlink("tag", name, title, class, tag, NULL, NULL); } +void cgit_ci_link(const char *name, const char *title, const char *class, + const char *head) +{ + reporevlink("ci", name, title, class, head, NULL, NULL); +} + void cgit_tree_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { @@ -761,6 +776,51 @@ void cgit_redirect(const char *url, bool permanent) html("\n\n"); } +static const struct { + const char *ext; + const char *mimetype; +} favicon_mimetypes[] = { + { "ico", "image/x-icon" }, + { "png", "image/png" }, + { "svg", "image/svg+xml" }, + { "gif", "image/gif" }, + { "jpg", "image/jpeg" }, + { "jpeg", "image/jpeg" }, + { "webp", "image/webp" }, + { "avif", "image/avif" }, +}; + +/* Deduce the mimetype of a favicon from the extension of its url, ignoring + * any query string or fragment. Returns NULL for unknown extensions. + */ +static const char *favicon_mimetype(const char *url) +{ + const char *p, *end, *ext = NULL; + size_t len; + int i; + + end = strpbrk(url, "?#"); + if (!end) + end = url + strlen(url); + + for (p = url; p < end; p++) { + if (*p == '/') + ext = NULL; + else if (*p == '.') + ext = p + 1; + } + if (!ext) + return NULL; + len = end - ext; + + for (i = 0; i < ARRAY_SIZE(favicon_mimetypes); i++) { + if (strlen(favicon_mimetypes[i].ext) == len && + !strncasecmp(favicon_mimetypes[i].ext, ext, len)) + return favicon_mimetypes[i].mimetype; + } + return NULL; +} + static void print_rel_vcs_link(const char *url) { html("<link rel='vcs-git' href='"); @@ -802,6 +862,27 @@ static int emit_js_link(struct string_list_item *s, void *arg) return 0; } +static int emit_favicon_link(struct string_list_item *s, void *arg) +{ + const char *url = s ? s->string : (const char *)arg; + const char *mimetype; + + /* Do not emit anything if favicon= is specified. */ + if (!*url) + return 0; + + html("<link rel='shortcut icon' href='"); + html_attr(url); + mimetype = favicon_mimetype(url); + if (mimetype) { + html("' type='"); + html_attr(mimetype); + } + html("'/>\n"); + + return 0; +} + void cgit_print_docstart(void) { char *host = cgit_hosturl(); @@ -832,11 +913,12 @@ void cgit_print_docstart(void) else emit_js_link(NULL, "/cgit.js"); - if (ctx.cfg.favicon) { - html("<link rel='shortcut icon' href='"); - html_attr(ctx.cfg.favicon); - html("'/>\n"); - } + if (ctx.cfg.favicon.items) + for_each_string_list(&ctx.cfg.favicon, emit_favicon_link, NULL); + else + emit_favicon_link(NULL, "/favicon.svg"); + emit_favicon_link(NULL, "/favicon.ico"); + if (host && ctx.repo && ctx.qry.head) { char *fileurl; struct strbuf sb = STRBUF_INIT; @@ -877,7 +959,7 @@ void cgit_print_docend(void) if (ctx.cfg.footer) html_include(ctx.cfg.footer); else { - htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> " + htmlf("<div class='footer'>generated by <a href='https://sayag.it/cgitext.git/about/'>cgitext %s</a> " "(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string); html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601))); html("</div>\n"); @@ -889,13 +971,18 @@ void cgit_print_docend(void) void cgit_print_error_page(int code, const char *msg, const char *fmt, ...) { va_list ap; + va_start(ap, fmt); + cgit_vprint_error_page(code, msg, fmt, ap); + va_end(ap); +} + +void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap) +{ ctx.page.expires = ctx.cfg.cache_dynamic_ttl; ctx.page.status = code; ctx.page.statusmsg = msg; cgit_print_layout_start(); - va_start(ap, fmt); cgit_vprint_error(fmt, ap); - va_end(ap); cgit_print_layout_end(); } @@ -936,10 +1023,9 @@ void cgit_add_clone_urls(void (*fn)(const char *)) add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url); } -static int print_branch_option(const char *refname, const char *referent UNUSED, - const struct object_id *oid, int flags, void *cb_data) +static int print_branch_option(const struct reference *ref, void *cb_data) { - char *name = (char *)refname; + char *name = (char *)ref->name; html_option(name, name, ctx.qry.head); return 0; } @@ -1038,7 +1124,7 @@ static void print_header(void) cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1); html(" : "); cgit_summary_link(ctx.repo->name, NULL, NULL, NULL); - if (ctx.env.authenticated) { + if (ctx.env.authenticated && cgit_have_repository()) { html("</td><td class='form'>"); html("<form method='get'>\n"); cgit_add_hidden_formfields(0, 1, ctx.qry.page); @@ -1106,6 +1192,8 @@ void cgit_print_pageheader(void) if (ctx.repo->max_stats) cgit_stats_link("stats", NULL, hc("stats"), ctx.qry.head, ctx.qry.vpath); + if (cgit_ci_available()) + cgit_ci_link("ci", NULL, hc("ci"), ctx.qry.head); if (ctx.repo->homepage) { html("<a href='"); html_attr(ctx.repo->homepage); @@ -1154,7 +1242,7 @@ void cgit_print_pageheader(void) html("<div class='path'>"); html("path: "); cgit_print_path_crumbs(ctx.qry.vpath); - if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) { + if (ctx.repo->enable_follow_links && !strcmp(ctx.qry.page, "log")) { html(" ("); ctx.qry.follow = !ctx.qry.follow; cgit_self_link(ctx.qry.follow ? "follow" : "unfollow", @@ -1243,6 +1331,17 @@ void cgit_print_snapshot_links(const struct cgit_repo *repo, const char *ref, strbuf_release(&filename); } +/* Locate the last '/' within the first len bytes of path, or NULL if + * there is none. Avoids memrchr(), which is a GNU extension. + */ +static const char *find_last_slash(const char *path, size_t len) +{ + while (len > 0) + if (path[--len] == '/') + return path + len; + return NULL; +} + void cgit_set_title_from_path(const char *path) { struct strbuf sb = STRBUF_INIT; @@ -1251,7 +1350,7 @@ void cgit_set_title_from_path(const char *path) if (!path) return; - for (last_slash = path + strlen(path); (slash = memrchr(path, '/', last_slash - path)) != NULL; last_slash = slash) { + for (last_slash = path + strlen(path); (slash = find_last_slash(path, last_slash - path)) != NULL; last_slash = slash) { strbuf_add(&sb, slash + 1, last_slash - slash - 1); strbuf_addstr(&sb, " \xc2\xab "); } diff --git a/ui-shared.h b/ui-shared.h index f12fa99..a6627d4 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -21,6 +21,8 @@ extern void cgit_summary_link(const char *name, const char *title, const char *class, const char *head); extern void cgit_tag_link(const char *name, const char *title, const char *class, const char *tag); +extern void cgit_ci_link(const char *name, const char *title, + const char *class, const char *head); extern void cgit_tree_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path); @@ -73,6 +75,7 @@ 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, diff --git a/ui-snapshot.c b/ui-snapshot.c index 3e38cd5..d157222 100644 --- a/ui-snapshot.c +++ b/ui-snapshot.c @@ -192,7 +192,7 @@ static int write_sig(const struct cgit_snapshot_format *format, return 0; } - buf = repo_read_object_file(the_repository, note, &type, &size); + buf = odb_read_object(the_repository->objects, note, &type, &size); if (!buf) { cgit_print_error_page(404, "Not found", "Not found"); return 0; @@ -66,7 +66,7 @@ void cgit_print_tag(char *revname) struct taginfo *info; tag = lookup_tag(the_repository, &oid); - if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) { + if (!tag || parse_tag(the_repository, tag) || !(info = cgit_parse_tag(tag))) { cgit_print_error_page(500, "Internal server error", "Bad tag object: %s", revname); goto cleanup; @@ -93,14 +93,14 @@ static void print_object(const struct object_id *oid, const char *path, const ch unsigned long size; bool is_binary; - type = oid_object_info(the_repository, oid, &size); + type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { cgit_print_error_page(404, "Not found", "Bad object name: %s", oid_to_hex(oid)); return; } - buf = repo_read_object_file(the_repository, oid, &type, &size); + buf = odb_read_object(the_repository->objects, oid, &type, &size); if (!buf) { cgit_print_error_page(500, "Internal server error", "Error reading object %s", oid_to_hex(oid)); @@ -217,7 +217,7 @@ static int ls_item(const struct object_id *oid, struct strbuf *base, ctx.qry.path ? "/" : "", name); if (!S_ISGITLINK(mode)) { - type = oid_object_info(the_repository, oid, &size); + type = odb_read_object_info(the_repository->objects, oid, &size); if (type == OBJ_BAD) { htmlf("<tr><td colspan='3'>Bad object: %s %s</td></tr>", name, @@ -244,7 +244,7 @@ static int ls_item(const struct object_id *oid, struct strbuf *base, } if (S_ISLNK(mode)) { html(" -> "); - buf = repo_read_object_file(the_repository, oid, &type, &size); + buf = odb_read_object(the_repository->objects, oid, &type, &size); if (!buf) { htmlf("Error reading object: %s", oid_to_hex(oid)); goto cleanup; |