diff options
| author | Bryce Kwon <bryce@brycekwon.com> | |
|---|---|---|
| committer | Bryce Kwon <bryce@brycekwon.com> | |
| commit | ||
| parent | ||
| tree | ||
| download | ||
Resolve submodule links from `.gitmodules`
Diffstat (limited to '')
| -rw-r--r-- | cgitrc.5.txt | 25 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | source/cgit.c | 5 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | source/cgit.h | 2 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | source/shared.c | 1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rw-r--r-- | source/ui-shared.c | 250 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rwxr-xr-x | tests/t0112-submodule-links.sh | 109 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| -rwxr-xr-x | tests/t0200-security.sh | 41 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 files changed, 422 insertions, 11 deletions
diff --git a/cgitrc.5.txt b/cgitrc.5.txt index ac33d6d..84dcdb6 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -194,6 +194,19 @@ enable-git-config:: with "cgit." will be mapped to the corresponding "repo." key in cgit. Default value: "0". See also: scan-path, section-from-path. +enable-gitmodules-links:: + Flag which, when set to "1", makes submodule listings derive a link + from the url recorded in the .gitmodules file at the shown revision, + for submodules that no module-link template covers. The url is first + matched, by its trailing path components, against the repositories + served by this cgit instance, so ssh, file and relative urls still + get a link when their target is hosted here, and the commit hash then + links to the target repository's commit page. Otherwise a plain http + or https url is linked as it is, an ssh url to a well-known public + host is rewritten to its https form, and any other url is left + unlinked and shown as a tooltip instead. Default value: "0". See + also: "repo.enable-gitmodules-links". + enable-http-clone:: If set to "1", cgit will act as a dumb HTTP endpoint for git clones. You can add "http://$HTTP_HOST$SCRIPT_NAME/$CGIT_REPO_URL" to clone-url @@ -375,8 +388,8 @@ mimetype-file:: module-link:: Text which will be used as the formatstring for a hyperlink when a submodule is printed in a directory listing. The arguments for the - formatstring are the path and SHA1 of the submodule commit. Default - value: none. + formatstring are the name of the submodule directory and the SHA1 of + the submodule commit. Default value: none. noplainemail:: If set to "1" showing full author email addresses will be disabled. @@ -570,6 +583,10 @@ repo.enable-follow-links:: A flag which can be used to disable the global setting `enable-follow-links'. Default value: none. +repo.enable-gitmodules-links:: + A flag which can be used to override the global setting + `enable-gitmodules-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. @@ -620,8 +637,8 @@ repo.logo-link:: repo.module-link:: Text which will be used as the formatstring for a hyperlink when a submodule is printed in a directory listing. The arguments for the - formatstring are the path and SHA1 of the submodule commit. Default - value: <module-link> + formatstring are the name of the submodule directory and the SHA1 of + the submodule commit. Default value: <module-link> repo.module-link.<path>:: Text which will be used as the formatstring for a hyperlink when a diff --git a/source/cgit.c b/source/cgit.c index 37b07d7..90ee702 100644 --- a/source/cgit.c +++ b/source/cgit.c @@ -237,6 +237,7 @@ static void print_repo(FILE *f, struct cgit_repo *repo) 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-gitmodules-links=%d\n", repo->enable_gitmodules_links); fprintf(f, "repo.enable-log-filecount=%d\n", repo->enable_log_filecount); fprintf(f, "repo.enable-log-linecount=%d\n", repo->enable_log_linecount); if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) @@ -560,6 +561,8 @@ static void apply_config(const char *name, const char *value) ctx.cfg.enable_blame = atoi(value); else if (!strcmp(name, "enable-commit-graph")) ctx.cfg.enable_commit_graph = atoi(value); + else if (!strcmp(name, "enable-gitmodules-links")) + ctx.cfg.enable_gitmodules_links = atoi(value); else if (!strcmp(name, "enable-log-filecount")) ctx.cfg.enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) @@ -1176,6 +1179,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->enable_commit_graph = atoi(value); else if (!strcmp(name, "enable-follow-links")) repo->enable_follow_links = atoi(value); + else if (!strcmp(name, "enable-gitmodules-links")) + repo->enable_gitmodules_links = atoi(value); else if (!strcmp(name, "enable-log-filecount")) repo->enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) diff --git a/source/cgit.h b/source/cgit.h index edae762..bab41c5 100644 --- a/source/cgit.h +++ b/source/cgit.h @@ -86,6 +86,7 @@ struct cgit_repo { int enable_blame; int enable_commit_graph; int enable_follow_links; + int enable_gitmodules_links; int enable_log_filecount; int enable_log_linecount; int enable_remote_branches; @@ -216,6 +217,7 @@ struct cgit_config { int embedded; int enable_filter_overrides; int enable_follow_links; + int enable_gitmodules_links; int enable_stats; int enable_http_clone; int enable_index_links; diff --git a/source/shared.c b/source/shared.c index 48d587d..37c21ae 100644 --- a/source/shared.c +++ b/source/shared.c @@ -218,6 +218,7 @@ struct cgit_repo *cgit_add_repo(const char *url) repo->enable_blame = ctx.cfg.enable_blame; repo->enable_commit_graph = ctx.cfg.enable_commit_graph; repo->enable_follow_links = ctx.cfg.enable_follow_links; + repo->enable_gitmodules_links = ctx.cfg.enable_gitmodules_links; repo->enable_log_filecount = ctx.cfg.enable_log_filecount; repo->enable_log_linecount = ctx.cfg.enable_log_linecount; repo->enable_remote_branches = ctx.cfg.enable_remote_branches; diff --git a/source/ui-shared.c b/source/ui-shared.c index 02352bf..317bff2 100644 --- a/source/ui-shared.c +++ b/source/ui-shared.c @@ -14,6 +14,7 @@ #include "cgit.h" #include "html.h" #include "shared.h" +#include "submodule-config.h" #include "ui-shared.h" #include "ui-snapshot.h" @@ -1028,12 +1029,229 @@ void cgit_object_link(struct object *obj) reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); } +/* + * The tree being listed decides which .gitmodules applies, so the revision + * the query names is resolved once and reused for every row. + */ +static const struct object_id *page_treeish(void) +{ + static struct object_id oid; + static int state; + + if (!state) { + const char *rev = ctx.qry.oid ? ctx.qry.oid : ctx.qry.head; + + state = rev && !repo_get_oid(the_repository, rev, &oid) ? 1 : -1; + } + return state > 0 ? &oid : NULL; +} + +static void strip_git_suffix(struct strbuf *sb) +{ + while (sb->len && sb->buf[sb->len - 1] == '/') + strbuf_setlen(sb, sb->len - 1); + strbuf_strip_suffix(sb, ".git"); +} + +/* + * Reduces a submodule url to the path it names on its host, so the tail can + * be compared against the urls this instance serves. A relative url points + * at a sibling of the repository's own clone, which maps onto a sibling of + * its cgit url. + */ +static char *submodule_url_path(const char *url) +{ + struct strbuf sb = STRBUF_INIT; + const char *rest, *colon; + + if (starts_with(url, "./") || starts_with(url, "../")) { + strbuf_addf(&sb, "%s/%s", ctx.repo->url, url); + if (strbuf_normalize_path(&sb)) { + strbuf_release(&sb); + return NULL; + } + } else if (skip_prefix(url, "http://", &rest) || + skip_prefix(url, "https://", &rest) || + skip_prefix(url, "git://", &rest) || + skip_prefix(url, "ssh://", &rest)) { + rest = strchr(rest, '/'); + if (!rest) + return NULL; + strbuf_addstr(&sb, rest); + } else if (skip_prefix(url, "file://", &rest)) { + strbuf_addstr(&sb, rest); + } else if ((colon = strchr(url, ':')) && + !memchr(url, '/', colon - url)) { + // scp syntax, user@host:path + strbuf_addstr(&sb, colon + 1); + } else { + strbuf_addstr(&sb, url); + } + + strip_git_suffix(&sb); + while (sb.len && sb.buf[0] == '/') + strbuf_remove(&sb, 0, 1); + if (!sb.len) { + strbuf_release(&sb); + return NULL; + } + return strbuf_detach(&sb, NULL); +} + +/* + * Finds the served repository whose url matches the most trailing components + * of the submodule url, whatever scheme carried it. + */ +static struct cgit_repo *repo_serving_url(const char *url) +{ + struct cgit_repo *repo, *best = NULL; + char *path = submodule_url_path(url); + size_t plen, rlen, blen = 0; + int i; + + if (!path) + return NULL; + plen = strlen(path); + for (i = 0; i < cgit_repolist.count; i++) { + repo = &cgit_repolist.repos[i]; + if (repo->ignore) + continue; + rlen = strlen(repo->url); + if (!rlen || rlen > plen) + continue; + if (strcmp(path + plen - rlen, repo->url)) + continue; + // Only whole trailing components count, so x/foo cannot + // claim a repository named oo. + if (rlen < plen && path[plen - rlen - 1] != '/') + continue; + if (rlen > blen) { + best = repo; + blen = rlen; + } + } + free(path); + return best; +} + +/* + * Hosts whose repository and commit pages follow a known form, so an ssh + * url can still be offered as a browser link. kernel.org runs cgit, which + * keeps the .git suffix in its own page urls. + */ +static const struct forge { + const char *host; + const char *commit_seg; + int keep_dot_git; +} forges[] = { + { "github.com", "/commit/", 0 }, + { "gitlab.com", "/-/commit/", 0 }, + { "bitbucket.org", "/commits/", 0 }, + { "codeberg.org", "/commit/", 0 }, + { "gitea.com", "/commit/", 0 }, + { "git.sr.ht", "/commit/", 0 }, + { "git.kernel.org", "/commit/?id=", 1 }, +}; + +static const struct forge *forge_for_host(const char *host, size_t len) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(forges); i++) + if (strlen(forges[i].host) == len && + !strncasecmp(forges[i].host, host, len)) + return &forges[i]; + return NULL; +} + +/* + * Derives a submodule row's links from its .gitmodules entry. The url is + * matched against this instance's own repositories first, so ssh, file and + * relative urls still land on an internal page when their target is served + * here, and the pinned commit gets a page of its own. A plain web url is + * linked as it is, an ssh url to a known host is rewritten to its web form, + * and anything else is left unlinked with the url as a tooltip, since a + * scheme cgit cannot vouch for has no place in an href. + */ +static void gitmodules_link(const char *path, const char *rev, + char **module, char **commit, + const char **tooltip) +{ + const struct object_id *treeish = page_treeish(); + const struct submodule *sub; + const struct forge *forge; + struct cgit_repo *target; + struct strbuf sb = STRBUF_INIT; + const char *url, *rest, *colon; + + if (!treeish) + return; + sub = submodule_from_path(the_repository, treeish, path); + if (!sub || !sub->url) + return; + url = sub->url; + + target = repo_serving_url(url); + if (target) { + char *query = cgit_fmtalloc("id=%s", rev); + + *module = cgit_pageurl(target->url, "tree", query); + *commit = cgit_pageurl(target->url, "commit", query); + free(query); + return; + } + + if (skip_prefix(url, "http://", &rest) || + skip_prefix(url, "https://", &rest)) { + *module = xstrdup(url); + forge = forge_for_host(rest, strcspn(rest, "/")); + if (forge) { + strbuf_addstr(&sb, url); + while (sb.len && sb.buf[sb.len - 1] == '/') + strbuf_setlen(&sb, sb.len - 1); + if (!forge->keep_dot_git) + strbuf_strip_suffix(&sb, ".git"); + strbuf_addstr(&sb, forge->commit_seg); + strbuf_addstr(&sb, rev); + *commit = strbuf_detach(&sb, NULL); + } + return; + } + + if ((colon = strchr(url, ':')) && !memchr(url, '/', colon - url)) { + const char *host = url; + const char *at = memchr(url, '@', colon - url); + + if (at) + host = at + 1; + forge = forge_for_host(host, colon - host); + if (forge) { + rest = colon + 1; + while (*rest == '/') + rest++; + strbuf_addf(&sb, "https://%.*s/%s", + (int)(colon - host), host, rest); + while (sb.len && sb.buf[sb.len - 1] == '/') + strbuf_setlen(&sb, sb.len - 1); + if (!forge->keep_dot_git) + strbuf_strip_suffix(&sb, ".git"); + *module = xstrdup(sb.buf); + strbuf_addstr(&sb, forge->commit_seg); + strbuf_addstr(&sb, rev); + *commit = strbuf_detach(&sb, NULL); + return; + } + } + + *tooltip = url; +} + void cgit_submodule_link(const char *class, char *path, const char *rev) { struct string_list *list; struct string_list_item *item; - char tail; - const char *name; + char tail, *module = NULL, *commit = NULL; + const char *name, *tooltip = NULL; size_t len; len = 0; @@ -1049,12 +1267,16 @@ void cgit_submodule_link(const char *class, char *path, const char *rev) } } + if (!item && !ctx.repo->module_link && + ctx.repo->enable_gitmodules_links) + gitmodules_link(path, rev, &module, &commit, &tooltip); + // The row shows the entry's own name, like every other row, while // the lookups above want the full path. name = strrchr(path, '/'); name = name ? name + 1 : path; - if (item || ctx.repo->module_link) { + if (item || ctx.repo->module_link || module) { html("<a "); if (class) htmlf("class='%s' ", class); @@ -1062,9 +1284,11 @@ void cgit_submodule_link(const char *class, char *path, const char *rev) if (item) { const char *args[] = { rev }; emit_module_link(item->util, args, 1); - } else { + } else if (ctx.repo->module_link) { const char *args[] = { name, rev }; emit_module_link(ctx.repo->module_link, args, 2); + } else { + html_attr(module); } html("'>"); html_txt(name); @@ -1073,15 +1297,29 @@ void cgit_submodule_link(const char *class, char *path, const char *rev) html("<span"); if (class) htmlf(" class='%s'", class); + if (tooltip) { + html(" title='"); + html_attr(tooltip); + html("'"); + } html(">"); html_txt(name); html("</span>"); } - html(" @ <span class='ls-mod-hash' title='"); + html(" @ "); + if (commit) { + html("<a class='ls-mod-hash' href='"); + html_attr(commit); + html("' title='"); + } else { + html("<span class='ls-mod-hash' title='"); + } html_attr(rev); html("'>"); html_txtf("%.7s", rev); - html("</span>"); + html(commit ? "</a>" : "</span>"); + free(module); + free(commit); if (tail == '/') path[len - 1] = tail; } diff --git a/tests/t0112-submodule-links.sh b/tests/t0112-submodule-links.sh new file mode 100755 index 0000000..bbd99b4 --- /dev/null +++ b/tests/t0112-submodule-links.sh @@ -0,0 +1,109 @@ +#!/bin/sh + +# Submodule rows can derive their links from the .gitmodules entry at the +# shown revision once enable-gitmodules-links is set. The url is matched +# against the repositories this instance serves first, so ssh and relative +# urls still land on an internal page, a plain web url is linked directly, +# an ssh url to a known host is rewritten to its web form, and anything +# else stays unlinked with the url offered as a tooltip. + +test_description='Check submodule links derived from .gitmodules' +. ./setup.sh + +test_expect_success 'set up a parent repo with submodules' ' + mkrepo repos/subtarget 1 && + mkrepo repos/subparent 1 && + ( + cd repos/subparent && + sub=$(git rev-parse HEAD) && + git update-index --add --cacheinfo 160000,$sub,local-ssh && + git update-index --add --cacheinfo 160000,$sub,sibling && + git update-index --add --cacheinfo 160000,$sub,forge-https && + git update-index --add --cacheinfo 160000,$sub,forge-ssh && + git update-index --add --cacheinfo 160000,$sub,opaque && + git update-index --add --cacheinfo 160000,$sub,cgit-host && + cat >.gitmodules <<-\EOF && + [submodule "local-ssh"] + path = local-ssh + url = git@example.com:mirrors/subtarget.git + [submodule "sibling"] + path = sibling + url = ../subtarget.git + [submodule "forge-https"] + path = forge-https + url = https://github.com/example/project.git + [submodule "forge-ssh"] + path = forge-ssh + url = git@github.com:example/other.git + [submodule "opaque"] + path = opaque + url = git@private.example.com:closed/thing.git + [submodule "cgit-host"] + path = cgit-host + url = https://git.kernel.org/pub/scm/git/git.git + EOF + git add .gitmodules && + git commit -m submodules + ) && + { + echo "virtual-root=/" && + echo "cache-size=0" && + echo "enable-gitmodules-links=1" && + echo "repo.url=subparent" && + echo "repo.path=$PWD/repos/subparent/.git" && + echo "repo.url=subtarget" && + echo "repo.path=$PWD/repos/subtarget/.git" + } >subrc +' + +test_expect_success 'generate subparent/tree' ' + CGIT_CONFIG="$PWD/subrc" QUERY_STRING="url=subparent/tree/" cgit >tmp +' + +test_expect_success 'an ssh url served here links to the local repo' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=./subtarget/tree/?id=$sub.>local-ssh</a>" tmp +' + +test_expect_success 'its hash links to the local commit page' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=./subtarget/commit/?id=$sub." tmp +' + +test_expect_success 'a relative url resolves against this repository' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=./subtarget/tree/?id=$sub.>sibling</a>" tmp +' + +test_expect_success 'a web url is linked as it is' ' + grep "href=.https://github.com/example/project.git.>forge-https</a>" tmp +' + +test_expect_success 'a known forge hash links to its commit page' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=.https://github.com/example/project/commit/$sub." tmp +' + +test_expect_success 'an ssh url to a known forge is rewritten' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=.https://github.com/example/other.>forge-ssh</a>" tmp && + grep "href=.https://github.com/example/other/commit/$sub." tmp +' + +test_expect_success 'a cgit host keeps its .git suffix in the commit link' ' + sub=$(git -C repos/subparent rev-parse HEAD~1) && + grep "href=.https://git.kernel.org/pub/scm/git/git.git/commit/?id=$sub." tmp +' + +test_expect_success 'an unresolvable url stays unlinked with a tooltip' ' + grep "class=.ls-mod. title=.git@private.example.com:closed/thing.git.>opaque</span>" tmp +' + +test_expect_success 'the links stay off without the flag' ' + sed "/enable-gitmodules-links/d" subrc >subrcoff && + CGIT_CONFIG="$PWD/subrcoff" QUERY_STRING="url=subparent/tree/" cgit >tmp && + ! grep "subtarget/tree" tmp && + grep "class=.ls-mod.>local-ssh</span>" tmp +' + +test_done diff --git a/tests/t0200-security.sh b/tests/t0200-security.sh index f475301..8fce5f4 100755 --- a/tests/t0200-security.sh +++ b/tests/t0200-security.sh @@ -277,7 +277,7 @@ test_expect_success 'a surplus conversion is shown literally, not filled' ' grep "href=./m/submod/[0-9a-f]*/%s/%s/" tmp ' -test_expect_success 'a well-formed module-link still takes path and sha1' ' +test_expect_success 'a well-formed module-link still takes name and sha1' ' { echo "virtual-root=/" && echo "cache-size=0" && @@ -316,4 +316,43 @@ test_expect_success 'a doubled percent in a module-link renders as one' ' grep "href=./m/submod/%/$sub." tmp ' +# The .gitmodules file is commit-controlled content, so a derived link may +# carry any scheme and any byte an author can commit. Only http and https +# may reach an href, and everything lands attribute-escaped. +test_expect_success 'set up a hostile .gitmodules' ' + ( + cd repos/modlink && + sub=$(git rev-parse HEAD) && + git update-index --add --cacheinfo 160000,$sub,quoted && + cat >.gitmodules <<-EOF && + [submodule "submod"] + path = submod + url = javascript:alert(1) + [submodule "quoted"] + path = quoted + url = https://example.com/x'\''><script>alert(1)</script> + EOF + git add .gitmodules && + git commit -m hostile + ) && + { + echo "virtual-root=/" && + echo "cache-size=0" && + echo "enable-gitmodules-links=1" && + echo "repo.url=modlink" && + echo "repo.path=$PWD/repos/modlink/.git" + } >modlinkgmrc +' + +test_expect_success 'a javascript url never reaches an href' ' + CGIT_CONFIG="$PWD/modlinkgmrc" QUERY_STRING="url=modlink/tree/" cgit >tmp && + ! grep "href=.javascript:" tmp && + grep "class=.ls-mod. title=.javascript:alert(1).>submod</span>" tmp +' + +test_expect_success 'a quote in a web url cannot break out of the href' ' + ! grep "<script>alert" tmp && + grep "href=.https://example.com/x'><script>" tmp +' + test_done |
