]> gitweb.ps.run Git - ps-cgit/blob - ui-shared.c
print git version string in footer
[ps-cgit] / ui-shared.c
1 /* ui-shared.c: common web output functions
2  *
3  * Copyright (C) 2006-2017 cgit Development Team <cgit@lists.zx2c4.com>
4  *
5  * Licensed under GNU General Public License v2
6  *   (see COPYING for full license text)
7  */
8
9 #include "cgit.h"
10 #include "ui-shared.h"
11 #include "cmd.h"
12 #include "html.h"
13 #include "version.h"
14
15 static const char cgit_doctype[] =
16 "<!DOCTYPE html>\n";
17
18 static char *http_date(time_t t)
19 {
20         static char day[][4] =
21                 {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
22         static char month[][4] =
23                 {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
24                  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
25         struct tm *tm = gmtime(&t);
26         return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday],
27                    tm->tm_mday, month[tm->tm_mon], 1900 + tm->tm_year,
28                    tm->tm_hour, tm->tm_min, tm->tm_sec);
29 }
30
31 void cgit_print_error(const char *fmt, ...)
32 {
33         va_list ap;
34         va_start(ap, fmt);
35         cgit_vprint_error(fmt, ap);
36         va_end(ap);
37 }
38
39 void cgit_vprint_error(const char *fmt, va_list ap)
40 {
41         va_list cp;
42         html("<div class='error'>");
43         va_copy(cp, ap);
44         html_vtxtf(fmt, cp);
45         va_end(cp);
46         html("</div>\n");
47 }
48
49 const char *cgit_httpscheme(void)
50 {
51         if (ctx.env.https && !strcmp(ctx.env.https, "on"))
52                 return "https://";
53         else
54                 return "http://";
55 }
56
57 char *cgit_hosturl(void)
58 {
59         if (ctx.env.http_host)
60                 return xstrdup(ctx.env.http_host);
61         if (!ctx.env.server_name)
62                 return NULL;
63         if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
64                 return xstrdup(ctx.env.server_name);
65         return fmtalloc("%s:%s", ctx.env.server_name, ctx.env.server_port);
66 }
67
68 char *cgit_currenturl(void)
69 {
70         const char *root = cgit_rooturl();
71         size_t len = strlen(root);
72
73         if (!ctx.qry.url)
74                 return xstrdup(root);
75         if (len && root[len - 1] == '/')
76                 return fmtalloc("%s%s", root, ctx.qry.url);
77         return fmtalloc("%s/%s", root, ctx.qry.url);
78 }
79
80 const char *cgit_rooturl(void)
81 {
82         if (ctx.cfg.virtual_root)
83                 return ctx.cfg.virtual_root;
84         else
85                 return ctx.cfg.script_name;
86 }
87
88 const char *cgit_loginurl(void)
89 {
90         static const char *login_url;
91         if (!login_url)
92                 login_url = fmtalloc("%s?p=login", cgit_rooturl());
93         return login_url;
94 }
95
96 char *cgit_repourl(const char *reponame)
97 {
98         if (ctx.cfg.virtual_root)
99                 return fmtalloc("%s%s/", ctx.cfg.virtual_root, reponame);
100         else
101                 return fmtalloc("?r=%s", reponame);
102 }
103
104 char *cgit_fileurl(const char *reponame, const char *pagename,
105                    const char *filename, const char *query)
106 {
107         struct strbuf sb = STRBUF_INIT;
108         char *delim;
109
110         if (ctx.cfg.virtual_root) {
111                 strbuf_addf(&sb, "%s%s/%s/%s", ctx.cfg.virtual_root, reponame,
112                             pagename, (filename ? filename:""));
113                 delim = "?";
114         } else {
115                 strbuf_addf(&sb, "?url=%s/%s/%s", reponame, pagename,
116                             (filename ? filename : ""));
117                 delim = "&amp;";
118         }
119         if (query)
120                 strbuf_addf(&sb, "%s%s", delim, query);
121         return strbuf_detach(&sb, NULL);
122 }
123
124 char *cgit_pageurl(const char *reponame, const char *pagename,
125                    const char *query)
126 {
127         return cgit_fileurl(reponame, pagename, NULL, query);
128 }
129
130 const char *cgit_repobasename(const char *reponame)
131 {
132         /* I assume we don't need to store more than one repo basename */
133         static char rvbuf[1024];
134         int p;
135         const char *rv;
136         strncpy(rvbuf, reponame, sizeof(rvbuf));
137         if (rvbuf[sizeof(rvbuf)-1])
138                 die("cgit_repobasename: truncated repository name '%s'", reponame);
139         p = strlen(rvbuf)-1;
140         /* strip trailing slashes */
141         while (p && rvbuf[p] == '/') rvbuf[p--] = 0;
142         /* strip trailing .git */
143         if (p >= 3 && starts_with(&rvbuf[p-3], ".git")) {
144                 p -= 3; rvbuf[p--] = 0;
145         }
146         /* strip more trailing slashes if any */
147         while ( p && rvbuf[p] == '/') rvbuf[p--] = 0;
148         /* find last slash in the remaining string */
149         rv = strrchr(rvbuf,'/');
150         if (rv)
151                 return ++rv;
152         return rvbuf;
153 }
154
155 static void site_url(const char *page, const char *search, const char *sort, int ofs, int always_root)
156 {
157         char *delim = "?";
158
159         if (always_root || page)
160                 html_attr(cgit_rooturl());
161         else {
162                 char *currenturl = cgit_currenturl();
163                 html_attr(currenturl);
164                 free(currenturl);
165         }
166
167         if (page) {
168                 htmlf("?p=%s", page);
169                 delim = "&amp;";
170         }
171         if (search) {
172                 html(delim);
173                 html("q=");
174                 html_attr(search);
175                 delim = "&amp;";
176         }
177         if (sort) {
178                 html(delim);
179                 html("s=");
180                 html_attr(sort);
181                 delim = "&amp;";
182         }
183         if (ofs) {
184                 html(delim);
185                 htmlf("ofs=%d", ofs);
186         }
187 }
188
189 static void site_link(const char *page, const char *name, const char *title,
190                       const char *class, const char *search, const char *sort, int ofs, int always_root)
191 {
192         html("<a");
193         if (title) {
194                 html(" title='");
195                 html_attr(title);
196                 html("'");
197         }
198         if (class) {
199                 html(" class='");
200                 html_attr(class);
201                 html("'");
202         }
203         html(" href='");
204         site_url(page, search, sort, ofs, always_root);
205         html("'>");
206         html_txt(name);
207         html("</a>");
208 }
209
210 void cgit_index_link(const char *name, const char *title, const char *class,
211                      const char *pattern, const char *sort, int ofs, int always_root)
212 {
213         site_link(NULL, name, title, class, pattern, sort, ofs, always_root);
214 }
215
216 static char *repolink(const char *title, const char *class, const char *page,
217                       const char *head, const char *path)
218 {
219         char *delim = "?";
220
221         html("<a");
222         if (title) {
223                 html(" title='");
224                 html_attr(title);
225                 html("'");
226         }
227         if (class) {
228                 html(" class='");
229                 html_attr(class);
230                 html("'");
231         }
232         html(" href='");
233         if (ctx.cfg.virtual_root) {
234                 html_url_path(ctx.cfg.virtual_root);
235                 html_url_path(ctx.repo->url);
236                 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
237                         html("/");
238                 if (page) {
239                         html_url_path(page);
240                         html("/");
241                         if (path)
242                                 html_url_path(path);
243                 }
244         } else {
245                 html_url_path(ctx.cfg.script_name);
246                 html("?url=");
247                 html_url_arg(ctx.repo->url);
248                 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
249                         html("/");
250                 if (page) {
251                         html_url_arg(page);
252                         html("/");
253                         if (path)
254                                 html_url_arg(path);
255                 }
256                 delim = "&amp;";
257         }
258         if (head && ctx.repo->defbranch && strcmp(head, ctx.repo->defbranch)) {
259                 html(delim);
260                 html("h=");
261                 html_url_arg(head);
262                 delim = "&amp;";
263         }
264         return fmt("%s", delim);
265 }
266
267 static void reporevlink(const char *page, const char *name, const char *title,
268                         const char *class, const char *head, const char *rev,
269                         const char *path)
270 {
271         char *delim;
272
273         delim = repolink(title, class, page, head, path);
274         if (rev && ctx.qry.head != NULL && strcmp(rev, ctx.qry.head)) {
275                 html(delim);
276                 html("id=");
277                 html_url_arg(rev);
278         }
279         html("'>");
280         html_txt(name);
281         html("</a>");
282 }
283
284 void cgit_summary_link(const char *name, const char *title, const char *class,
285                        const char *head)
286 {
287         reporevlink(NULL, name, title, class, head, NULL, NULL);
288 }
289
290 void cgit_tag_link(const char *name, const char *title, const char *class,
291                    const char *tag)
292 {
293         reporevlink("tag", name, title, class, tag, NULL, NULL);
294 }
295
296 void cgit_tree_link(const char *name, const char *title, const char *class,
297                     const char *head, const char *rev, const char *path)
298 {
299         reporevlink("tree", name, title, class, head, rev, path);
300 }
301
302 void cgit_plain_link(const char *name, const char *title, const char *class,
303                      const char *head, const char *rev, const char *path)
304 {
305         reporevlink("plain", name, title, class, head, rev, path);
306 }
307
308 void cgit_blame_link(const char *name, const char *title, const char *class,
309                      const char *head, const char *rev, const char *path)
310 {
311         reporevlink("blame", name, title, class, head, rev, path);
312 }
313
314 void cgit_log_link(const char *name, const char *title, const char *class,
315                    const char *head, const char *rev, const char *path,
316                    int ofs, const char *grep, const char *pattern, int showmsg,
317                    int follow)
318 {
319         char *delim;
320
321         delim = repolink(title, class, "log", head, path);
322         if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
323                 html(delim);
324                 html("id=");
325                 html_url_arg(rev);
326                 delim = "&amp;";
327         }
328         if (grep && pattern) {
329                 html(delim);
330                 html("qt=");
331                 html_url_arg(grep);
332                 delim = "&amp;";
333                 html(delim);
334                 html("q=");
335                 html_url_arg(pattern);
336         }
337         if (ofs > 0) {
338                 html(delim);
339                 html("ofs=");
340                 htmlf("%d", ofs);
341                 delim = "&amp;";
342         }
343         if (showmsg) {
344                 html(delim);
345                 html("showmsg=1");
346                 delim = "&amp;";
347         }
348         if (follow) {
349                 html(delim);
350                 html("follow=1");
351         }
352         html("'>");
353         html_txt(name);
354         html("</a>");
355 }
356
357 void cgit_commit_link(const char *name, const char *title, const char *class,
358                       const char *head, const char *rev, const char *path)
359 {
360         char *delim;
361
362         delim = repolink(title, class, "commit", head, path);
363         if (rev && ctx.qry.head && strcmp(rev, ctx.qry.head)) {
364                 html(delim);
365                 html("id=");
366                 html_url_arg(rev);
367                 delim = "&amp;";
368         }
369         if (ctx.qry.difftype) {
370                 html(delim);
371                 htmlf("dt=%d", ctx.qry.difftype);
372                 delim = "&amp;";
373         }
374         if (ctx.qry.context > 0 && ctx.qry.context != 3) {
375                 html(delim);
376                 html("context=");
377                 htmlf("%d", ctx.qry.context);
378                 delim = "&amp;";
379         }
380         if (ctx.qry.ignorews) {
381                 html(delim);
382                 html("ignorews=1");
383                 delim = "&amp;";
384         }
385         if (ctx.qry.follow) {
386                 html(delim);
387                 html("follow=1");
388         }
389         html("'>");
390         if (name[0] != '\0') {
391                 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
392                         html_ntxt(name, ctx.cfg.max_msg_len - 3);
393                         html("...");
394                 } else
395                         html_txt(name);
396         } else
397                 html_txt("(no commit message)");
398         html("</a>");
399 }
400
401 void cgit_refs_link(const char *name, const char *title, const char *class,
402                     const char *head, const char *rev, const char *path)
403 {
404         reporevlink("refs", name, title, class, head, rev, path);
405 }
406
407 void cgit_snapshot_link(const char *name, const char *title, const char *class,
408                         const char *head, const char *rev,
409                         const char *archivename)
410 {
411         reporevlink("snapshot", name, title, class, head, rev, archivename);
412 }
413
414 void cgit_diff_link(const char *name, const char *title, const char *class,
415                     const char *head, const char *new_rev, const char *old_rev,
416                     const char *path)
417 {
418         char *delim;
419
420         delim = repolink(title, class, "diff", head, path);
421         if (new_rev && ctx.qry.head != NULL && strcmp(new_rev, ctx.qry.head)) {
422                 html(delim);
423                 html("id=");
424                 html_url_arg(new_rev);
425                 delim = "&amp;";
426         }
427         if (old_rev) {
428                 html(delim);
429                 html("id2=");
430                 html_url_arg(old_rev);
431                 delim = "&amp;";
432         }
433         if (ctx.qry.difftype) {
434                 html(delim);
435                 htmlf("dt=%d", ctx.qry.difftype);
436                 delim = "&amp;";
437         }
438         if (ctx.qry.context > 0 && ctx.qry.context != 3) {
439                 html(delim);
440                 html("context=");
441                 htmlf("%d", ctx.qry.context);
442                 delim = "&amp;";
443         }
444         if (ctx.qry.ignorews) {
445                 html(delim);
446                 html("ignorews=1");
447                 delim = "&amp;";
448         }
449         if (ctx.qry.follow) {
450                 html(delim);
451                 html("follow=1");
452         }
453         html("'>");
454         html_txt(name);
455         html("</a>");
456 }
457
458 void cgit_patch_link(const char *name, const char *title, const char *class,
459                      const char *head, const char *rev, const char *path)
460 {
461         reporevlink("patch", name, title, class, head, rev, path);
462 }
463
464 void cgit_stats_link(const char *name, const char *title, const char *class,
465                      const char *head, const char *path)
466 {
467         reporevlink("stats", name, title, class, head, NULL, path);
468 }
469
470 static void cgit_self_link(char *name, const char *title, const char *class)
471 {
472         if (!strcmp(ctx.qry.page, "repolist"))
473                 cgit_index_link(name, title, class, ctx.qry.search, ctx.qry.sort,
474                                 ctx.qry.ofs, 1);
475         else if (!strcmp(ctx.qry.page, "summary"))
476                 cgit_summary_link(name, title, class, ctx.qry.head);
477         else if (!strcmp(ctx.qry.page, "tag"))
478                 cgit_tag_link(name, title, class, ctx.qry.has_sha1 ?
479                                ctx.qry.sha1 : ctx.qry.head);
480         else if (!strcmp(ctx.qry.page, "tree"))
481                 cgit_tree_link(name, title, class, ctx.qry.head,
482                                ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
483                                ctx.qry.path);
484         else if (!strcmp(ctx.qry.page, "plain"))
485                 cgit_plain_link(name, title, class, ctx.qry.head,
486                                 ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
487                                 ctx.qry.path);
488         else if (!strcmp(ctx.qry.page, "blame"))
489                 cgit_blame_link(name, title, class, ctx.qry.head,
490                                 ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
491                                 ctx.qry.path);
492         else if (!strcmp(ctx.qry.page, "log"))
493                 cgit_log_link(name, title, class, ctx.qry.head,
494                               ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
495                               ctx.qry.path, ctx.qry.ofs,
496                               ctx.qry.grep, ctx.qry.search,
497                               ctx.qry.showmsg, ctx.qry.follow);
498         else if (!strcmp(ctx.qry.page, "commit"))
499                 cgit_commit_link(name, title, class, ctx.qry.head,
500                                  ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
501                                  ctx.qry.path);
502         else if (!strcmp(ctx.qry.page, "patch"))
503                 cgit_patch_link(name, title, class, ctx.qry.head,
504                                 ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
505                                 ctx.qry.path);
506         else if (!strcmp(ctx.qry.page, "refs"))
507                 cgit_refs_link(name, title, class, ctx.qry.head,
508                                ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
509                                ctx.qry.path);
510         else if (!strcmp(ctx.qry.page, "snapshot"))
511                 cgit_snapshot_link(name, title, class, ctx.qry.head,
512                                    ctx.qry.has_sha1 ? ctx.qry.sha1 : NULL,
513                                    ctx.qry.path);
514         else if (!strcmp(ctx.qry.page, "diff"))
515                 cgit_diff_link(name, title, class, ctx.qry.head,
516                                ctx.qry.sha1, ctx.qry.sha2,
517                                ctx.qry.path);
518         else if (!strcmp(ctx.qry.page, "stats"))
519                 cgit_stats_link(name, title, class, ctx.qry.head,
520                                 ctx.qry.path);
521         else {
522                 /* Don't known how to make link for this page */
523                 repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path);
524                 html("><!-- cgit_self_link() doesn't know how to make link for page '");
525                 html_txt(ctx.qry.page);
526                 html("' -->");
527                 html_txt(name);
528                 html("</a>");
529         }
530 }
531
532 void cgit_object_link(struct object *obj)
533 {
534         char *page, *shortrev, *fullrev, *name;
535
536         fullrev = oid_to_hex(&obj->oid);
537         shortrev = xstrdup(fullrev);
538         shortrev[10] = '\0';
539         if (obj->type == OBJ_COMMIT) {
540                 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
541                                  ctx.qry.head, fullrev, NULL);
542                 return;
543         } else if (obj->type == OBJ_TREE)
544                 page = "tree";
545         else if (obj->type == OBJ_TAG)
546                 page = "tag";
547         else
548                 page = "blob";
549         name = fmt("%s %s...", type_name(obj->type), shortrev);
550         reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
551 }
552
553 static struct string_list_item *lookup_path(struct string_list *list,
554                                             const char *path)
555 {
556         struct string_list_item *item;
557
558         while (path && path[0]) {
559                 if ((item = string_list_lookup(list, path)))
560                         return item;
561                 if (!(path = strchr(path, '/')))
562                         break;
563                 path++;
564         }
565         return NULL;
566 }
567
568 void cgit_submodule_link(const char *class, char *path, const char *rev)
569 {
570         struct string_list *list;
571         struct string_list_item *item;
572         char tail, *dir;
573         size_t len;
574
575         len = 0;
576         tail = 0;
577         list = &ctx.repo->submodules;
578         item = lookup_path(list, path);
579         if (!item) {
580                 len = strlen(path);
581                 tail = path[len - 1];
582                 if (tail == '/') {
583                         path[len - 1] = 0;
584                         item = lookup_path(list, path);
585                 }
586         }
587         if (item || ctx.repo->module_link) {
588                 html("<a ");
589                 if (class)
590                         htmlf("class='%s' ", class);
591                 html("href='");
592                 if (item) {
593                         html_attrf(item->util, rev);
594                 } else {
595                         dir = strrchr(path, '/');
596                         if (dir)
597                                 dir++;
598                         else
599                                 dir = path;
600                         html_attrf(ctx.repo->module_link, dir, rev);
601                 }
602                 html("'>");
603                 html_txt(path);
604                 html("</a>");
605         } else {
606                 html("<span");
607                 if (class)
608                         htmlf(" class='%s'", class);
609                 html(">");
610                 html_txt(path);
611                 html("</span>");
612         }
613         html_txtf(" @ %.7s", rev);
614         if (item && tail)
615                 path[len - 1] = tail;
616 }
617
618 const struct date_mode *cgit_date_mode(enum date_mode_type type)
619 {
620         static struct date_mode mode;
621         mode.type = type;
622         mode.local = ctx.cfg.local_time;
623         return &mode;
624 }
625
626 static void print_rel_date(time_t t, int tz, double value,
627         const char *class, const char *suffix)
628 {
629         htmlf("<span class='%s' title='", class);
630         html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
631         htmlf("'>%.0f %s</span>", value, suffix);
632 }
633
634 void cgit_print_age(time_t t, int tz, time_t max_relative)
635 {
636         time_t now, secs;
637
638         if (!t)
639                 return;
640         time(&now);
641         secs = now - t;
642         if (secs < 0)
643                 secs = 0;
644
645         if (secs > max_relative && max_relative >= 0) {
646                 html("<span title='");
647                 html_attr(show_date(t, tz, cgit_date_mode(DATE_ISO8601)));
648                 html("'>");
649                 html_txt(show_date(t, tz, cgit_date_mode(DATE_SHORT)));
650                 html("</span>");
651                 return;
652         }
653
654         if (secs < TM_HOUR * 2) {
655                 print_rel_date(t, tz, secs * 1.0 / TM_MIN, "age-mins", "min.");
656                 return;
657         }
658         if (secs < TM_DAY * 2) {
659                 print_rel_date(t, tz, secs * 1.0 / TM_HOUR, "age-hours", "hours");
660                 return;
661         }
662         if (secs < TM_WEEK * 2) {
663                 print_rel_date(t, tz, secs * 1.0 / TM_DAY, "age-days", "days");
664                 return;
665         }
666         if (secs < TM_MONTH * 2) {
667                 print_rel_date(t, tz, secs * 1.0 / TM_WEEK, "age-weeks", "weeks");
668                 return;
669         }
670         if (secs < TM_YEAR * 2) {
671                 print_rel_date(t, tz, secs * 1.0 / TM_MONTH, "age-months", "months");
672                 return;
673         }
674         print_rel_date(t, tz, secs * 1.0 / TM_YEAR, "age-years", "years");
675 }
676
677 void cgit_print_http_headers(void)
678 {
679         if (ctx.env.no_http && !strcmp(ctx.env.no_http, "1"))
680                 return;
681
682         if (ctx.page.status)
683                 htmlf("Status: %d %s\n", ctx.page.status, ctx.page.statusmsg);
684         if (ctx.page.mimetype && ctx.page.charset)
685                 htmlf("Content-Type: %s; charset=%s\n", ctx.page.mimetype,
686                       ctx.page.charset);
687         else if (ctx.page.mimetype)
688                 htmlf("Content-Type: %s\n", ctx.page.mimetype);
689         if (ctx.page.size)
690                 htmlf("Content-Length: %zd\n", ctx.page.size);
691         if (ctx.page.filename) {
692                 html("Content-Disposition: inline; filename=\"");
693                 html_header_arg_in_quotes(ctx.page.filename);
694                 html("\"\n");
695         }
696         if (!ctx.env.authenticated)
697                 html("Cache-Control: no-cache, no-store\n");
698         htmlf("Last-Modified: %s\n", http_date(ctx.page.modified));
699         htmlf("Expires: %s\n", http_date(ctx.page.expires));
700         if (ctx.page.etag)
701                 htmlf("ETag: \"%s\"\n", ctx.page.etag);
702         html("\n");
703         if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
704                 exit(0);
705 }
706
707 void cgit_redirect(const char *url, bool permanent)
708 {
709         htmlf("Status: %d %s\n", permanent ? 301 : 302, permanent ? "Moved" : "Found");
710         html("Location: ");
711         html_url_path(url);
712         html("\n\n");
713 }
714
715 static void print_rel_vcs_link(const char *url)
716 {
717         html("<link rel='vcs-git' href='");
718         html_attr(url);
719         html("' title='");
720         html_attr(ctx.repo->name);
721         html(" Git repository'/>\n");
722 }
723
724 void cgit_print_docstart(void)
725 {
726         char *host = cgit_hosturl();
727
728         if (ctx.cfg.embedded) {
729                 if (ctx.cfg.header)
730                         html_include(ctx.cfg.header);
731                 return;
732         }
733
734         html(cgit_doctype);
735         html("<html lang='en'>\n");
736         html("<head>\n");
737         html("<title>");
738         html_txt(ctx.page.title);
739         html("</title>\n");
740         htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
741         if (ctx.cfg.robots && *ctx.cfg.robots)
742                 htmlf("<meta name='robots' content='%s'/>\n", ctx.cfg.robots);
743         html("<link rel='stylesheet' type='text/css' href='");
744         html_attr(ctx.cfg.css);
745         html("'/>\n");
746         if (ctx.cfg.favicon) {
747                 html("<link rel='shortcut icon' href='");
748                 html_attr(ctx.cfg.favicon);
749                 html("'/>\n");
750         }
751         if (host && ctx.repo && ctx.qry.head) {
752                 char *fileurl;
753                 struct strbuf sb = STRBUF_INIT;
754                 strbuf_addf(&sb, "h=%s", ctx.qry.head);
755
756                 html("<link rel='alternate' title='Atom feed' href='");
757                 html(cgit_httpscheme());
758                 html_attr(host);
759                 fileurl = cgit_fileurl(ctx.repo->url, "atom", ctx.qry.vpath,
760                                        sb.buf);
761                 html_attr(fileurl);
762                 html("' type='application/atom+xml'/>\n");
763                 strbuf_release(&sb);
764                 free(fileurl);
765         }
766         if (ctx.repo)
767                 cgit_add_clone_urls(print_rel_vcs_link);
768         if (ctx.cfg.head_include)
769                 html_include(ctx.cfg.head_include);
770         html("</head>\n");
771         html("<body>\n");
772         if (ctx.cfg.header)
773                 html_include(ctx.cfg.header);
774         free(host);
775 }
776
777 void cgit_print_docend(void)
778 {
779         html("</div> <!-- class=content -->\n");
780         if (ctx.cfg.embedded) {
781                 html("</div> <!-- id=cgit -->\n");
782                 if (ctx.cfg.footer)
783                         html_include(ctx.cfg.footer);
784                 return;
785         }
786         if (ctx.cfg.footer)
787                 html_include(ctx.cfg.footer);
788         else {
789                 htmlf("<div class='footer'>generated by <a href='https://git.zx2c4.com/cgit/about/'>cgit %s</a> "
790                         "(<a href='https://git-scm.com/'>git %s</a>) at ", cgit_version, git_version_string);
791                 html_txt(show_date(time(NULL), 0, cgit_date_mode(DATE_ISO8601)));
792                 html("</div>\n");
793         }
794         html("</div> <!-- id=cgit -->\n");
795         html("</body>\n</html>\n");
796 }
797
798 void cgit_print_error_page(int code, const char *msg, const char *fmt, ...)
799 {
800         va_list ap;
801         ctx.page.expires = ctx.cfg.cache_dynamic_ttl;
802         ctx.page.status = code;
803         ctx.page.statusmsg = msg;
804         cgit_print_layout_start();
805         va_start(ap, fmt);
806         cgit_vprint_error(fmt, ap);
807         va_end(ap);
808         cgit_print_layout_end();
809 }
810
811 void cgit_print_layout_start(void)
812 {
813         cgit_print_http_headers();
814         cgit_print_docstart();
815         cgit_print_pageheader();
816 }
817
818 void cgit_print_layout_end(void)
819 {
820         cgit_print_docend();
821 }
822
823 static void add_clone_urls(void (*fn)(const char *), char *txt, char *suffix)
824 {
825         struct strbuf **url_list = strbuf_split_str(txt, ' ', 0);
826         int i;
827
828         for (i = 0; url_list[i]; i++) {
829                 strbuf_rtrim(url_list[i]);
830                 if (url_list[i]->len == 0)
831                         continue;
832                 if (suffix && *suffix)
833                         strbuf_addf(url_list[i], "/%s", suffix);
834                 fn(url_list[i]->buf);
835         }
836
837         strbuf_list_free(url_list);
838 }
839
840 void cgit_add_clone_urls(void (*fn)(const char *))
841 {
842         if (ctx.repo->clone_url)
843                 add_clone_urls(fn, expand_macros(ctx.repo->clone_url), NULL);
844         else if (ctx.cfg.clone_prefix)
845                 add_clone_urls(fn, ctx.cfg.clone_prefix, ctx.repo->url);
846 }
847
848 static int print_branch_option(const char *refname, const struct object_id *oid,
849                                int flags, void *cb_data)
850 {
851         char *name = (char *)refname;
852         html_option(name, name, ctx.qry.head);
853         return 0;
854 }
855
856 void cgit_add_hidden_formfields(int incl_head, int incl_search,
857                                 const char *page)
858 {
859         if (!ctx.cfg.virtual_root) {
860                 struct strbuf url = STRBUF_INIT;
861
862                 strbuf_addf(&url, "%s/%s", ctx.qry.repo, page);
863                 if (ctx.qry.vpath)
864                         strbuf_addf(&url, "/%s", ctx.qry.vpath);
865                 html_hidden("url", url.buf);
866                 strbuf_release(&url);
867         }
868
869         if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
870             strcmp(ctx.qry.head, ctx.repo->defbranch))
871                 html_hidden("h", ctx.qry.head);
872
873         if (ctx.qry.sha1)
874                 html_hidden("id", ctx.qry.sha1);
875         if (ctx.qry.sha2)
876                 html_hidden("id2", ctx.qry.sha2);
877         if (ctx.qry.showmsg)
878                 html_hidden("showmsg", "1");
879
880         if (incl_search) {
881                 if (ctx.qry.grep)
882                         html_hidden("qt", ctx.qry.grep);
883                 if (ctx.qry.search)
884                         html_hidden("q", ctx.qry.search);
885         }
886 }
887
888 static const char *hc(const char *page)
889 {
890         if (!ctx.qry.page)
891                 return NULL;
892
893         return strcmp(ctx.qry.page, page) ? NULL : "active";
894 }
895
896 static void cgit_print_path_crumbs(char *path)
897 {
898         char *old_path = ctx.qry.path;
899         char *p = path, *q, *end = path + strlen(path);
900
901         ctx.qry.path = NULL;
902         cgit_self_link("root", NULL, NULL);
903         ctx.qry.path = p = path;
904         while (p < end) {
905                 if (!(q = strchr(p, '/')))
906                         q = end;
907                 *q = '\0';
908                 html_txt("/");
909                 cgit_self_link(p, NULL, NULL);
910                 if (q < end)
911                         *q = '/';
912                 p = q + 1;
913         }
914         ctx.qry.path = old_path;
915 }
916
917 static void print_header(void)
918 {
919         char *logo = NULL, *logo_link = NULL;
920
921         html("<table id='header'>\n");
922         html("<tr>\n");
923
924         if (ctx.repo && ctx.repo->logo && *ctx.repo->logo)
925                 logo = ctx.repo->logo;
926         else
927                 logo = ctx.cfg.logo;
928         if (ctx.repo && ctx.repo->logo_link && *ctx.repo->logo_link)
929                 logo_link = ctx.repo->logo_link;
930         else
931                 logo_link = ctx.cfg.logo_link;
932         if (logo && *logo) {
933                 html("<td class='logo' rowspan='2'><a href='");
934                 if (logo_link && *logo_link)
935                         html_attr(logo_link);
936                 else
937                         html_attr(cgit_rooturl());
938                 html("'><img src='");
939                 html_attr(logo);
940                 html("' alt='cgit logo'/></a></td>\n");
941         }
942
943         html("<td class='main'>");
944         if (ctx.repo) {
945                 cgit_index_link("index", NULL, NULL, NULL, NULL, 0, 1);
946                 html(" : ");
947                 cgit_summary_link(ctx.repo->name, ctx.repo->name, NULL, NULL);
948                 if (ctx.env.authenticated) {
949                         html("</td><td class='form'>");
950                         html("<form method='get'>\n");
951                         cgit_add_hidden_formfields(0, 1, ctx.qry.page);
952                         html("<select name='h' onchange='this.form.submit();'>\n");
953                         for_each_branch_ref(print_branch_option, ctx.qry.head);
954                         if (ctx.repo->enable_remote_branches)
955                                 for_each_remote_ref(print_branch_option, ctx.qry.head);
956                         html("</select> ");
957                         html("<input type='submit' value='switch'/>");
958                         html("</form>");
959                 }
960         } else
961                 html_txt(ctx.cfg.root_title);
962         html("</td></tr>\n");
963
964         html("<tr><td class='sub'>");
965         if (ctx.repo) {
966                 html_txt(ctx.repo->desc);
967                 html("</td><td class='sub right'>");
968                 html_txt(ctx.repo->owner);
969         } else {
970                 if (ctx.cfg.root_desc)
971                         html_txt(ctx.cfg.root_desc);
972                 else if (ctx.cfg.index_info)
973                         html_include(ctx.cfg.index_info);
974         }
975         html("</td></tr></table>\n");
976 }
977
978 void cgit_print_pageheader(void)
979 {
980         html("<div id='cgit'>");
981         if (!ctx.env.authenticated || !ctx.cfg.noheader)
982                 print_header();
983
984         html("<table class='tabs'><tr><td>\n");
985         if (ctx.env.authenticated && ctx.repo) {
986                 if (ctx.repo->readme.nr)
987                         reporevlink("about", "about", NULL,
988                                     hc("about"), ctx.qry.head, NULL,
989                                     NULL);
990                 cgit_summary_link("summary", NULL, hc("summary"),
991                                   ctx.qry.head);
992                 cgit_refs_link("refs", NULL, hc("refs"), ctx.qry.head,
993                                ctx.qry.sha1, NULL);
994                 cgit_log_link("log", NULL, hc("log"), ctx.qry.head,
995                               NULL, ctx.qry.vpath, 0, NULL, NULL,
996                               ctx.qry.showmsg, ctx.qry.follow);
997                 if (ctx.qry.page && !strcmp(ctx.qry.page, "blame"))
998                         cgit_blame_link("blame", NULL, hc("blame"), ctx.qry.head,
999                                         ctx.qry.sha1, ctx.qry.vpath);
1000                 else
1001                         cgit_tree_link("tree", NULL, hc("tree"), ctx.qry.head,
1002                                        ctx.qry.sha1, ctx.qry.vpath);
1003                 cgit_commit_link("commit", NULL, hc("commit"),
1004                                  ctx.qry.head, ctx.qry.sha1, ctx.qry.vpath);
1005                 cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head,
1006                                ctx.qry.sha1, ctx.qry.sha2, ctx.qry.vpath);
1007                 if (ctx.repo->max_stats)
1008                         cgit_stats_link("stats", NULL, hc("stats"),
1009                                         ctx.qry.head, ctx.qry.vpath);
1010                 if (ctx.repo->homepage) {
1011                         html("<a href='");
1012                         html_attr(ctx.repo->homepage);
1013                         html("'>homepage</a>");
1014                 }
1015                 html("</td><td class='form'>");
1016                 html("<form class='right' method='get' action='");
1017                 if (ctx.cfg.virtual_root) {
1018                         char *fileurl = cgit_fileurl(ctx.qry.repo, "log",
1019                                                    ctx.qry.vpath, NULL);
1020                         html_url_path(fileurl);
1021                         free(fileurl);
1022                 }
1023                 html("'>\n");
1024                 cgit_add_hidden_formfields(1, 0, "log");
1025                 html("<select name='qt'>\n");
1026                 html_option("grep", "log msg", ctx.qry.grep);
1027                 html_option("author", "author", ctx.qry.grep);
1028                 html_option("committer", "committer", ctx.qry.grep);
1029                 html_option("range", "range", ctx.qry.grep);
1030                 html("</select>\n");
1031                 html("<input class='txt' type='search' size='10' name='q' value='");
1032                 html_attr(ctx.qry.search);
1033                 html("'/>\n");
1034                 html("<input type='submit' value='search'/>\n");
1035                 html("</form>\n");
1036         } else if (ctx.env.authenticated) {
1037                 char *currenturl = cgit_currenturl();
1038                 site_link(NULL, "index", NULL, hc("repolist"), NULL, NULL, 0, 1);
1039                 if (ctx.cfg.root_readme)
1040                         site_link("about", "about", NULL, hc("about"),
1041                                   NULL, NULL, 0, 1);
1042                 html("</td><td class='form'>");
1043                 html("<form method='get' action='");
1044                 html_attr(currenturl);
1045                 html("'>\n");
1046                 html("<input type='search' name='q' size='10' value='");
1047                 html_attr(ctx.qry.search);
1048                 html("'/>\n");
1049                 html("<input type='submit' value='search'/>\n");
1050                 html("</form>");
1051                 free(currenturl);
1052         }
1053         html("</td></tr></table>\n");
1054         if (ctx.env.authenticated && ctx.repo && ctx.qry.vpath) {
1055                 html("<div class='path'>");
1056                 html("path: ");
1057                 cgit_print_path_crumbs(ctx.qry.vpath);
1058                 if (ctx.cfg.enable_follow_links && !strcmp(ctx.qry.page, "log")) {
1059                         html(" (");
1060                         ctx.qry.follow = !ctx.qry.follow;
1061                         cgit_self_link(ctx.qry.follow ? "follow" : "unfollow",
1062                                         NULL, NULL);
1063                         ctx.qry.follow = !ctx.qry.follow;
1064                         html(")");
1065                 }
1066                 html("</div>");
1067         }
1068         html("<div class='content'>");
1069 }
1070
1071 void cgit_print_filemode(unsigned short mode)
1072 {
1073         if (S_ISDIR(mode))
1074                 html("d");
1075         else if (S_ISLNK(mode))
1076                 html("l");
1077         else if (S_ISGITLINK(mode))
1078                 html("m");
1079         else
1080                 html("-");
1081         html_fileperm(mode >> 6);
1082         html_fileperm(mode >> 3);
1083         html_fileperm(mode);
1084 }
1085
1086 void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
1087                                   const char *ref)
1088 {
1089         struct object_id oid;
1090
1091         /*
1092          * Prettify snapshot names by stripping leading "v" or "V" if the tag
1093          * name starts with {v,V}[0-9] and the prettify mapping is injective,
1094          * i.e. each stripped tag can be inverted without ambiguities.
1095          */
1096         if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
1097             (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
1098             ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
1099              (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
1100              (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
1101                 ref++;
1102
1103         strbuf_addf(filename, "%s-%s", base, ref);
1104 }
1105
1106 void cgit_print_snapshot_links(const char *repo, const char *head,
1107                                const char *hex, int snapshots)
1108 {
1109         const struct cgit_snapshot_format* f;
1110         struct strbuf filename = STRBUF_INIT;
1111         size_t prefixlen;
1112
1113         cgit_compose_snapshot_prefix(&filename, cgit_repobasename(repo), hex);
1114         prefixlen = filename.len;
1115         for (f = cgit_snapshot_formats; f->suffix; f++) {
1116                 if (!(snapshots & f->bit))
1117                         continue;
1118                 strbuf_setlen(&filename, prefixlen);
1119                 strbuf_addstr(&filename, f->suffix);
1120                 cgit_snapshot_link(filename.buf, NULL, NULL, NULL, NULL,
1121                                    filename.buf);
1122                 html("<br/>");
1123         }
1124         strbuf_release(&filename);
1125 }
1126
1127 void cgit_set_title_from_path(const char *path)
1128 {
1129         size_t path_len, path_index, path_last_end;
1130         char *new_title;
1131
1132         if (!path)
1133                 return;
1134
1135         path_len = strlen(path);
1136         new_title = xmalloc(path_len + 3 + strlen(ctx.page.title) + 1);
1137         new_title[0] = '\0';
1138
1139         for (path_index = path_len, path_last_end = path_len; path_index-- > 0;) {
1140                 if (path[path_index] == '/') {
1141                         if (path_index == path_len - 1) {
1142                                 path_last_end = path_index - 1;
1143                                 continue;
1144                         }
1145                         strncat(new_title, &path[path_index + 1], path_last_end - path_index - 1);
1146                         strcat(new_title, "\\");
1147                         path_last_end = path_index;
1148                 }
1149         }
1150         if (path_last_end)
1151                 strncat(new_title, path, path_last_end);
1152
1153         strcat(new_title, " - ");
1154         strcat(new_title, ctx.page.title);
1155         ctx.page.title = new_title;
1156 }