2 * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery <tracey@traceyemery.net>
3 * Copyright (c) 2015 Mike Larkin <mlarkin@openbsd.org>
4 * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
5 * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
6 * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
8 * Permission to use, copy, modify, and distribute this software for any
9 * purpose with or without fee is hereby granted, provided that the above
10 * copyright notice and this permission notice appear in all copies.
12 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 #include <netinet/in.h>
23 #include <sys/queue.h>
25 #include <sys/types.h>
38 #include "got_error.h"
39 #include "got_object.h"
40 #include "got_reference.h"
41 #include "got_repository.h"
43 #include "got_cancel.h"
44 #include "got_worktree.h"
46 #include "got_commit_graph.h"
47 #include "got_blame.h"
48 #include "got_privsep.h"
50 #include "got_compat.h"
56 static const struct querystring_keys querystring_keys
[] = {
61 { "headref", HEADREF
},
62 { "index_page", INDEX_PAGE
},
67 static const struct action_keys action_keys
[] = {
71 { "commits", COMMITS
},
75 { "summary", SUMMARY
},
82 static const struct got_error
*gotweb_init_querystring(struct querystring
**);
83 static const struct got_error
*gotweb_parse_querystring(struct querystring
**,
85 static const struct got_error
*gotweb_assign_querystring(struct querystring
**,
87 static const struct got_error
*gotweb_render_index(struct request
*);
88 static const struct got_error
*gotweb_init_repo_dir(struct repo_dir
**,
90 static const struct got_error
*gotweb_load_got_path(struct request
*c
,
92 static const struct got_error
*gotweb_get_repo_description(char **,
93 struct server
*, const char *, int);
94 static const struct got_error
*gotweb_get_clone_url(char **, struct server
*,
96 static const struct got_error
*gotweb_render_blame(struct request
*);
97 static const struct got_error
*gotweb_render_diff(struct request
*);
98 static const struct got_error
*gotweb_render_summary(struct request
*);
99 static const struct got_error
*gotweb_render_tag(struct request
*);
100 static const struct got_error
*gotweb_render_tags(struct request
*);
101 static const struct got_error
*gotweb_render_tree(struct request
*);
102 static const struct got_error
*gotweb_render_branches(struct request
*);
104 static void gotweb_free_querystring(struct querystring
*);
105 static void gotweb_free_repo_dir(struct repo_dir
*);
107 struct server
*gotweb_get_server(uint8_t *, uint8_t *);
110 gotweb_process_request(struct request
*c
)
112 const struct got_error
*error
= NULL
, *error2
= NULL
;
113 struct server
*srv
= NULL
;
114 struct querystring
*qs
= NULL
;
115 struct repo_dir
*repo_dir
= NULL
;
116 uint8_t err
[] = "gotwebd experienced an error: ";
119 /* init the transport */
120 error
= gotweb_init_transport(&c
->t
);
122 log_warnx("%s: %s", __func__
, error
->msg
);
125 /* don't process any further if client disconnected */
126 if (c
->sock
->client_status
== CLIENT_DISCONNECT
)
128 /* get the gotwebd server */
129 srv
= gotweb_get_server(c
->server_name
, c
->http_host
);
131 log_warnx("%s: error server is NULL", __func__
);
135 /* parse our querystring */
136 error
= gotweb_init_querystring(&qs
);
138 log_warnx("%s: %s", __func__
, error
->msg
);
142 error
= gotweb_parse_querystring(&qs
, c
->querystring
);
144 log_warnx("%s: %s", __func__
, error
->msg
);
149 * certain actions require a commit id in the querystring. this stops
150 * bad actors from exploiting this by manually manipulating the
154 if (qs
->commit
== NULL
&& (qs
->action
== BLAME
|| qs
->action
== BLOB
||
155 qs
->action
== DIFF
)) {
156 error2
= got_error(GOT_ERR_QUERYSTRING
);
160 if (qs
->action
!= INDEX
) {
161 error
= gotweb_init_repo_dir(&repo_dir
, qs
->path
);
164 error
= gotweb_load_got_path(c
, repo_dir
);
165 c
->t
->repo_dir
= repo_dir
;
166 if (error
&& error
->code
!= GOT_ERR_LONELY_PACKIDX
)
170 if (qs
!= NULL
&& qs
->action
== BLOB
) {
171 error
= got_get_repo_commits(c
, 1);
174 error
= got_output_file_blob(c
);
176 log_warnx("%s: %s", __func__
, error
->msg
);
182 if (qs
->action
== RSS
) {
183 error
= gotweb_render_content_type(c
,
184 "application/rss+xml;charset=utf-8");
186 log_warnx("%s: %s", __func__
, error
->msg
);
190 error
= got_get_repo_tags(c
, D_MAXSLCOMMDISP
);
192 log_warnx("%s: %s", __func__
, error
->msg
);
195 if (gotweb_render_rss(c
->tp
) == -1)
201 error
= gotweb_render_content_type(c
, "text/html");
203 log_warnx("%s: %s", __func__
, error
->msg
);
208 if (gotweb_render_header(c
->tp
) == -1)
218 error
= gotweb_render_blame(c
);
220 log_warnx("%s: %s", __func__
, error
->msg
);
225 if (gotweb_render_briefs(c
->tp
) == -1)
229 error
= got_get_repo_commits(c
, srv
->max_commits_display
);
231 log_warnx("%s: %s", __func__
, error
->msg
);
234 if (gotweb_render_commits(c
->tp
) == -1)
238 error
= gotweb_render_diff(c
);
240 log_warnx("%s: %s", __func__
, error
->msg
);
245 error
= gotweb_render_index(c
);
247 log_warnx("%s: %s", __func__
, error
->msg
);
252 error
= gotweb_render_summary(c
);
254 log_warnx("%s: %s", __func__
, error
->msg
);
259 error
= gotweb_render_tag(c
);
261 log_warnx("%s: %s", __func__
, error
->msg
);
266 error
= gotweb_render_tags(c
);
268 log_warnx("%s: %s", __func__
, error
->msg
);
273 error
= gotweb_render_tree(c
);
275 log_warnx("%s: %s", __func__
, error
->msg
);
281 r
= fcgi_printf(c
, "<div id='err_content'>%s</div>\n",
282 "Erorr: Bad Querystring");
290 if (html
&& fcgi_printf(c
, "<div id='err_content'>") == -1)
292 if (fcgi_printf(c
, "\n%s", err
) == -1)
295 if (fcgi_printf(c
, "%s", error
->msg
) == -1)
298 if (fcgi_printf(c
, "see daemon logs for details") == -1)
301 if (html
&& fcgi_printf(c
, "</div>\n") == -1)
304 if (html
&& srv
!= NULL
)
305 gotweb_render_footer(c
->tp
);
309 gotweb_get_server(uint8_t *server_name
, uint8_t *subdomain
)
311 struct server
*srv
= NULL
;
313 /* check against the server name first */
314 if (strlen(server_name
) > 0)
315 TAILQ_FOREACH(srv
, &gotwebd_env
->servers
, entry
)
316 if (strcmp(srv
->name
, server_name
) == 0)
319 /* check against subdomain second */
320 if (strlen(subdomain
) > 0)
321 TAILQ_FOREACH(srv
, &gotwebd_env
->servers
, entry
)
322 if (strcmp(srv
->name
, subdomain
) == 0)
325 /* if those fail, send first server */
326 TAILQ_FOREACH(srv
, &gotwebd_env
->servers
, entry
)
333 const struct got_error
*
334 gotweb_init_transport(struct transport
**t
)
336 const struct got_error
*error
= NULL
;
338 *t
= calloc(1, sizeof(**t
));
340 return got_error_from_errno2("%s: calloc", __func__
);
342 TAILQ_INIT(&(*t
)->repo_commits
);
343 TAILQ_INIT(&(*t
)->repo_tags
);
346 (*t
)->repo_dir
= NULL
;
348 (*t
)->next_id
= NULL
;
349 (*t
)->prev_id
= NULL
;
356 static const struct got_error
*
357 gotweb_init_querystring(struct querystring
**qs
)
359 const struct got_error
*error
= NULL
;
361 *qs
= calloc(1, sizeof(**qs
));
363 return got_error_from_errno2("%s: calloc", __func__
);
365 (*qs
)->headref
= strdup("HEAD");
366 if ((*qs
)->headref
== NULL
) {
369 return got_error_from_errno2("%s: strdup", __func__
);
372 (*qs
)->action
= INDEX
;
373 (*qs
)->commit
= NULL
;
375 (*qs
)->folder
= NULL
;
376 (*qs
)->index_page
= 0;
382 static const struct got_error
*
383 gotweb_parse_querystring(struct querystring
**qs
, char *qst
)
385 const struct got_error
*error
= NULL
;
386 char *tok1
= NULL
, *tok1_pair
= NULL
, *tok1_end
= NULL
;
387 char *tok2
= NULL
, *tok2_pair
= NULL
, *tok2_end
= NULL
;
394 return got_error_from_errno2("%s: strdup", __func__
);
399 while (tok1_pair
!= NULL
) {
400 strsep(&tok1_end
, "&");
402 tok2
= strdup(tok1_pair
);
405 return got_error_from_errno2("%s: strdup", __func__
);
411 while (tok2_pair
!= NULL
) {
412 strsep(&tok2_end
, "=");
414 error
= gotweb_assign_querystring(qs
, tok2_pair
,
419 tok2_pair
= tok2_end
;
422 tok1_pair
= tok1_end
;
433 * Adapted from usr.sbin/httpd/httpd.c url_decode.
435 static const struct got_error
*
436 gotweb_urldecode(char *url
)
448 /* Encoding character is followed by two hex chars */
449 if (!isxdigit((unsigned char)p
[1]) ||
450 !isxdigit((unsigned char)p
[2]) ||
451 (p
[1] == '0' && p
[2] == '0'))
452 return got_error(GOT_ERR_BAD_QUERYSTRING
);
458 * We don't have to validate "hex" because it is
459 * guaranteed to include two hex chars followed by nul.
461 x
= strtoul(hex
, NULL
, 16);
477 static const struct got_error
*
478 gotweb_assign_querystring(struct querystring
**qs
, char *key
, char *value
)
480 const struct got_error
*error
= NULL
;
484 error
= gotweb_urldecode(value
);
488 for (el_cnt
= 0; el_cnt
< QSELEM__MAX
; el_cnt
++) {
489 if (strcmp(key
, querystring_keys
[el_cnt
].name
) != 0)
492 switch (querystring_keys
[el_cnt
].element
) {
494 for (a_cnt
= 0; a_cnt
< ACTIONS__MAX
; a_cnt
++) {
495 if (strcmp(value
, action_keys
[a_cnt
].name
) != 0)
497 else if (strcmp(value
,
498 action_keys
[a_cnt
].name
) == 0){
500 action_keys
[a_cnt
].action
;
508 (*qs
)->commit
= strdup(value
);
509 if ((*qs
)->commit
== NULL
) {
510 error
= got_error_from_errno2("%s: strdup",
516 (*qs
)->file
= strdup(value
);
517 if ((*qs
)->file
== NULL
) {
518 error
= got_error_from_errno2("%s: strdup",
524 (*qs
)->folder
= strdup(value
);
525 if ((*qs
)->folder
== NULL
) {
526 error
= got_error_from_errno2("%s: strdup",
532 free((*qs
)->headref
);
533 (*qs
)->headref
= strdup(value
);
534 if ((*qs
)->headref
== NULL
) {
535 error
= got_error_from_errno2("%s: strdup",
541 if (strlen(value
) == 0)
543 (*qs
)->index_page
= strtonum(value
, INT64_MIN
,
546 error
= got_error_from_errno3("%s: strtonum %s",
550 if ((*qs
)->index_page
< 0)
551 (*qs
)->index_page
= 0;
554 (*qs
)->path
= strdup(value
);
555 if ((*qs
)->path
== NULL
) {
556 error
= got_error_from_errno2("%s: strdup",
562 if (strlen(value
) == 0)
564 (*qs
)->page
= strtonum(value
, INT64_MIN
,
567 error
= got_error_from_errno3("%s: strtonum %s",
583 gotweb_free_repo_tag(struct repo_tag
*rt
)
588 free(rt
->tag_commit
);
589 free(rt
->commit_msg
);
596 gotweb_free_repo_commit(struct repo_commit
*rc
)
606 free(rc
->commit_msg
);
612 gotweb_free_querystring(struct querystring
*qs
)
625 gotweb_free_repo_dir(struct repo_dir
*repo_dir
)
627 if (repo_dir
!= NULL
) {
628 free(repo_dir
->name
);
629 free(repo_dir
->owner
);
630 free(repo_dir
->description
);
633 free(repo_dir
->path
);
639 gotweb_free_transport(struct transport
*t
)
641 struct repo_commit
*rc
= NULL
, *trc
= NULL
;
642 struct repo_tag
*rt
= NULL
, *trt
= NULL
;
644 TAILQ_FOREACH_SAFE(rc
, &t
->repo_commits
, entry
, trc
) {
645 TAILQ_REMOVE(&t
->repo_commits
, rc
, entry
);
646 gotweb_free_repo_commit(rc
);
648 TAILQ_FOREACH_SAFE(rt
, &t
->repo_tags
, entry
, trt
) {
649 TAILQ_REMOVE(&t
->repo_tags
, rt
, entry
);
650 gotweb_free_repo_tag(rt
);
652 gotweb_free_repo_dir(t
->repo_dir
);
653 gotweb_free_querystring(t
->qs
);
659 const struct got_error
*
660 gotweb_render_content_type(struct request
*c
, const uint8_t *type
)
662 const char *csp
= "default-src 'self'; script-src 'none'; "
663 "object-src 'none';";
666 "Content-Security-Policy: %s\r\n"
667 "Content-Type: %s\r\n\r\n",
672 const struct got_error
*
673 gotweb_render_content_type_file(struct request
*c
, const uint8_t *type
,
676 fcgi_printf(c
, "Content-type: %s\r\n"
677 "Content-disposition: attachment; filename=%s\r\n\r\n",
683 gotweb_get_navs(struct request
*c
, struct gotweb_url
*prev
, int *have_prev
,
684 struct gotweb_url
*next
, int *have_next
)
686 struct transport
*t
= c
->t
;
687 struct querystring
*qs
= t
->qs
;
688 struct server
*srv
= c
->srv
;
690 *have_prev
= *have_next
= 0;
694 if (qs
->index_page
> 0) {
696 *prev
= (struct gotweb_url
){
698 .index_page
= qs
->index_page
- 1,
702 if (t
->next_disp
== srv
->max_repos_display
&&
703 t
->repos_total
!= (qs
->index_page
+ 1) *
704 srv
->max_repos_display
) {
706 *next
= (struct gotweb_url
){
708 .index_page
= qs
->index_page
+ 1,
714 if (t
->prev_id
&& qs
->commit
!= NULL
&&
715 strcmp(qs
->commit
, t
->prev_id
) != 0) {
717 *prev
= (struct gotweb_url
){
720 .page
= qs
->page
- 1,
722 .commit
= t
->prev_id
,
723 .headref
= qs
->headref
,
728 *next
= (struct gotweb_url
){
731 .page
= qs
->page
+ 1,
733 .commit
= t
->next_id
,
734 .headref
= qs
->headref
,
739 if (t
->prev_id
&& qs
->commit
!= NULL
&&
740 strcmp(qs
->commit
, t
->prev_id
) != 0) {
742 *prev
= (struct gotweb_url
){
745 .page
= qs
->page
- 1,
747 .commit
= t
->prev_id
,
748 .headref
= qs
->headref
,
749 .folder
= qs
->folder
,
755 *next
= (struct gotweb_url
){
758 .page
= qs
->page
+ 1,
760 .commit
= t
->next_id
,
761 .headref
= qs
->headref
,
762 .folder
= qs
->folder
,
768 if (t
->prev_id
&& qs
->commit
!= NULL
&&
769 strcmp(qs
->commit
, t
->prev_id
) != 0) {
771 *prev
= (struct gotweb_url
){
774 .page
= qs
->page
- 1,
776 .commit
= t
->prev_id
,
777 .headref
= qs
->headref
,
782 *next
= (struct gotweb_url
){
785 .page
= qs
->page
+ 1,
787 .commit
= t
->next_id
,
788 .headref
= qs
->headref
,
795 static const struct got_error
*
796 gotweb_render_index(struct request
*c
)
798 const struct got_error
*error
= NULL
;
799 struct server
*srv
= c
->srv
;
800 struct transport
*t
= c
->t
;
801 struct querystring
*qs
= t
->qs
;
802 struct repo_dir
*repo_dir
= NULL
;
804 struct dirent
**sd_dent
= NULL
;
805 unsigned int d_cnt
, d_i
, d_disp
= 0;
806 unsigned int d_skipped
= 0;
809 d
= opendir(srv
->repos_path
);
811 error
= got_error_from_errno2("opendir", srv
->repos_path
);
815 d_cnt
= scandir(srv
->repos_path
, &sd_dent
, NULL
, alphasort
);
818 error
= got_error_from_errno2("scandir", srv
->repos_path
);
822 if (gotweb_render_repo_table_hdr(c
->tp
) == -1)
825 for (d_i
= 0; d_i
< d_cnt
; d_i
++) {
826 if (srv
->max_repos
> 0 && t
->prev_disp
== srv
->max_repos
)
829 if (strcmp(sd_dent
[d_i
]->d_name
, ".") == 0 ||
830 strcmp(sd_dent
[d_i
]->d_name
, "..") == 0) {
835 error
= got_path_dirent_type(&type
, srv
->repos_path
,
839 if (type
!= DT_DIR
) {
844 if (qs
->index_page
> 0 && (qs
->index_page
*
845 srv
->max_repos_display
) > t
->prev_disp
) {
850 error
= gotweb_init_repo_dir(&repo_dir
, sd_dent
[d_i
]->d_name
);
854 error
= gotweb_load_got_path(c
, repo_dir
);
855 if (error
&& error
->code
== GOT_ERR_NOT_GIT_REPO
) {
857 gotweb_free_repo_dir(repo_dir
);
862 if (error
&& error
->code
!= GOT_ERR_LONELY_PACKIDX
)
868 if (gotweb_render_repo_fragment(c
->tp
, repo_dir
) == -1)
871 gotweb_free_repo_dir(repo_dir
);
874 if (d_disp
== srv
->max_repos_display
)
877 t
->repos_total
= d_cnt
- d_skipped
;
879 if (srv
->max_repos_display
== 0)
881 if (srv
->max_repos
> 0 && srv
->max_repos
< srv
->max_repos_display
)
883 if (t
->repos_total
<= srv
->max_repos
||
884 t
->repos_total
<= srv
->max_repos_display
)
887 if (gotweb_render_navs(c
->tp
) == -1)
891 for (d_i
= 0; d_i
< d_cnt
; d_i
++)
895 if (d
!= NULL
&& closedir(d
) == EOF
&& error
== NULL
)
896 error
= got_error_from_errno("closedir");
900 static const struct got_error
*
901 gotweb_render_blame(struct request
*c
)
903 const struct got_error
*error
= NULL
;
904 struct transport
*t
= c
->t
;
905 struct repo_commit
*rc
= NULL
;
906 char *age
= NULL
, *msg
= NULL
;
909 error
= got_get_repo_commits(c
, 1);
913 rc
= TAILQ_FIRST(&t
->repo_commits
);
915 error
= gotweb_get_time_str(&age
, rc
->committer_time
, TM_LONG
);
918 error
= gotweb_escape_html(&msg
, rc
->commit_msg
);
922 r
= fcgi_printf(c
, "<div id='blame_title_wrapper'>\n"
923 "<div id='blame_title'>Blame</div>\n"
924 "</div>\n" /* #blame_title_wrapper */
925 "<div id='blame_content'>\n"
926 "<div id='blame_header_wrapper'>\n"
927 "<div id='blame_header'>\n"
928 "<div class='header_age_title'>Date:</div>\n"
929 "<div class='header_age'>%s</div>\n"
930 "<div id='header_commit_msg_title'>Message:</div>\n"
931 "<div id='header_commit_msg'>%s</div>\n"
932 "</div>\n" /* #blame_header */
933 "</div>\n" /* #blame_header_wrapper */
934 "<div class='dotted_line'></div>\n"
935 "<div id='blame'>\n",
941 error
= got_output_file_blame(c
);
945 fcgi_printf(c
, "</div>\n" /* #blame */
946 "</div>\n"); /* #blame_content */
953 static const struct got_error
*
954 gotweb_render_branches(struct request
*c
)
956 const struct got_error
*error
= NULL
;
957 struct got_reflist_head refs
;
958 struct got_reflist_entry
*re
;
959 struct transport
*t
= c
->t
;
960 struct querystring
*qs
= t
->qs
;
961 struct got_repository
*repo
= t
->repo
;
962 char *escaped_refname
= NULL
;
968 error
= got_ref_list(&refs
, repo
, "refs/heads",
969 got_ref_cmp_by_name
, NULL
);
973 r
= fcgi_printf(c
, "<div id='branches_title_wrapper'>\n"
974 "<div id='branches_title'>Branches</div>\n"
975 "</div>\n" /* #branches_title_wrapper */
976 "<div id='branches_content'>\n");
980 TAILQ_FOREACH(re
, &refs
, entry
) {
981 const char *refname
= NULL
;
983 if (got_ref_is_symbolic(re
->ref
))
986 refname
= got_ref_get_name(re
->ref
);
987 if (refname
== NULL
) {
988 error
= got_error_from_errno("strdup");
991 if (strncmp(refname
, "refs/heads/", 11) != 0)
994 error
= got_get_repo_age(&age
, c
, refname
, TM_DIFF
);
998 if (strncmp(refname
, "refs/heads/", 11) == 0)
1000 error
= gotweb_escape_html(&escaped_refname
, refname
);
1004 r
= fcgi_printf(c
, "<div class='branches_wrapper'>\n"
1005 "<div class='branches_age'>%s</div>\n"
1006 "<div class='branches_space'> </div>\n"
1007 "<div class='branch'>", age
);
1011 r
= gotweb_link(c
, &(struct gotweb_url
){
1017 }, "%s", escaped_refname
);
1021 if (fcgi_printf(c
, "</div>\n" /* .branch */
1022 "<div class='navs_wrapper'>\n"
1023 "<div class='navs'>") == -1)
1026 r
= gotweb_link(c
, &(struct gotweb_url
){
1036 if (fcgi_printf(c
, " | ") == -1)
1039 r
= gotweb_link(c
, &(struct gotweb_url
){
1045 }, "commit briefs");
1049 if (fcgi_printf(c
, " | ") == -1)
1052 r
= gotweb_link(c
, &(struct gotweb_url
){
1062 r
= fcgi_printf(c
, "</div>\n" /* .navs */
1063 "</div>\n" /* .navs_wrapper */
1064 "<div class='dotted_line'></div>\n"
1065 "</div>\n"); /* .branches_wrapper */
1071 free(escaped_refname
);
1072 escaped_refname
= NULL
;
1074 fcgi_printf(c
, "</div>\n"); /* #branches_content */
1077 free(escaped_refname
);
1078 got_ref_list_free(&refs
);
1082 static const struct got_error
*
1083 gotweb_render_tree(struct request
*c
)
1085 const struct got_error
*error
= NULL
;
1086 struct transport
*t
= c
->t
;
1087 struct repo_commit
*rc
= NULL
;
1088 char *age
= NULL
, *msg
= NULL
;
1091 error
= got_get_repo_commits(c
, 1);
1095 rc
= TAILQ_FIRST(&t
->repo_commits
);
1097 error
= gotweb_get_time_str(&age
, rc
->committer_time
, TM_LONG
);
1101 error
= gotweb_escape_html(&msg
, rc
->commit_msg
);
1105 r
= fcgi_printf(c
, "<div id='tree_title_wrapper'>\n"
1106 "<div id='tree_title'>Tree</div>\n"
1107 "</div>\n" /* #tree_title_wrapper */
1108 "<div id='tree_content'>\n"
1109 "<div id='tree_header_wrapper'>\n"
1110 "<div id='tree_header'>\n"
1111 "<div id='header_tree_title'>Tree:</div>\n"
1112 "<div id='header_tree'>%s</div>\n"
1113 "<div class='header_age_title'>Date:</div>\n"
1114 "<div class='header_age'>%s</div>\n"
1115 "<div id='header_commit_msg_title'>Message:</div>\n"
1116 "<div id='header_commit_msg'>%s</div>\n"
1117 "</div>\n" /* #tree_header */
1118 "</div>\n" /* #tree_header_wrapper */
1119 "<div class='dotted_line'></div>\n"
1120 "<div id='tree'>\n",
1127 error
= got_output_repo_tree(c
);
1131 fcgi_printf(c
, "</div>\n"); /* #tree */
1132 fcgi_printf(c
, "</div>\n"); /* #tree_content */
1139 static const struct got_error
*
1140 gotweb_render_diff(struct request
*c
)
1142 const struct got_error
*error
= NULL
;
1143 struct transport
*t
= c
->t
;
1144 struct repo_commit
*rc
= NULL
;
1145 char *age
= NULL
, *author
= NULL
, *msg
= NULL
;
1148 error
= got_get_repo_commits(c
, 1);
1152 rc
= TAILQ_FIRST(&t
->repo_commits
);
1154 error
= gotweb_get_time_str(&age
, rc
->committer_time
, TM_LONG
);
1157 error
= gotweb_escape_html(&author
, rc
->author
);
1160 error
= gotweb_escape_html(&msg
, rc
->commit_msg
);
1164 r
= fcgi_printf(c
, "<div id='diff_title_wrapper'>\n"
1165 "<div id='diff_title'>Commit Diff</div>\n"
1166 "</div>\n" /* #diff_title_wrapper */
1167 "<div id='diff_content'>\n"
1168 "<div id='diff_header_wrapper'>\n"
1169 "<div id='diff_header'>\n"
1170 "<div id='header_diff_title'>Diff:</div>\n"
1171 "<div id='header_diff'>%s<br />%s</div>\n"
1172 "<div class='header_commit_title'>Commit:</div>\n"
1173 "<div class='header_commit'>%s</div>\n"
1174 "<div id='header_tree_title'>Tree:</div>\n"
1175 "<div id='header_tree'>%s</div>\n"
1176 "<div class='header_author_title'>Author:</div>\n"
1177 "<div class='header_author'>%s</div>\n"
1178 "<div class='header_age_title'>Date:</div>\n"
1179 "<div class='header_age'>%s</div>\n"
1180 "<div id='header_commit_msg_title'>Message:</div>\n"
1181 "<div id='header_commit_msg'>%s</div>\n"
1182 "</div>\n" /* #diff_header */
1183 "</div>\n" /* #diff_header_wrapper */
1184 "<div class='dotted_line'></div>\n"
1185 "<div id='diff'>\n",
1186 rc
->parent_id
, rc
->commit_id
,
1195 error
= got_output_repo_diff(c
);
1199 fcgi_printf(c
, "</div>\n"); /* #diff */
1200 fcgi_printf(c
, "</div>\n"); /* #diff_content */
1208 static const struct got_error
*
1209 gotweb_render_summary(struct request
*c
)
1211 const struct got_error
*error
= NULL
;
1212 struct transport
*t
= c
->t
;
1213 struct server
*srv
= c
->srv
;
1216 if (fcgi_printf(c
, "<div id='summary_wrapper'>\n") == -1)
1219 if (srv
->show_repo_description
) {
1221 "<div id='description_title'>Description:</div>\n"
1222 "<div id='description'>%s</div>\n",
1223 t
->repo_dir
->description
? t
->repo_dir
->description
: "");
1228 if (srv
->show_repo_owner
) {
1230 "<div id='repo_owner_title'>Owner:</div>\n"
1231 "<div id='repo_owner'>%s</div>\n",
1232 t
->repo_dir
->owner
? t
->repo_dir
->owner
: "");
1237 if (srv
->show_repo_age
) {
1239 "<div id='last_change_title'>Last Change:</div>\n"
1240 "<div id='last_change'>%s</div>\n",
1246 if (srv
->show_repo_cloneurl
) {
1248 "<div id='cloneurl_title'>Clone URL:</div>\n"
1249 "<div id='cloneurl'>%s</div>\n",
1250 t
->repo_dir
->url
? t
->repo_dir
->url
: "");
1255 r
= fcgi_printf(c
, "</div>\n"); /* #summary_wrapper */
1259 if (gotweb_render_briefs(c
->tp
) == -1)
1262 error
= gotweb_render_tags(c
);
1264 log_warnx("%s: %s", __func__
, error
->msg
);
1268 error
= gotweb_render_branches(c
);
1270 log_warnx("%s: %s", __func__
, error
->msg
);
1275 static const struct got_error
*
1276 gotweb_render_tag(struct request
*c
)
1278 const struct got_error
*error
= NULL
;
1279 struct repo_tag
*rt
= NULL
;
1280 struct transport
*t
= c
->t
;
1281 char *tagname
= NULL
, *age
= NULL
, *author
= NULL
, *msg
= NULL
;
1283 error
= got_get_repo_tags(c
, 1);
1287 if (t
->tag_count
== 0) {
1288 error
= got_error_set_errno(GOT_ERR_BAD_OBJ_ID
,
1293 rt
= TAILQ_LAST(&t
->repo_tags
, repo_tags_head
);
1295 error
= gotweb_get_time_str(&age
, rt
->tagger_time
, TM_LONG
);
1298 error
= gotweb_escape_html(&author
, rt
->tagger
);
1301 error
= gotweb_escape_html(&msg
, rt
->commit_msg
);
1305 tagname
= rt
->tag_name
;
1306 if (strncmp(tagname
, "refs/", 5) == 0)
1308 error
= gotweb_escape_html(&tagname
, tagname
);
1312 fcgi_printf(c
, "<div id='tags_title_wrapper'>\n"
1313 "<div id='tags_title'>Tag</div>\n"
1314 "</div>\n" /* #tags_title_wrapper */
1315 "<div id='tags_content'>\n"
1316 "<div id='tag_header_wrapper'>\n"
1317 "<div id='tag_header'>\n"
1318 "<div class='header_commit_title'>Commit:</div>\n"
1319 "<div class='header_commit'>%s"
1320 " <span class='refs_str'>(%s)</span></div>\n"
1321 "<div class='header_author_title'>Tagger:</div>\n"
1322 "<div class='header_author'>%s</div>\n"
1323 "<div class='header_age_title'>Date:</div>\n"
1324 "<div class='header_age'>%s</div>\n"
1325 "<div id='header_commit_msg_title'>Message:</div>\n"
1326 "<div id='header_commit_msg'>%s</div>\n"
1327 "</div>\n" /* #tag_header */
1328 "<div class='dotted_line'></div>\n"
1329 "<div id='tag_commit'>\n%s</div>"
1330 "</div>" /* #tag_header_wrapper */
1331 "</div>", /* #tags_content */
1346 static const struct got_error
*
1347 gotweb_render_tags(struct request
*c
)
1349 const struct got_error
*error
= NULL
;
1350 struct repo_tag
*rt
= NULL
;
1351 struct server
*srv
= c
->srv
;
1352 struct transport
*t
= c
->t
;
1353 struct querystring
*qs
= t
->qs
;
1354 struct repo_dir
*repo_dir
= t
->repo_dir
;
1355 char *age
= NULL
, *tagname
= NULL
, *msg
= NULL
, *newline
;
1356 int r
, commit_found
= 0;
1358 if (qs
->action
== BRIEFS
) {
1360 error
= got_get_repo_tags(c
, D_MAXSLCOMMDISP
);
1362 error
= got_get_repo_tags(c
, srv
->max_commits_display
);
1366 r
= fcgi_printf(c
, "<div id='tags_title_wrapper'>\n"
1367 "<div id='tags_title'>Tags</div>\n"
1368 "</div>\n" /* #tags_title_wrapper */
1369 "<div id='tags_content'>\n");
1373 if (t
->tag_count
== 0) {
1374 r
= fcgi_printf(c
, "<div id='err_content'>%s\n</div>\n",
1375 "This repository contains no tags");
1380 TAILQ_FOREACH(rt
, &t
->repo_tags
, entry
) {
1381 if (commit_found
== 0 && qs
->commit
!= NULL
) {
1382 if (strcmp(qs
->commit
, rt
->commit_id
) != 0)
1387 error
= gotweb_get_time_str(&age
, rt
->tagger_time
, TM_DIFF
);
1391 tagname
= rt
->tag_name
;
1392 if (strncmp(tagname
, "refs/tags/", 10) == 0)
1394 error
= gotweb_escape_html(&tagname
, tagname
);
1398 if (rt
->tag_commit
!= NULL
) {
1399 newline
= strchr(rt
->tag_commit
, '\n');
1402 error
= gotweb_escape_html(&msg
, rt
->tag_commit
);
1407 if (fcgi_printf(c
, "<div class='tag_age'>%s</div>\n"
1408 "<div class='tag'>%s</div>\n"
1409 "<div class='tag_log'>", age
, tagname
) == -1)
1412 r
= gotweb_link(c
, &(struct gotweb_url
){
1416 .path
= repo_dir
->name
,
1417 .commit
= rt
->commit_id
,
1418 }, "%s", msg
? msg
: "");
1422 if (fcgi_printf(c
, "</div>\n" /* .tag_log */
1423 "<div class='navs_wrapper'>\n"
1424 "<div class='navs'>") == -1)
1427 r
= gotweb_link(c
, &(struct gotweb_url
){
1431 .path
= repo_dir
->name
,
1432 .commit
= rt
->commit_id
,
1437 if (fcgi_printf(c
, " | ") == -1)
1440 r
= gotweb_link(c
, &(struct gotweb_url
){
1444 .path
= repo_dir
->name
,
1445 .commit
= rt
->commit_id
,
1446 }, "commit briefs");
1450 if (fcgi_printf(c
, " | ") == -1)
1453 r
= gotweb_link(c
, &(struct gotweb_url
){
1457 .path
= repo_dir
->name
,
1458 .commit
= rt
->commit_id
,
1464 "</div>\n" /* .navs */
1465 "</div>\n" /* .navs_wrapper */
1466 "<div class='dotted_line'></div>\n");
1477 if (t
->next_id
|| t
->prev_id
) {
1478 if (gotweb_render_navs(c
->tp
) == -1)
1481 fcgi_printf(c
, "</div>\n"); /* #tags_content */
1489 const struct got_error
*
1490 gotweb_escape_html(char **escaped_html
, const char *orig_html
)
1492 const struct got_error
*error
= NULL
;
1493 struct escape_pair
{
1504 size_t orig_len
, len
;
1507 orig_len
= strlen(orig_html
);
1509 for (i
= 0; i
< orig_len
; i
++) {
1510 for (j
= 0; j
< nitems(esc
); j
++) {
1511 if (orig_html
[i
] != esc
[j
].c
)
1513 len
+= strlen(esc
[j
].s
) - 1 /* escaped char */;
1517 *escaped_html
= calloc(len
+ 1 /* NUL */, sizeof(**escaped_html
));
1518 if (*escaped_html
== NULL
)
1519 return got_error_from_errno("calloc");
1522 for (i
= 0; i
< orig_len
; i
++) {
1524 for (j
= 0; j
< nitems(esc
); j
++) {
1525 if (orig_html
[i
] != esc
[j
].c
)
1528 if (strlcat(*escaped_html
, esc
[j
].s
, len
+ 1)
1530 error
= got_error(GOT_ERR_NO_SPACE
);
1533 x
+= strlen(esc
[j
].s
);
1538 (*escaped_html
)[x
] = orig_html
[i
];
1544 free(*escaped_html
);
1545 *escaped_html
= NULL
;
1547 (*escaped_html
)[x
] = '\0';
1554 should_urlencode(int c
)
1556 if (c
<= ' ' || c
>= 127)
1587 gotweb_urlencode(const char *str
)
1595 for (s
= str
; *s
; ++s
) {
1597 if (should_urlencode(*s
))
1601 escaped
= calloc(1, len
+ 1);
1602 if (escaped
== NULL
)
1606 for (s
= str
; *s
; ++s
) {
1607 if (should_urlencode(*s
)) {
1608 a
= (*s
& 0xF0) >> 4;
1612 escaped
[i
++] = a
<= 9 ? ('0' + a
) : ('7' + a
);
1613 escaped
[i
++] = b
<= 9 ? ('0' + b
) : ('7' + b
);
1622 gotweb_action_name(int action
)
1655 gotweb_render_url(struct request
*c
, struct gotweb_url
*url
)
1657 const char *sep
= "?", *action
;
1661 action
= gotweb_action_name(url
->action
);
1662 if (action
!= NULL
) {
1663 if (fcgi_printf(c
, "?action=%s", action
) == -1)
1669 if (fcgi_printf(c
, "%scommit=%s", sep
, url
->commit
) == -1)
1675 if (fcgi_printf(c
, "%sprevid=%s", sep
, url
->previd
) == -1)
1681 if (fcgi_printf(c
, "%sprevset=%s", sep
, url
->prevset
) == -1)
1687 tmp
= gotweb_urlencode(url
->file
);
1690 r
= fcgi_printf(c
, "%sfile=%s", sep
, tmp
);
1698 tmp
= gotweb_urlencode(url
->folder
);
1701 r
= fcgi_printf(c
, "%sfolder=%s", sep
, tmp
);
1709 tmp
= gotweb_urlencode(url
->headref
);
1712 r
= fcgi_printf(c
, "%sheadref=%s", sep
, url
->headref
);
1719 if (url
->index_page
!= -1) {
1720 if (fcgi_printf(c
, "%sindex_page=%d", sep
,
1721 url
->index_page
) == -1)
1727 tmp
= gotweb_urlencode(url
->path
);
1730 r
= fcgi_printf(c
, "%spath=%s", sep
, tmp
);
1737 if (url
->page
!= -1) {
1738 if (fcgi_printf(c
, "%spage=%d", sep
, url
->page
) == -1)
1747 gotweb_render_absolute_url(struct request
*c
, struct gotweb_url
*url
)
1749 struct template *tp
= c
->tp
;
1750 const char *proto
= c
->https
? "https" : "http";
1752 if (fcgi_puts(tp
, proto
) == -1 ||
1753 fcgi_puts(tp
, "://") == -1 ||
1754 tp_htmlescape(tp
, c
->server_name
) == -1 ||
1755 tp_htmlescape(tp
, c
->document_uri
) == -1)
1758 return gotweb_render_url(c
, url
);
1762 gotweb_link(struct request
*c
, struct gotweb_url
*url
, const char *fmt
, ...)
1767 if (fcgi_printf(c
, "<a href='") == -1)
1770 if (gotweb_render_url(c
, url
) == -1)
1773 if (fcgi_printf(c
, "'>") == -1)
1777 r
= fcgi_vprintf(c
, fmt
, ap
);
1782 if (fcgi_printf(c
, "</a>"))
1787 static struct got_repository
*
1788 find_cached_repo(struct server
*srv
, const char *path
)
1792 for (i
= 0; i
< srv
->ncached_repos
; i
++) {
1793 if (strcmp(srv
->cached_repos
[i
].path
, path
) == 0)
1794 return srv
->cached_repos
[i
].repo
;
1800 static const struct got_error
*
1801 cache_repo(struct got_repository
**new, struct server
*srv
,
1802 struct repo_dir
*repo_dir
, struct socket
*sock
)
1804 const struct got_error
*error
= NULL
;
1805 struct got_repository
*repo
;
1806 struct cached_repo
*cr
;
1809 if (srv
->ncached_repos
>= GOTWEBD_REPO_CACHESIZE
) {
1810 cr
= &srv
->cached_repos
[srv
->ncached_repos
- 1];
1811 error
= got_repo_close(cr
->repo
);
1812 memset(cr
, 0, sizeof(*cr
));
1813 srv
->ncached_repos
--;
1816 memmove(&srv
->cached_repos
[1], &srv
->cached_repos
[0],
1817 srv
->ncached_repos
* sizeof(srv
->cached_repos
[0]));
1818 cr
= &srv
->cached_repos
[0];
1821 cr
= &srv
->cached_repos
[srv
->ncached_repos
];
1824 error
= got_repo_open(&repo
, repo_dir
->path
, NULL
, sock
->pack_fds
);
1827 memmove(&srv
->cached_repos
[0], &srv
->cached_repos
[1],
1828 srv
->ncached_repos
* sizeof(srv
->cached_repos
[0]));
1833 if (strlcpy(cr
->path
, repo_dir
->path
, sizeof(cr
->path
))
1834 >= sizeof(cr
->path
)) {
1836 memmove(&srv
->cached_repos
[0], &srv
->cached_repos
[1],
1837 srv
->ncached_repos
* sizeof(srv
->cached_repos
[0]));
1839 return got_error(GOT_ERR_NO_SPACE
);
1843 srv
->ncached_repos
++;
1848 static const struct got_error
*
1849 gotweb_load_got_path(struct request
*c
, struct repo_dir
*repo_dir
)
1851 const struct got_error
*error
= NULL
;
1852 struct socket
*sock
= c
->sock
;
1853 struct server
*srv
= c
->srv
;
1854 struct transport
*t
= c
->t
;
1855 struct got_repository
*repo
= NULL
;
1859 if (asprintf(&dir_test
, "%s/%s/%s", srv
->repos_path
, repo_dir
->name
,
1860 GOTWEB_GIT_DIR
) == -1)
1861 return got_error_from_errno("asprintf");
1863 dt
= opendir(dir_test
);
1867 repo_dir
->path
= dir_test
;
1872 if (asprintf(&dir_test
, "%s/%s", srv
->repos_path
,
1873 repo_dir
->name
) == -1)
1874 return got_error_from_errno("asprintf");
1876 dt
= opendir(dir_test
);
1878 error
= got_error_path(repo_dir
->name
, GOT_ERR_NOT_GIT_REPO
);
1881 repo_dir
->path
= dir_test
;
1886 if (srv
->respect_exportok
&&
1887 faccessat(dirfd(dt
), "git-daemon-export-ok", F_OK
, 0) == -1) {
1888 error
= got_error_path(repo_dir
->name
, GOT_ERR_NOT_GIT_REPO
);
1892 repo
= find_cached_repo(srv
, repo_dir
->path
);
1894 error
= cache_repo(&repo
, srv
, repo_dir
, sock
);
1899 error
= gotweb_get_repo_description(&repo_dir
->description
, srv
,
1900 repo_dir
->path
, dirfd(dt
));
1903 error
= got_get_repo_owner(&repo_dir
->owner
, c
);
1906 error
= got_get_repo_age(&repo_dir
->age
, c
, NULL
, TM_DIFF
);
1909 error
= gotweb_get_clone_url(&repo_dir
->url
, srv
, repo_dir
->path
,
1913 if (dt
!= NULL
&& closedir(dt
) == EOF
&& error
== NULL
)
1914 error
= got_error_from_errno("closedir");
1918 static const struct got_error
*
1919 gotweb_init_repo_dir(struct repo_dir
**repo_dir
, const char *dir
)
1921 const struct got_error
*error
;
1923 *repo_dir
= calloc(1, sizeof(**repo_dir
));
1924 if (*repo_dir
== NULL
)
1925 return got_error_from_errno("calloc");
1927 if (asprintf(&(*repo_dir
)->name
, "%s", dir
) == -1) {
1928 error
= got_error_from_errno("asprintf");
1933 (*repo_dir
)->owner
= NULL
;
1934 (*repo_dir
)->description
= NULL
;
1935 (*repo_dir
)->url
= NULL
;
1936 (*repo_dir
)->age
= NULL
;
1937 (*repo_dir
)->path
= NULL
;
1942 static const struct got_error
*
1943 gotweb_get_repo_description(char **description
, struct server
*srv
,
1944 const char *dirpath
, int dir
)
1946 const struct got_error
*error
= NULL
;
1951 *description
= NULL
;
1952 if (srv
->show_repo_description
== 0)
1955 fd
= openat(dir
, "description", O_RDONLY
);
1957 if (errno
!= ENOENT
&& errno
!= EACCES
) {
1958 error
= got_error_from_errno_fmt("openat %s/%s",
1959 dirpath
, "description");
1964 if (fstat(fd
, &sb
) == -1) {
1965 error
= got_error_from_errno_fmt("fstat %s/%s",
1966 dirpath
, "description");
1971 if (len
> GOTWEBD_MAXDESCRSZ
- 1)
1972 len
= GOTWEBD_MAXDESCRSZ
- 1;
1974 *description
= calloc(len
+ 1, sizeof(**description
));
1975 if (*description
== NULL
) {
1976 error
= got_error_from_errno("calloc");
1980 if (read(fd
, *description
, len
) == -1)
1981 error
= got_error_from_errno("read");
1983 if (fd
!= -1 && close(fd
) == -1 && error
== NULL
)
1984 error
= got_error_from_errno("close");
1988 static const struct got_error
*
1989 gotweb_get_clone_url(char **url
, struct server
*srv
, const char *dirpath
,
1992 const struct got_error
*error
= NULL
;
1998 if (srv
->show_repo_cloneurl
== 0)
2001 fd
= openat(dir
, "cloneurl", O_RDONLY
);
2003 if (errno
!= ENOENT
&& errno
!= EACCES
) {
2004 error
= got_error_from_errno_fmt("openat %s/%s",
2005 dirpath
, "cloneurl");
2010 if (fstat(fd
, &sb
) == -1) {
2011 error
= got_error_from_errno_fmt("fstat %s/%s",
2012 dirpath
, "cloneurl");
2017 if (len
> GOTWEBD_MAXCLONEURLSZ
- 1)
2018 len
= GOTWEBD_MAXCLONEURLSZ
- 1;
2020 *url
= calloc(len
+ 1, sizeof(**url
));
2022 error
= got_error_from_errno("calloc");
2026 if (read(fd
, *url
, len
) == -1)
2027 error
= got_error_from_errno("read");
2029 if (fd
!= -1 && close(fd
) == -1 && error
== NULL
)
2030 error
= got_error_from_errno("close");
2034 const struct got_error
*
2035 gotweb_get_time_str(char **repo_age
, time_t committer_time
, int ref_tm
)
2038 long long diff_time
;
2039 const char *years
= "years ago", *months
= "months ago";
2040 const char *weeks
= "weeks ago", *days
= "days ago";
2041 const char *hours
= "hours ago", *minutes
= "minutes ago";
2042 const char *seconds
= "seconds ago", *now
= "right now";
2051 diff_time
= time(NULL
) - committer_time
;
2052 if (diff_time
> 60 * 60 * 24 * 365 * 2) {
2053 if (asprintf(repo_age
, "%lld %s",
2054 (diff_time
/ 60 / 60 / 24 / 365), years
) == -1)
2055 return got_error_from_errno("asprintf");
2056 } else if (diff_time
> 60 * 60 * 24 * (365 / 12) * 2) {
2057 if (asprintf(repo_age
, "%lld %s",
2058 (diff_time
/ 60 / 60 / 24 / (365 / 12)),
2060 return got_error_from_errno("asprintf");
2061 } else if (diff_time
> 60 * 60 * 24 * 7 * 2) {
2062 if (asprintf(repo_age
, "%lld %s",
2063 (diff_time
/ 60 / 60 / 24 / 7), weeks
) == -1)
2064 return got_error_from_errno("asprintf");
2065 } else if (diff_time
> 60 * 60 * 24 * 2) {
2066 if (asprintf(repo_age
, "%lld %s",
2067 (diff_time
/ 60 / 60 / 24), days
) == -1)
2068 return got_error_from_errno("asprintf");
2069 } else if (diff_time
> 60 * 60 * 2) {
2070 if (asprintf(repo_age
, "%lld %s",
2071 (diff_time
/ 60 / 60), hours
) == -1)
2072 return got_error_from_errno("asprintf");
2073 } else if (diff_time
> 60 * 2) {
2074 if (asprintf(repo_age
, "%lld %s", (diff_time
/ 60),
2076 return got_error_from_errno("asprintf");
2077 } else if (diff_time
> 2) {
2078 if (asprintf(repo_age
, "%lld %s", diff_time
,
2080 return got_error_from_errno("asprintf");
2082 if (asprintf(repo_age
, "%s", now
) == -1)
2083 return got_error_from_errno("asprintf");
2087 if (gmtime_r(&committer_time
, &tm
) == NULL
)
2088 return got_error_from_errno("gmtime_r");
2090 s
= asctime_r(&tm
, datebuf
);
2092 return got_error_from_errno("asctime_r");
2094 if (asprintf(repo_age
, "%s UTC", datebuf
) == -1)
2095 return got_error_from_errno("asprintf");
2098 if (gmtime_r(&committer_time
, &tm
) == NULL
)
2099 return got_error_from_errno("gmtime_r");
2101 r
= strftime(datebuf
, sizeof(datebuf
),
2102 "%a, %d %b %Y %H:%M:%S GMT", &tm
);
2104 return got_error(GOT_ERR_NO_SPACE
);
2106 *repo_age
= strdup(datebuf
);
2107 if (*repo_age
== NULL
)
2108 return got_error_from_errno("asprintf");