| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | cgit.c | 6 | ||||
| -rw-r--r-- | cgit.css | 412 | ||||
| -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 | ||||
| -rwxr-xr-x | filters/html-converters/md2html | 84 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.py | 45 | ||||
| -rwxr-xr-x | filters/syntax-highlighting.sh | 14 | ||||
| -rw-r--r-- | solarized.css | 140 | ||||
| -rwxr-xr-x | tests/t0106-diff.sh | 82 | ||||
| -rw-r--r-- | ui-diff.c | 86 | ||||
| -rw-r--r-- | ui-shared.c | 79 |
16 files changed, 817 insertions, 182 deletions
@@ -10,3 +10,4 @@ cgitrc.5.pdf cgitrc.5.xml *.o *.d +__pycache__/ @@ -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,12 +1,22 @@ -## 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: * 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 +## 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; @@ -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,18 +393,18 @@ 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: red; + color: var(--red); content: "\2BA9"; } @@ -351,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 { @@ -374,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; } @@ -405,10 +478,18 @@ 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 #aaa; - background-color: #eee; + border: solid 1px var(--border-mid); + background-color: var(--bg-hl); } div#cgit div.cgit-panel th { @@ -459,8 +540,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; } @@ -476,8 +557,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 { @@ -500,19 +581,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 { @@ -531,15 +612,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; } @@ -555,19 +636,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 { @@ -585,7 +666,7 @@ div#cgit .right { div#cgit table.list td.reposection { font-style: italic; - color: #888; + color: var(--fg-dim); } div#cgit a.button { @@ -593,6 +674,11 @@ div#cgit a.button { padding: 0em 0.5em; } +div#cgit form.diff-ctrls div.cgit-panel a.button { + border: solid 1px var(--border-mid); + background-color: var(--bg-hl); +} + div#cgit a.primary { font-size: 100%; } @@ -622,7 +708,7 @@ div#cgit ul.pager li { } div#cgit ul.pager a { - color: #777; + color: var(--fg-mute); } div#cgit ul.pager .current { @@ -631,46 +717,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; } @@ -679,47 +765,47 @@ 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; } @@ -733,21 +819,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 { @@ -756,9 +842,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 { @@ -767,14 +853,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; } @@ -784,18 +870,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; } @@ -805,7 +891,7 @@ div#cgit table.hgraph td { } div#cgit table.hgraph div.bar { - background-color: #eee; + background-color: var(--bg-hl); height: 1em; } @@ -818,83 +904,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; @@ -907,3 +993,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); +} @@ -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/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/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..1100596 100755 --- a/tests/t0106-diff.sh +++ b/tests/t0106-diff.sh @@ -16,4 +16,86 @@ 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 'flip link present' ' + grep "<a title=.swap from and to. class=.button. href=./foo/diff/?" tmp +' + +test_expect_success 'flip link swaps the default refs' ' + old=$(git --git-dir="$PWD/repos/foo/.git" rev-parse master~1) && + grep "/foo/diff/?id=$old&id2=master" 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_expect_success 'flip link swaps explicit refs' ' + cgit_query "url=foo/diff&id=master&id2=HEAD" >tmp && + new=$(git --git-dir="$PWD/repos/foo/.git" rev-parse HEAD) && + grep "/foo/diff/?id=$new&id2=master" tmp +' + +test_expect_success 'flip link omits id for a diff against a root commit' ' + root=$(git --git-dir="$PWD/repos/foo/.git" rev-list --max-parents=0 HEAD) && + cgit_query "url=foo/diff&id=$root" >tmp && + grep "href=./foo/diff/?id2=$root." 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,92 @@ 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("<tr><td/><td class='ctrl'>"); + cgit_diff_link("flip", "swap from and to", "button", + ctx.qry.head, + is_null_oid(old_rev_oid) ? NULL : + oid_to_hex(old_rev_oid), + new_ref, ctx.qry.path); + 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 +459,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; |