| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | cgit.c | 6 | ||||
| -rw-r--r-- | cgit.css | 8 | ||||
| -rw-r--r-- | cgit.h | 2 | ||||
| -rw-r--r-- | cgit.svg | 5 | ||||
| -rw-r--r-- | cgitrc.5.txt | 18 | ||||
| -rw-r--r-- | favicon.svg | 5 | ||||
| -rw-r--r-- | solarized.css | 140 | ||||
| -rwxr-xr-x | tests/t0106-diff.sh | 61 | ||||
| -rw-r--r-- | ui-diff.c | 78 | ||||
| -rw-r--r-- | ui-shared.c | 79 |
12 files changed, 401 insertions, 19 deletions
@@ -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) @@ -116,9 +119,12 @@ 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))) @@ -1,4 +1,4 @@ -## cgitext - CGI for Git extension by Saya Andy +## cgitext – CGI for Git extension by Saya Andy This is a custom expansion to cgit. Its most prominent changes by now are: @@ -6,9 +6,17 @@ This is a custom expansion to cgit. Its most prominent changes by now are: * 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 +## 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. @@ -5,6 +5,7 @@ * * 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) @@ -170,7 +171,7 @@ static void config_cb(const char *name, const char *value) else if (!strcmp(name, "js")) string_list_append(&ctx.cfg.js, strdup_first_line(value)); else if (!strcmp(name, "favicon")) - ctx.cfg.favicon = strdup_first_line(value); + string_list_append(&ctx.cfg.favicon, strdup_first_line(value)); else if (!strcmp(name, "footer")) ctx.cfg.footer = strdup_first_line(value); else if (!strcmp(name, "head-include")) @@ -412,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; @@ -478,6 +478,14 @@ div#cgit div.cgit-panel { margin-top: 1.5em; } +div#cgit form.diff-ctrls div.cgit-panel { + float: none; +} + +div#cgit form.diff-ctrls { + float: right; +} + div#cgit div.cgit-panel table { border-collapse: collapse; border: solid 1px var(--border-mid); @@ -205,7 +205,7 @@ struct cgit_config { char *ci_tag_url; char *clone_prefix; char *clone_url; - char *favicon; + struct string_list favicon; char *footer; char *head_include; char *header; 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 76b2085..527233d 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -240,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 @@ -272,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 @@ -916,8 +921,10 @@ 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 @@ -957,7 +964,8 @@ enable-log-linecount=1 branch-sort=age -# Add a cgit favicon +# Add cgit favicons +favicon=/favicon.svg favicon=/favicon.ico 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> 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..e4751e9 100755 --- a/tests/t0106-diff.sh +++ b/tests/t0106-diff.sh @@ -16,4 +16,65 @@ 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 pickers live in a separate refs panel' ' + grep "<b>diff refs</b>" tmp && + grep "<b>diff options</b>" 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 @@ -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,16 +340,84 @@ 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; + html("<form class='diff-ctrls' method='get'>"); + /* + * Emit the same hidden fields as cgit_add_hidden_formfields(), except + * id/id2, which are carried by the ref-picker selects below. One + * form wraps both panels so any submission carries all controls. + */ + 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("<div class='cgit-panel'>"); + html("<b>diff refs</b>"); + 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("</table>"); + html("</div>"); + html("<div class='cgit-panel'>"); html("<b>diff options</b>"); - html("<form method='get'>"); - cgit_add_hidden_formfields(1, 0, ctx.qry.page); html("<table>"); html("<tr><td colspan='2'/></tr>"); + html("<tr>"); html("<td class='label'>context:</td>"); html("<td class='ctrl'>"); @@ -379,8 +451,8 @@ void cgit_print_diff_ctrls(void) html("<tr><td/><td class='ctrl'>"); html("<noscript><input type='submit' value='reload'/></noscript>"); html("</td></tr></table>"); - html("</form>"); html("</div>"); + html("</form>"); } void cgit_print_diff(const char *new_rev, const char *old_rev, diff --git a/ui-shared.c b/ui-shared.c index 97d1839..686fde0 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -7,6 +7,8 @@ * 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) @@ -774,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='"); @@ -815,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(); @@ -845,11 +913,12 @@ void cgit_print_docstart(void) else emit_js_link(NULL, "/cgit.js"); - if (ctx.cfg.favicon && *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; |