summaryrefslogtreecommitdiffabout
Unidiff
Diffstat (more/less context) (ignore whitespace changes)
-rw-r--r--Makefile1
-rw-r--r--cgit.c5
-rw-r--r--cgit.css99
-rw-r--r--cgit.h2
-rw-r--r--cgitrc.5.txt4
-rw-r--r--ui-commit.c11
-rw-r--r--ui-diff.c62
-rw-r--r--ui-log.c4
-rw-r--r--ui-refs.c2
-rw-r--r--ui-shared.c34
-rw-r--r--ui-shared.h5
-rw-r--r--ui-ssdiff.c369
-rw-r--r--ui-ssdiff.h13
13 files changed, 586 insertions, 25 deletions
diff --git a/Makefile b/Makefile
index 4e101d3..f8a4d47 100644
--- a/Makefile
+++ b/Makefile
@@ -1,180 +1,181 @@
1CGIT_VERSION = v0.8.3 1CGIT_VERSION = v0.8.3
2CGIT_SCRIPT_NAME = cgit.cgi 2CGIT_SCRIPT_NAME = cgit.cgi
3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit 3CGIT_SCRIPT_PATH = /var/www/htdocs/cgit
4CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH) 4CGIT_DATA_PATH = $(CGIT_SCRIPT_PATH)
5CGIT_CONFIG = /etc/cgitrc 5CGIT_CONFIG = /etc/cgitrc
6CACHE_ROOT = /var/cache/cgit 6CACHE_ROOT = /var/cache/cgit
7SHA1_HEADER = <openssl/sha.h> 7SHA1_HEADER = <openssl/sha.h>
8GIT_VER = 1.6.4.3 8GIT_VER = 1.6.4.3
9GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2 9GIT_URL = http://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.bz2
10INSTALL = install 10INSTALL = install
11 11
12# Define NO_STRCASESTR if you don't have strcasestr. 12# Define NO_STRCASESTR if you don't have strcasestr.
13# 13#
14# Define NO_OPENSSL to disable linking with OpenSSL and use bundled SHA1 14# Define NO_OPENSSL to disable linking with OpenSSL and use bundled SHA1
15# implementation (slower). 15# implementation (slower).
16# 16#
17# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin). 17# Define NEEDS_LIBICONV if linking with libc is not enough (eg. Darwin).
18# 18#
19 19
20#-include config.mak 20#-include config.mak
21 21
22# 22#
23# Platform specific tweaks 23# Platform specific tweaks
24# 24#
25 25
26uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') 26uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
27uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not') 27uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not')
28uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not') 28uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not')
29 29
30ifeq ($(uname_O),Cygwin) 30ifeq ($(uname_O),Cygwin)
31 NO_STRCASESTR = YesPlease 31 NO_STRCASESTR = YesPlease
32 NEEDS_LIBICONV = YesPlease 32 NEEDS_LIBICONV = YesPlease
33endif 33endif
34 34
35# 35#
36# Let the user override the above settings. 36# Let the user override the above settings.
37# 37#
38-include cgit.conf 38-include cgit.conf
39 39
40# 40#
41# Define a way to invoke make in subdirs quietly, shamelessly ripped 41# Define a way to invoke make in subdirs quietly, shamelessly ripped
42# from git.git 42# from git.git
43# 43#
44QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir 44QUIET_SUBDIR0 = +$(MAKE) -C # space to separate -C and subdir
45QUIET_SUBDIR1 = 45QUIET_SUBDIR1 =
46 46
47ifneq ($(findstring $(MAKEFLAGS),w),w) 47ifneq ($(findstring $(MAKEFLAGS),w),w)
48PRINT_DIR = --no-print-directory 48PRINT_DIR = --no-print-directory
49else # "make -w" 49else # "make -w"
50NO_SUBDIR = : 50NO_SUBDIR = :
51endif 51endif
52 52
53ifndef V 53ifndef V
54 QUIET_CC = @echo ' ' CC $@; 54 QUIET_CC = @echo ' ' CC $@;
55 QUIET_MM = @echo ' ' MM $@; 55 QUIET_MM = @echo ' ' MM $@;
56 QUIET_SUBDIR0 = +@subdir= 56 QUIET_SUBDIR0 = +@subdir=
57 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \ 57 QUIET_SUBDIR1 = ;$(NO_SUBDIR) echo ' ' SUBDIR $$subdir; \
58 $(MAKE) $(PRINT_DIR) -C $$subdir 58 $(MAKE) $(PRINT_DIR) -C $$subdir
59endif 59endif
60 60
61# 61#
62# Define a pattern rule for automatic dependency building 62# Define a pattern rule for automatic dependency building
63# 63#
64%.d: %.c 64%.d: %.c
65 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@ 65 $(QUIET_MM)$(CC) $(CFLAGS) -MM $< | sed -e 's/\($*\)\.o:/\1.o $@:/g' >$@
66 66
67# 67#
68# Define a pattern rule for silent object building 68# Define a pattern rule for silent object building
69# 69#
70%.o: %.c 70%.o: %.c
71 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $< 71 $(QUIET_CC)$(CC) -o $*.o -c $(CFLAGS) $<
72 72
73 73
74EXTLIBS = git/libgit.a git/xdiff/lib.a -lz 74EXTLIBS = git/libgit.a git/xdiff/lib.a -lz
75OBJECTS = 75OBJECTS =
76OBJECTS += cache.o 76OBJECTS += cache.o
77OBJECTS += cgit.o 77OBJECTS += cgit.o
78OBJECTS += cmd.o 78OBJECTS += cmd.o
79OBJECTS += configfile.o 79OBJECTS += configfile.o
80OBJECTS += html.o 80OBJECTS += html.o
81OBJECTS += parsing.o 81OBJECTS += parsing.o
82OBJECTS += scan-tree.o 82OBJECTS += scan-tree.o
83OBJECTS += shared.o 83OBJECTS += shared.o
84OBJECTS += ui-atom.o 84OBJECTS += ui-atom.o
85OBJECTS += ui-blob.o 85OBJECTS += ui-blob.o
86OBJECTS += ui-clone.o 86OBJECTS += ui-clone.o
87OBJECTS += ui-commit.o 87OBJECTS += ui-commit.o
88OBJECTS += ui-diff.o 88OBJECTS += ui-diff.o
89OBJECTS += ui-log.o 89OBJECTS += ui-log.o
90OBJECTS += ui-patch.o 90OBJECTS += ui-patch.o
91OBJECTS += ui-plain.o 91OBJECTS += ui-plain.o
92OBJECTS += ui-refs.o 92OBJECTS += ui-refs.o
93OBJECTS += ui-repolist.o 93OBJECTS += ui-repolist.o
94OBJECTS += ui-shared.o 94OBJECTS += ui-shared.o
95OBJECTS += ui-snapshot.o 95OBJECTS += ui-snapshot.o
96OBJECTS += ui-ssdiff.o
96OBJECTS += ui-stats.o 97OBJECTS += ui-stats.o
97OBJECTS += ui-summary.o 98OBJECTS += ui-summary.o
98OBJECTS += ui-tag.o 99OBJECTS += ui-tag.o
99OBJECTS += ui-tree.o 100OBJECTS += ui-tree.o
100 101
101ifdef NEEDS_LIBICONV 102ifdef NEEDS_LIBICONV
102 EXTLIBS += -liconv 103 EXTLIBS += -liconv
103endif 104endif
104 105
105 106
106.PHONY: all libgit test install uninstall clean force-version get-git \ 107.PHONY: all libgit test install uninstall clean force-version get-git \
107 doc man-doc html-doc clean-doc 108 doc man-doc html-doc clean-doc
108 109
109all: cgit 110all: cgit
110 111
111VERSION: force-version 112VERSION: force-version
112 @./gen-version.sh "$(CGIT_VERSION)" 113 @./gen-version.sh "$(CGIT_VERSION)"
113-include VERSION 114-include VERSION
114 115
115 116
116CFLAGS += -g -Wall -Igit 117CFLAGS += -g -Wall -Igit
117CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)' 118CFLAGS += -DSHA1_HEADER='$(SHA1_HEADER)'
118CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' 119CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"'
119CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' 120CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"'
120CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' 121CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"'
121CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' 122CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"'
122 123
123ifdef NO_ICONV 124ifdef NO_ICONV
124 CFLAGS += -DNO_ICONV 125 CFLAGS += -DNO_ICONV
125endif 126endif
126ifdef NO_STRCASESTR 127ifdef NO_STRCASESTR
127 CFLAGS += -DNO_STRCASESTR 128 CFLAGS += -DNO_STRCASESTR
128endif 129endif
129ifdef NO_OPENSSL 130ifdef NO_OPENSSL
130 CFLAGS += -DNO_OPENSSL 131 CFLAGS += -DNO_OPENSSL
131 GIT_OPTIONS += NO_OPENSSL=1 132 GIT_OPTIONS += NO_OPENSSL=1
132else 133else
133 EXTLIBS += -lcrypto 134 EXTLIBS += -lcrypto
134endif 135endif
135 136
136cgit: $(OBJECTS) libgit 137cgit: $(OBJECTS) libgit
137 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS) 138 $(QUIET_CC)$(CC) $(CFLAGS) $(LDFLAGS) -o cgit $(OBJECTS) $(EXTLIBS)
138 139
139cgit.o: VERSION 140cgit.o: VERSION
140 141
141-include $(OBJECTS:.o=.d) 142-include $(OBJECTS:.o=.d)
142 143
143libgit: 144libgit:
144 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 $(GIT_OPTIONS) libgit.a 145 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 $(GIT_OPTIONS) libgit.a
145 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 $(GIT_OPTIONS) xdiff/lib.a 146 $(QUIET_SUBDIR0)git $(QUIET_SUBDIR1) NO_CURL=1 $(GIT_OPTIONS) xdiff/lib.a
146 147
147test: all 148test: all
148 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all 149 $(QUIET_SUBDIR0)tests $(QUIET_SUBDIR1) all
149 150
150install: all 151install: all
151 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH) 152 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_SCRIPT_PATH)
152 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 153 $(INSTALL) -m 0755 cgit $(DESTDIR)$(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
153 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH) 154 $(INSTALL) -m 0755 -d $(DESTDIR)$(CGIT_DATA_PATH)
154 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css 155 $(INSTALL) -m 0644 cgit.css $(DESTDIR)$(CGIT_DATA_PATH)/cgit.css
155 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png 156 $(INSTALL) -m 0644 cgit.png $(DESTDIR)$(CGIT_DATA_PATH)/cgit.png
156 157
157uninstall: 158uninstall:
158 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME) 159 rm -f $(CGIT_SCRIPT_PATH)/$(CGIT_SCRIPT_NAME)
159 rm -f $(CGIT_DATA_PATH)/cgit.css 160 rm -f $(CGIT_DATA_PATH)/cgit.css
160 rm -f $(CGIT_DATA_PATH)/cgit.png 161 rm -f $(CGIT_DATA_PATH)/cgit.png
161 162
162doc: man-doc html-doc pdf-doc 163doc: man-doc html-doc pdf-doc
163 164
164man-doc: cgitrc.5.txt 165man-doc: cgitrc.5.txt
165 a2x -f manpage cgitrc.5.txt 166 a2x -f manpage cgitrc.5.txt
166 167
167html-doc: cgitrc.5.txt 168html-doc: cgitrc.5.txt
168 a2x -f xhtml --stylesheet=cgit-doc.css cgitrc.5.txt 169 a2x -f xhtml --stylesheet=cgit-doc.css cgitrc.5.txt
169 170
170pdf-doc: cgitrc.5.txt 171pdf-doc: cgitrc.5.txt
171 a2x -f pdf cgitrc.5.txt 172 a2x -f pdf cgitrc.5.txt
172 173
173clean: clean-doc 174clean: clean-doc
174 rm -f cgit VERSION *.o *.d 175 rm -f cgit VERSION *.o *.d
175 176
176clean-doc: 177clean-doc:
177 rm -f cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo 178 rm -f cgitrc.5 cgitrc.5.html cgitrc.5.pdf cgitrc.5.xml cgitrc.5.fo
178 179
179get-git: 180get-git:
180 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git 181 curl $(GIT_URL) | tar -xj && rm -rf git && mv git-$(GIT_VER) git
diff --git a/cgit.c b/cgit.c
index 08cb5d2..4f68a4b 100644
--- a/cgit.c
+++ b/cgit.c
@@ -1,723 +1,728 @@
1/* cgit.c: cgi for the git scm 1/* cgit.c: cgi for the git scm
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cache.h" 10#include "cache.h"
11#include "cmd.h" 11#include "cmd.h"
12#include "configfile.h" 12#include "configfile.h"
13#include "html.h" 13#include "html.h"
14#include "ui-shared.h" 14#include "ui-shared.h"
15#include "ui-stats.h" 15#include "ui-stats.h"
16#include "scan-tree.h" 16#include "scan-tree.h"
17 17
18const char *cgit_version = CGIT_VERSION; 18const char *cgit_version = CGIT_VERSION;
19 19
20void add_mimetype(const char *name, const char *value) 20void add_mimetype(const char *name, const char *value)
21{ 21{
22 struct string_list_item *item; 22 struct string_list_item *item;
23 23
24 item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes); 24 item = string_list_insert(xstrdup(name), &ctx.cfg.mimetypes);
25 item->util = xstrdup(value); 25 item->util = xstrdup(value);
26} 26}
27 27
28struct cgit_filter *new_filter(const char *cmd, int extra_args) 28struct cgit_filter *new_filter(const char *cmd, int extra_args)
29{ 29{
30 struct cgit_filter *f; 30 struct cgit_filter *f;
31 31
32 if (!cmd || !cmd[0]) 32 if (!cmd || !cmd[0])
33 return NULL; 33 return NULL;
34 34
35 f = xmalloc(sizeof(struct cgit_filter)); 35 f = xmalloc(sizeof(struct cgit_filter));
36 f->cmd = xstrdup(cmd); 36 f->cmd = xstrdup(cmd);
37 f->argv = xmalloc((2 + extra_args) * sizeof(char *)); 37 f->argv = xmalloc((2 + extra_args) * sizeof(char *));
38 f->argv[0] = f->cmd; 38 f->argv[0] = f->cmd;
39 f->argv[1] = NULL; 39 f->argv[1] = NULL;
40 return f; 40 return f;
41} 41}
42 42
43static void process_cached_repolist(const char *path); 43static void process_cached_repolist(const char *path);
44 44
45void repo_config(struct cgit_repo *repo, const char *name, const char *value) 45void repo_config(struct cgit_repo *repo, const char *name, const char *value)
46{ 46{
47 if (!strcmp(name, "name")) 47 if (!strcmp(name, "name"))
48 repo->name = xstrdup(value); 48 repo->name = xstrdup(value);
49 else if (!strcmp(name, "clone-url")) 49 else if (!strcmp(name, "clone-url"))
50 repo->clone_url = xstrdup(value); 50 repo->clone_url = xstrdup(value);
51 else if (!strcmp(name, "desc")) 51 else if (!strcmp(name, "desc"))
52 repo->desc = xstrdup(value); 52 repo->desc = xstrdup(value);
53 else if (!strcmp(name, "owner")) 53 else if (!strcmp(name, "owner"))
54 repo->owner = xstrdup(value); 54 repo->owner = xstrdup(value);
55 else if (!strcmp(name, "defbranch")) 55 else if (!strcmp(name, "defbranch"))
56 repo->defbranch = xstrdup(value); 56 repo->defbranch = xstrdup(value);
57 else if (!strcmp(name, "snapshots")) 57 else if (!strcmp(name, "snapshots"))
58 repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); 58 repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value);
59 else if (!strcmp(name, "enable-log-filecount")) 59 else if (!strcmp(name, "enable-log-filecount"))
60 repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value); 60 repo->enable_log_filecount = ctx.cfg.enable_log_filecount * atoi(value);
61 else if (!strcmp(name, "enable-log-linecount")) 61 else if (!strcmp(name, "enable-log-linecount"))
62 repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value); 62 repo->enable_log_linecount = ctx.cfg.enable_log_linecount * atoi(value);
63 else if (!strcmp(name, "max-stats")) 63 else if (!strcmp(name, "max-stats"))
64 repo->max_stats = cgit_find_stats_period(value, NULL); 64 repo->max_stats = cgit_find_stats_period(value, NULL);
65 else if (!strcmp(name, "module-link")) 65 else if (!strcmp(name, "module-link"))
66 repo->module_link= xstrdup(value); 66 repo->module_link= xstrdup(value);
67 else if (!strcmp(name, "section")) 67 else if (!strcmp(name, "section"))
68 repo->section = xstrdup(value); 68 repo->section = xstrdup(value);
69 else if (!strcmp(name, "readme") && value != NULL) { 69 else if (!strcmp(name, "readme") && value != NULL) {
70 if (*value == '/') 70 if (*value == '/')
71 repo->readme = xstrdup(value); 71 repo->readme = xstrdup(value);
72 else 72 else
73 repo->readme = xstrdup(fmt("%s/%s", repo->path, value)); 73 repo->readme = xstrdup(fmt("%s/%s", repo->path, value));
74 } else if (ctx.cfg.enable_filter_overrides) { 74 } else if (ctx.cfg.enable_filter_overrides) {
75 if (!strcmp(name, "about-filter")) 75 if (!strcmp(name, "about-filter"))
76 repo->about_filter = new_filter(value, 0); 76 repo->about_filter = new_filter(value, 0);
77 else if (!strcmp(name, "commit-filter")) 77 else if (!strcmp(name, "commit-filter"))
78 repo->commit_filter = new_filter(value, 0); 78 repo->commit_filter = new_filter(value, 0);
79 else if (!strcmp(name, "source-filter")) 79 else if (!strcmp(name, "source-filter"))
80 repo->source_filter = new_filter(value, 1); 80 repo->source_filter = new_filter(value, 1);
81 } 81 }
82} 82}
83 83
84void config_cb(const char *name, const char *value) 84void config_cb(const char *name, const char *value)
85{ 85{
86 if (!strcmp(name, "section") || !strcmp(name, "repo.group")) 86 if (!strcmp(name, "section") || !strcmp(name, "repo.group"))
87 ctx.cfg.section = xstrdup(value); 87 ctx.cfg.section = xstrdup(value);
88 else if (!strcmp(name, "repo.url")) 88 else if (!strcmp(name, "repo.url"))
89 ctx.repo = cgit_add_repo(value); 89 ctx.repo = cgit_add_repo(value);
90 else if (ctx.repo && !strcmp(name, "repo.path")) 90 else if (ctx.repo && !strcmp(name, "repo.path"))
91 ctx.repo->path = trim_end(value, '/'); 91 ctx.repo->path = trim_end(value, '/');
92 else if (ctx.repo && !prefixcmp(name, "repo.")) 92 else if (ctx.repo && !prefixcmp(name, "repo."))
93 repo_config(ctx.repo, name + 5, value); 93 repo_config(ctx.repo, name + 5, value);
94 else if (!strcmp(name, "root-title")) 94 else if (!strcmp(name, "root-title"))
95 ctx.cfg.root_title = xstrdup(value); 95 ctx.cfg.root_title = xstrdup(value);
96 else if (!strcmp(name, "root-desc")) 96 else if (!strcmp(name, "root-desc"))
97 ctx.cfg.root_desc = xstrdup(value); 97 ctx.cfg.root_desc = xstrdup(value);
98 else if (!strcmp(name, "root-readme")) 98 else if (!strcmp(name, "root-readme"))
99 ctx.cfg.root_readme = xstrdup(value); 99 ctx.cfg.root_readme = xstrdup(value);
100 else if (!strcmp(name, "css")) 100 else if (!strcmp(name, "css"))
101 ctx.cfg.css = xstrdup(value); 101 ctx.cfg.css = xstrdup(value);
102 else if (!strcmp(name, "favicon")) 102 else if (!strcmp(name, "favicon"))
103 ctx.cfg.favicon = xstrdup(value); 103 ctx.cfg.favicon = xstrdup(value);
104 else if (!strcmp(name, "footer")) 104 else if (!strcmp(name, "footer"))
105 ctx.cfg.footer = xstrdup(value); 105 ctx.cfg.footer = xstrdup(value);
106 else if (!strcmp(name, "head-include")) 106 else if (!strcmp(name, "head-include"))
107 ctx.cfg.head_include = xstrdup(value); 107 ctx.cfg.head_include = xstrdup(value);
108 else if (!strcmp(name, "header")) 108 else if (!strcmp(name, "header"))
109 ctx.cfg.header = xstrdup(value); 109 ctx.cfg.header = xstrdup(value);
110 else if (!strcmp(name, "logo")) 110 else if (!strcmp(name, "logo"))
111 ctx.cfg.logo = xstrdup(value); 111 ctx.cfg.logo = xstrdup(value);
112 else if (!strcmp(name, "index-header")) 112 else if (!strcmp(name, "index-header"))
113 ctx.cfg.index_header = xstrdup(value); 113 ctx.cfg.index_header = xstrdup(value);
114 else if (!strcmp(name, "index-info")) 114 else if (!strcmp(name, "index-info"))
115 ctx.cfg.index_info = xstrdup(value); 115 ctx.cfg.index_info = xstrdup(value);
116 else if (!strcmp(name, "logo-link")) 116 else if (!strcmp(name, "logo-link"))
117 ctx.cfg.logo_link = xstrdup(value); 117 ctx.cfg.logo_link = xstrdup(value);
118 else if (!strcmp(name, "module-link")) 118 else if (!strcmp(name, "module-link"))
119 ctx.cfg.module_link = xstrdup(value); 119 ctx.cfg.module_link = xstrdup(value);
120 else if (!strcmp(name, "virtual-root")) { 120 else if (!strcmp(name, "virtual-root")) {
121 ctx.cfg.virtual_root = trim_end(value, '/'); 121 ctx.cfg.virtual_root = trim_end(value, '/');
122 if (!ctx.cfg.virtual_root && (!strcmp(value, "/"))) 122 if (!ctx.cfg.virtual_root && (!strcmp(value, "/")))
123 ctx.cfg.virtual_root = ""; 123 ctx.cfg.virtual_root = "";
124 } else if (!strcmp(name, "nocache")) 124 } else if (!strcmp(name, "nocache"))
125 ctx.cfg.nocache = atoi(value); 125 ctx.cfg.nocache = atoi(value);
126 else if (!strcmp(name, "noplainemail")) 126 else if (!strcmp(name, "noplainemail"))
127 ctx.cfg.noplainemail = atoi(value); 127 ctx.cfg.noplainemail = atoi(value);
128 else if (!strcmp(name, "noheader")) 128 else if (!strcmp(name, "noheader"))
129 ctx.cfg.noheader = atoi(value); 129 ctx.cfg.noheader = atoi(value);
130 else if (!strcmp(name, "snapshots")) 130 else if (!strcmp(name, "snapshots"))
131 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); 131 ctx.cfg.snapshots = cgit_parse_snapshots_mask(value);
132 else if (!strcmp(name, "enable-filter-overrides")) 132 else if (!strcmp(name, "enable-filter-overrides"))
133 ctx.cfg.enable_filter_overrides = atoi(value); 133 ctx.cfg.enable_filter_overrides = atoi(value);
134 else if (!strcmp(name, "enable-index-links")) 134 else if (!strcmp(name, "enable-index-links"))
135 ctx.cfg.enable_index_links = atoi(value); 135 ctx.cfg.enable_index_links = atoi(value);
136 else if (!strcmp(name, "enable-log-filecount")) 136 else if (!strcmp(name, "enable-log-filecount"))
137 ctx.cfg.enable_log_filecount = atoi(value); 137 ctx.cfg.enable_log_filecount = atoi(value);
138 else if (!strcmp(name, "enable-log-linecount")) 138 else if (!strcmp(name, "enable-log-linecount"))
139 ctx.cfg.enable_log_linecount = atoi(value); 139 ctx.cfg.enable_log_linecount = atoi(value);
140 else if (!strcmp(name, "enable-tree-linenumbers")) 140 else if (!strcmp(name, "enable-tree-linenumbers"))
141 ctx.cfg.enable_tree_linenumbers = atoi(value); 141 ctx.cfg.enable_tree_linenumbers = atoi(value);
142 else if (!strcmp(name, "max-stats")) 142 else if (!strcmp(name, "max-stats"))
143 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); 143 ctx.cfg.max_stats = cgit_find_stats_period(value, NULL);
144 else if (!strcmp(name, "cache-size")) 144 else if (!strcmp(name, "cache-size"))
145 ctx.cfg.cache_size = atoi(value); 145 ctx.cfg.cache_size = atoi(value);
146 else if (!strcmp(name, "cache-root")) 146 else if (!strcmp(name, "cache-root"))
147 ctx.cfg.cache_root = xstrdup(value); 147 ctx.cfg.cache_root = xstrdup(value);
148 else if (!strcmp(name, "cache-root-ttl")) 148 else if (!strcmp(name, "cache-root-ttl"))
149 ctx.cfg.cache_root_ttl = atoi(value); 149 ctx.cfg.cache_root_ttl = atoi(value);
150 else if (!strcmp(name, "cache-repo-ttl")) 150 else if (!strcmp(name, "cache-repo-ttl"))
151 ctx.cfg.cache_repo_ttl = atoi(value); 151 ctx.cfg.cache_repo_ttl = atoi(value);
152 else if (!strcmp(name, "cache-scanrc-ttl")) 152 else if (!strcmp(name, "cache-scanrc-ttl"))
153 ctx.cfg.cache_scanrc_ttl = atoi(value); 153 ctx.cfg.cache_scanrc_ttl = atoi(value);
154 else if (!strcmp(name, "cache-static-ttl")) 154 else if (!strcmp(name, "cache-static-ttl"))
155 ctx.cfg.cache_static_ttl = atoi(value); 155 ctx.cfg.cache_static_ttl = atoi(value);
156 else if (!strcmp(name, "cache-dynamic-ttl")) 156 else if (!strcmp(name, "cache-dynamic-ttl"))
157 ctx.cfg.cache_dynamic_ttl = atoi(value); 157 ctx.cfg.cache_dynamic_ttl = atoi(value);
158 else if (!strcmp(name, "about-filter")) 158 else if (!strcmp(name, "about-filter"))
159 ctx.cfg.about_filter = new_filter(value, 0); 159 ctx.cfg.about_filter = new_filter(value, 0);
160 else if (!strcmp(name, "commit-filter")) 160 else if (!strcmp(name, "commit-filter"))
161 ctx.cfg.commit_filter = new_filter(value, 0); 161 ctx.cfg.commit_filter = new_filter(value, 0);
162 else if (!strcmp(name, "embedded")) 162 else if (!strcmp(name, "embedded"))
163 ctx.cfg.embedded = atoi(value); 163 ctx.cfg.embedded = atoi(value);
164 else if (!strcmp(name, "max-message-length")) 164 else if (!strcmp(name, "max-message-length"))
165 ctx.cfg.max_msg_len = atoi(value); 165 ctx.cfg.max_msg_len = atoi(value);
166 else if (!strcmp(name, "max-repodesc-length")) 166 else if (!strcmp(name, "max-repodesc-length"))
167 ctx.cfg.max_repodesc_len = atoi(value); 167 ctx.cfg.max_repodesc_len = atoi(value);
168 else if (!strcmp(name, "max-blob-size")) 168 else if (!strcmp(name, "max-blob-size"))
169 ctx.cfg.max_blob_size = atoi(value); 169 ctx.cfg.max_blob_size = atoi(value);
170 else if (!strcmp(name, "max-repo-count")) 170 else if (!strcmp(name, "max-repo-count"))
171 ctx.cfg.max_repo_count = atoi(value); 171 ctx.cfg.max_repo_count = atoi(value);
172 else if (!strcmp(name, "max-commit-count")) 172 else if (!strcmp(name, "max-commit-count"))
173 ctx.cfg.max_commit_count = atoi(value); 173 ctx.cfg.max_commit_count = atoi(value);
174 else if (!strcmp(name, "scan-path")) 174 else if (!strcmp(name, "scan-path"))
175 if (!ctx.cfg.nocache && ctx.cfg.cache_size) 175 if (!ctx.cfg.nocache && ctx.cfg.cache_size)
176 process_cached_repolist(value); 176 process_cached_repolist(value);
177 else 177 else
178 scan_tree(value, repo_config); 178 scan_tree(value, repo_config);
179 else if (!strcmp(name, "source-filter")) 179 else if (!strcmp(name, "source-filter"))
180 ctx.cfg.source_filter = new_filter(value, 1); 180 ctx.cfg.source_filter = new_filter(value, 1);
181 else if (!strcmp(name, "summary-log")) 181 else if (!strcmp(name, "summary-log"))
182 ctx.cfg.summary_log = atoi(value); 182 ctx.cfg.summary_log = atoi(value);
183 else if (!strcmp(name, "summary-branches")) 183 else if (!strcmp(name, "summary-branches"))
184 ctx.cfg.summary_branches = atoi(value); 184 ctx.cfg.summary_branches = atoi(value);
185 else if (!strcmp(name, "summary-tags")) 185 else if (!strcmp(name, "summary-tags"))
186 ctx.cfg.summary_tags = atoi(value); 186 ctx.cfg.summary_tags = atoi(value);
187 else if (!strcmp(name, "side-by-side-diffs"))
188 ctx.cfg.ssdiff = atoi(value);
187 else if (!strcmp(name, "agefile")) 189 else if (!strcmp(name, "agefile"))
188 ctx.cfg.agefile = xstrdup(value); 190 ctx.cfg.agefile = xstrdup(value);
189 else if (!strcmp(name, "renamelimit")) 191 else if (!strcmp(name, "renamelimit"))
190 ctx.cfg.renamelimit = atoi(value); 192 ctx.cfg.renamelimit = atoi(value);
191 else if (!strcmp(name, "robots")) 193 else if (!strcmp(name, "robots"))
192 ctx.cfg.robots = xstrdup(value); 194 ctx.cfg.robots = xstrdup(value);
193 else if (!strcmp(name, "clone-prefix")) 195 else if (!strcmp(name, "clone-prefix"))
194 ctx.cfg.clone_prefix = xstrdup(value); 196 ctx.cfg.clone_prefix = xstrdup(value);
195 else if (!strcmp(name, "local-time")) 197 else if (!strcmp(name, "local-time"))
196 ctx.cfg.local_time = atoi(value); 198 ctx.cfg.local_time = atoi(value);
197 else if (!prefixcmp(name, "mimetype.")) 199 else if (!prefixcmp(name, "mimetype."))
198 add_mimetype(name + 9, value); 200 add_mimetype(name + 9, value);
199 else if (!strcmp(name, "include")) 201 else if (!strcmp(name, "include"))
200 parse_configfile(value, config_cb); 202 parse_configfile(value, config_cb);
201} 203}
202 204
203static void querystring_cb(const char *name, const char *value) 205static void querystring_cb(const char *name, const char *value)
204{ 206{
205 if (!value) 207 if (!value)
206 value = ""; 208 value = "";
207 209
208 if (!strcmp(name,"r")) { 210 if (!strcmp(name,"r")) {
209 ctx.qry.repo = xstrdup(value); 211 ctx.qry.repo = xstrdup(value);
210 ctx.repo = cgit_get_repoinfo(value); 212 ctx.repo = cgit_get_repoinfo(value);
211 } else if (!strcmp(name, "p")) { 213 } else if (!strcmp(name, "p")) {
212 ctx.qry.page = xstrdup(value); 214 ctx.qry.page = xstrdup(value);
213 } else if (!strcmp(name, "url")) { 215 } else if (!strcmp(name, "url")) {
214 if (*value == '/') 216 if (*value == '/')
215 value++; 217 value++;
216 ctx.qry.url = xstrdup(value); 218 ctx.qry.url = xstrdup(value);
217 cgit_parse_url(value); 219 cgit_parse_url(value);
218 } else if (!strcmp(name, "qt")) { 220 } else if (!strcmp(name, "qt")) {
219 ctx.qry.grep = xstrdup(value); 221 ctx.qry.grep = xstrdup(value);
220 } else if (!strcmp(name, "q")) { 222 } else if (!strcmp(name, "q")) {
221 ctx.qry.search = xstrdup(value); 223 ctx.qry.search = xstrdup(value);
222 } else if (!strcmp(name, "h")) { 224 } else if (!strcmp(name, "h")) {
223 ctx.qry.head = xstrdup(value); 225 ctx.qry.head = xstrdup(value);
224 ctx.qry.has_symref = 1; 226 ctx.qry.has_symref = 1;
225 } else if (!strcmp(name, "id")) { 227 } else if (!strcmp(name, "id")) {
226 ctx.qry.sha1 = xstrdup(value); 228 ctx.qry.sha1 = xstrdup(value);
227 ctx.qry.has_sha1 = 1; 229 ctx.qry.has_sha1 = 1;
228 } else if (!strcmp(name, "id2")) { 230 } else if (!strcmp(name, "id2")) {
229 ctx.qry.sha2 = xstrdup(value); 231 ctx.qry.sha2 = xstrdup(value);
230 ctx.qry.has_sha1 = 1; 232 ctx.qry.has_sha1 = 1;
231 } else if (!strcmp(name, "ofs")) { 233 } else if (!strcmp(name, "ofs")) {
232 ctx.qry.ofs = atoi(value); 234 ctx.qry.ofs = atoi(value);
233 } else if (!strcmp(name, "path")) { 235 } else if (!strcmp(name, "path")) {
234 ctx.qry.path = trim_end(value, '/'); 236 ctx.qry.path = trim_end(value, '/');
235 } else if (!strcmp(name, "name")) { 237 } else if (!strcmp(name, "name")) {
236 ctx.qry.name = xstrdup(value); 238 ctx.qry.name = xstrdup(value);
237 } else if (!strcmp(name, "mimetype")) { 239 } else if (!strcmp(name, "mimetype")) {
238 ctx.qry.mimetype = xstrdup(value); 240 ctx.qry.mimetype = xstrdup(value);
239 } else if (!strcmp(name, "s")){ 241 } else if (!strcmp(name, "s")){
240 ctx.qry.sort = xstrdup(value); 242 ctx.qry.sort = xstrdup(value);
241 } else if (!strcmp(name, "showmsg")) { 243 } else if (!strcmp(name, "showmsg")) {
242 ctx.qry.showmsg = atoi(value); 244 ctx.qry.showmsg = atoi(value);
243 } else if (!strcmp(name, "period")) { 245 } else if (!strcmp(name, "period")) {
244 ctx.qry.period = xstrdup(value); 246 ctx.qry.period = xstrdup(value);
247 } else if (!strcmp(name, "ss")) {
248 ctx.qry.ssdiff = atoi(value);
245 } 249 }
246} 250}
247 251
248char *xstrdupn(const char *str) 252char *xstrdupn(const char *str)
249{ 253{
250 return (str ? xstrdup(str) : NULL); 254 return (str ? xstrdup(str) : NULL);
251} 255}
252 256
253static void prepare_context(struct cgit_context *ctx) 257static void prepare_context(struct cgit_context *ctx)
254{ 258{
255 memset(ctx, 0, sizeof(ctx)); 259 memset(ctx, 0, sizeof(ctx));
256 ctx->cfg.agefile = "info/web/last-modified"; 260 ctx->cfg.agefile = "info/web/last-modified";
257 ctx->cfg.nocache = 0; 261 ctx->cfg.nocache = 0;
258 ctx->cfg.cache_size = 0; 262 ctx->cfg.cache_size = 0;
259 ctx->cfg.cache_dynamic_ttl = 5; 263 ctx->cfg.cache_dynamic_ttl = 5;
260 ctx->cfg.cache_max_create_time = 5; 264 ctx->cfg.cache_max_create_time = 5;
261 ctx->cfg.cache_repo_ttl = 5; 265 ctx->cfg.cache_repo_ttl = 5;
262 ctx->cfg.cache_root = CGIT_CACHE_ROOT; 266 ctx->cfg.cache_root = CGIT_CACHE_ROOT;
263 ctx->cfg.cache_root_ttl = 5; 267 ctx->cfg.cache_root_ttl = 5;
264 ctx->cfg.cache_scanrc_ttl = 15; 268 ctx->cfg.cache_scanrc_ttl = 15;
265 ctx->cfg.cache_static_ttl = -1; 269 ctx->cfg.cache_static_ttl = -1;
266 ctx->cfg.css = "/cgit.css"; 270 ctx->cfg.css = "/cgit.css";
267 ctx->cfg.logo = "/cgit.png"; 271 ctx->cfg.logo = "/cgit.png";
268 ctx->cfg.local_time = 0; 272 ctx->cfg.local_time = 0;
269 ctx->cfg.enable_tree_linenumbers = 1; 273 ctx->cfg.enable_tree_linenumbers = 1;
270 ctx->cfg.max_repo_count = 50; 274 ctx->cfg.max_repo_count = 50;
271 ctx->cfg.max_commit_count = 50; 275 ctx->cfg.max_commit_count = 50;
272 ctx->cfg.max_lock_attempts = 5; 276 ctx->cfg.max_lock_attempts = 5;
273 ctx->cfg.max_msg_len = 80; 277 ctx->cfg.max_msg_len = 80;
274 ctx->cfg.max_repodesc_len = 80; 278 ctx->cfg.max_repodesc_len = 80;
275 ctx->cfg.max_blob_size = 0; 279 ctx->cfg.max_blob_size = 0;
276 ctx->cfg.max_stats = 0; 280 ctx->cfg.max_stats = 0;
277 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s"; 281 ctx->cfg.module_link = "./?repo=%s&page=commit&id=%s";
278 ctx->cfg.renamelimit = -1; 282 ctx->cfg.renamelimit = -1;
279 ctx->cfg.robots = "index, nofollow"; 283 ctx->cfg.robots = "index, nofollow";
280 ctx->cfg.root_title = "Git repository browser"; 284 ctx->cfg.root_title = "Git repository browser";
281 ctx->cfg.root_desc = "a fast webinterface for the git dscm"; 285 ctx->cfg.root_desc = "a fast webinterface for the git dscm";
282 ctx->cfg.script_name = CGIT_SCRIPT_NAME; 286 ctx->cfg.script_name = CGIT_SCRIPT_NAME;
283 ctx->cfg.section = ""; 287 ctx->cfg.section = "";
284 ctx->cfg.summary_branches = 10; 288 ctx->cfg.summary_branches = 10;
285 ctx->cfg.summary_log = 10; 289 ctx->cfg.summary_log = 10;
286 ctx->cfg.summary_tags = 10; 290 ctx->cfg.summary_tags = 10;
291 ctx->cfg.ssdiff = 0;
287 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG")); 292 ctx->env.cgit_config = xstrdupn(getenv("CGIT_CONFIG"));
288 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST")); 293 ctx->env.http_host = xstrdupn(getenv("HTTP_HOST"));
289 ctx->env.https = xstrdupn(getenv("HTTPS")); 294 ctx->env.https = xstrdupn(getenv("HTTPS"));
290 ctx->env.no_http = xstrdupn(getenv("NO_HTTP")); 295 ctx->env.no_http = xstrdupn(getenv("NO_HTTP"));
291 ctx->env.path_info = xstrdupn(getenv("PATH_INFO")); 296 ctx->env.path_info = xstrdupn(getenv("PATH_INFO"));
292 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING")); 297 ctx->env.query_string = xstrdupn(getenv("QUERY_STRING"));
293 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD")); 298 ctx->env.request_method = xstrdupn(getenv("REQUEST_METHOD"));
294 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME")); 299 ctx->env.script_name = xstrdupn(getenv("SCRIPT_NAME"));
295 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME")); 300 ctx->env.server_name = xstrdupn(getenv("SERVER_NAME"));
296 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT")); 301 ctx->env.server_port = xstrdupn(getenv("SERVER_PORT"));
297 ctx->page.mimetype = "text/html"; 302 ctx->page.mimetype = "text/html";
298 ctx->page.charset = PAGE_ENCODING; 303 ctx->page.charset = PAGE_ENCODING;
299 ctx->page.filename = NULL; 304 ctx->page.filename = NULL;
300 ctx->page.size = 0; 305 ctx->page.size = 0;
301 ctx->page.modified = time(NULL); 306 ctx->page.modified = time(NULL);
302 ctx->page.expires = ctx->page.modified; 307 ctx->page.expires = ctx->page.modified;
303 ctx->page.etag = NULL; 308 ctx->page.etag = NULL;
304 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list)); 309 memset(&ctx->cfg.mimetypes, 0, sizeof(struct string_list));
305 if (ctx->env.script_name) 310 if (ctx->env.script_name)
306 ctx->cfg.script_name = ctx->env.script_name; 311 ctx->cfg.script_name = ctx->env.script_name;
307 if (ctx->env.query_string) 312 if (ctx->env.query_string)
308 ctx->qry.raw = ctx->env.query_string; 313 ctx->qry.raw = ctx->env.query_string;
309 if (!ctx->env.cgit_config) 314 if (!ctx->env.cgit_config)
310 ctx->env.cgit_config = CGIT_CONFIG; 315 ctx->env.cgit_config = CGIT_CONFIG;
311} 316}
312 317
313struct refmatch { 318struct refmatch {
314 char *req_ref; 319 char *req_ref;
315 char *first_ref; 320 char *first_ref;
316 int match; 321 int match;
317}; 322};
318 323
319int find_current_ref(const char *refname, const unsigned char *sha1, 324int find_current_ref(const char *refname, const unsigned char *sha1,
320 int flags, void *cb_data) 325 int flags, void *cb_data)
321{ 326{
322 struct refmatch *info; 327 struct refmatch *info;
323 328
324 info = (struct refmatch *)cb_data; 329 info = (struct refmatch *)cb_data;
325 if (!strcmp(refname, info->req_ref)) 330 if (!strcmp(refname, info->req_ref))
326 info->match = 1; 331 info->match = 1;
327 if (!info->first_ref) 332 if (!info->first_ref)
328 info->first_ref = xstrdup(refname); 333 info->first_ref = xstrdup(refname);
329 return info->match; 334 return info->match;
330} 335}
331 336
332char *find_default_branch(struct cgit_repo *repo) 337char *find_default_branch(struct cgit_repo *repo)
333{ 338{
334 struct refmatch info; 339 struct refmatch info;
335 char *ref; 340 char *ref;
336 341
337 info.req_ref = repo->defbranch; 342 info.req_ref = repo->defbranch;
338 info.first_ref = NULL; 343 info.first_ref = NULL;
339 info.match = 0; 344 info.match = 0;
340 for_each_branch_ref(find_current_ref, &info); 345 for_each_branch_ref(find_current_ref, &info);
341 if (info.match) 346 if (info.match)
342 ref = info.req_ref; 347 ref = info.req_ref;
343 else 348 else
344 ref = info.first_ref; 349 ref = info.first_ref;
345 if (ref) 350 if (ref)
346 ref = xstrdup(ref); 351 ref = xstrdup(ref);
347 return ref; 352 return ref;
348} 353}
349 354
350static int prepare_repo_cmd(struct cgit_context *ctx) 355static int prepare_repo_cmd(struct cgit_context *ctx)
351{ 356{
352 char *tmp; 357 char *tmp;
353 unsigned char sha1[20]; 358 unsigned char sha1[20];
354 int nongit = 0; 359 int nongit = 0;
355 360
356 setenv("GIT_DIR", ctx->repo->path, 1); 361 setenv("GIT_DIR", ctx->repo->path, 1);
357 setup_git_directory_gently(&nongit); 362 setup_git_directory_gently(&nongit);
358 if (nongit) { 363 if (nongit) {
359 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title, 364 ctx->page.title = fmt("%s - %s", ctx->cfg.root_title,
360 "config error"); 365 "config error");
361 tmp = fmt("Not a git repository: '%s'", ctx->repo->path); 366 tmp = fmt("Not a git repository: '%s'", ctx->repo->path);
362 ctx->repo = NULL; 367 ctx->repo = NULL;
363 cgit_print_http_headers(ctx); 368 cgit_print_http_headers(ctx);
364 cgit_print_docstart(ctx); 369 cgit_print_docstart(ctx);
365 cgit_print_pageheader(ctx); 370 cgit_print_pageheader(ctx);
366 cgit_print_error(tmp); 371 cgit_print_error(tmp);
367 cgit_print_docend(); 372 cgit_print_docend();
368 return 1; 373 return 1;
369 } 374 }
370 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc); 375 ctx->page.title = fmt("%s - %s", ctx->repo->name, ctx->repo->desc);
371 376
372 if (!ctx->qry.head) { 377 if (!ctx->qry.head) {
373 ctx->qry.nohead = 1; 378 ctx->qry.nohead = 1;
374 ctx->qry.head = find_default_branch(ctx->repo); 379 ctx->qry.head = find_default_branch(ctx->repo);
375 ctx->repo->defbranch = ctx->qry.head; 380 ctx->repo->defbranch = ctx->qry.head;
376 } 381 }
377 382
378 if (!ctx->qry.head) { 383 if (!ctx->qry.head) {
379 cgit_print_http_headers(ctx); 384 cgit_print_http_headers(ctx);
380 cgit_print_docstart(ctx); 385 cgit_print_docstart(ctx);
381 cgit_print_pageheader(ctx); 386 cgit_print_pageheader(ctx);
382 cgit_print_error("Repository seems to be empty"); 387 cgit_print_error("Repository seems to be empty");
383 cgit_print_docend(); 388 cgit_print_docend();
384 return 1; 389 return 1;
385 } 390 }
386 391
387 if (get_sha1(ctx->qry.head, sha1)) { 392 if (get_sha1(ctx->qry.head, sha1)) {
388 tmp = xstrdup(ctx->qry.head); 393 tmp = xstrdup(ctx->qry.head);
389 ctx->qry.head = ctx->repo->defbranch; 394 ctx->qry.head = ctx->repo->defbranch;
390 ctx->page.status = 404; 395 ctx->page.status = 404;
391 ctx->page.statusmsg = "not found"; 396 ctx->page.statusmsg = "not found";
392 cgit_print_http_headers(ctx); 397 cgit_print_http_headers(ctx);
393 cgit_print_docstart(ctx); 398 cgit_print_docstart(ctx);
394 cgit_print_pageheader(ctx); 399 cgit_print_pageheader(ctx);
395 cgit_print_error(fmt("Invalid branch: %s", tmp)); 400 cgit_print_error(fmt("Invalid branch: %s", tmp));
396 cgit_print_docend(); 401 cgit_print_docend();
397 return 1; 402 return 1;
398 } 403 }
399 return 0; 404 return 0;
400} 405}
401 406
402static void process_request(void *cbdata) 407static void process_request(void *cbdata)
403{ 408{
404 struct cgit_context *ctx = cbdata; 409 struct cgit_context *ctx = cbdata;
405 struct cgit_cmd *cmd; 410 struct cgit_cmd *cmd;
406 411
407 cmd = cgit_get_cmd(ctx); 412 cmd = cgit_get_cmd(ctx);
408 if (!cmd) { 413 if (!cmd) {
409 ctx->page.title = "cgit error"; 414 ctx->page.title = "cgit error";
410 cgit_print_http_headers(ctx); 415 cgit_print_http_headers(ctx);
411 cgit_print_docstart(ctx); 416 cgit_print_docstart(ctx);
412 cgit_print_pageheader(ctx); 417 cgit_print_pageheader(ctx);
413 cgit_print_error("Invalid request"); 418 cgit_print_error("Invalid request");
414 cgit_print_docend(); 419 cgit_print_docend();
415 return; 420 return;
416 } 421 }
417 422
418 if (cmd->want_repo && !ctx->repo) { 423 if (cmd->want_repo && !ctx->repo) {
419 cgit_print_http_headers(ctx); 424 cgit_print_http_headers(ctx);
420 cgit_print_docstart(ctx); 425 cgit_print_docstart(ctx);
421 cgit_print_pageheader(ctx); 426 cgit_print_pageheader(ctx);
422 cgit_print_error(fmt("No repository selected")); 427 cgit_print_error(fmt("No repository selected"));
423 cgit_print_docend(); 428 cgit_print_docend();
424 return; 429 return;
425 } 430 }
426 431
427 if (ctx->repo && prepare_repo_cmd(ctx)) 432 if (ctx->repo && prepare_repo_cmd(ctx))
428 return; 433 return;
429 434
430 if (cmd->want_layout) { 435 if (cmd->want_layout) {
431 cgit_print_http_headers(ctx); 436 cgit_print_http_headers(ctx);
432 cgit_print_docstart(ctx); 437 cgit_print_docstart(ctx);
433 cgit_print_pageheader(ctx); 438 cgit_print_pageheader(ctx);
434 } 439 }
435 440
436 cmd->fn(ctx); 441 cmd->fn(ctx);
437 442
438 if (cmd->want_layout) 443 if (cmd->want_layout)
439 cgit_print_docend(); 444 cgit_print_docend();
440} 445}
441 446
442int cmp_repos(const void *a, const void *b) 447int cmp_repos(const void *a, const void *b)
443{ 448{
444 const struct cgit_repo *ra = a, *rb = b; 449 const struct cgit_repo *ra = a, *rb = b;
445 return strcmp(ra->url, rb->url); 450 return strcmp(ra->url, rb->url);
446} 451}
447 452
448char *build_snapshot_setting(int bitmap) 453char *build_snapshot_setting(int bitmap)
449{ 454{
450 const struct cgit_snapshot_format *f; 455 const struct cgit_snapshot_format *f;
451 char *result = xstrdup(""); 456 char *result = xstrdup("");
452 char *tmp; 457 char *tmp;
453 int len; 458 int len;
454 459
455 for (f = cgit_snapshot_formats; f->suffix; f++) { 460 for (f = cgit_snapshot_formats; f->suffix; f++) {
456 if (f->bit & bitmap) { 461 if (f->bit & bitmap) {
457 tmp = result; 462 tmp = result;
458 result = xstrdup(fmt("%s%s ", tmp, f->suffix)); 463 result = xstrdup(fmt("%s%s ", tmp, f->suffix));
459 free(tmp); 464 free(tmp);
460 } 465 }
461 } 466 }
462 len = strlen(result); 467 len = strlen(result);
463 if (len) 468 if (len)
464 result[len - 1] = '\0'; 469 result[len - 1] = '\0';
465 return result; 470 return result;
466} 471}
467 472
468char *get_first_line(char *txt) 473char *get_first_line(char *txt)
469{ 474{
470 char *t = xstrdup(txt); 475 char *t = xstrdup(txt);
471 char *p = strchr(t, '\n'); 476 char *p = strchr(t, '\n');
472 if (p) 477 if (p)
473 *p = '\0'; 478 *p = '\0';
474 return t; 479 return t;
475} 480}
476 481
477void print_repo(FILE *f, struct cgit_repo *repo) 482void print_repo(FILE *f, struct cgit_repo *repo)
478{ 483{
479 fprintf(f, "repo.url=%s\n", repo->url); 484 fprintf(f, "repo.url=%s\n", repo->url);
480 fprintf(f, "repo.name=%s\n", repo->name); 485 fprintf(f, "repo.name=%s\n", repo->name);
481 fprintf(f, "repo.path=%s\n", repo->path); 486 fprintf(f, "repo.path=%s\n", repo->path);
482 if (repo->owner) 487 if (repo->owner)
483 fprintf(f, "repo.owner=%s\n", repo->owner); 488 fprintf(f, "repo.owner=%s\n", repo->owner);
484 if (repo->desc) { 489 if (repo->desc) {
485 char *tmp = get_first_line(repo->desc); 490 char *tmp = get_first_line(repo->desc);
486 fprintf(f, "repo.desc=%s\n", tmp); 491 fprintf(f, "repo.desc=%s\n", tmp);
487 free(tmp); 492 free(tmp);
488 } 493 }
489 if (repo->readme) 494 if (repo->readme)
490 fprintf(f, "repo.readme=%s\n", repo->readme); 495 fprintf(f, "repo.readme=%s\n", repo->readme);
491 if (repo->defbranch) 496 if (repo->defbranch)
492 fprintf(f, "repo.defbranch=%s\n", repo->defbranch); 497 fprintf(f, "repo.defbranch=%s\n", repo->defbranch);
493 if (repo->module_link) 498 if (repo->module_link)
494 fprintf(f, "repo.module-link=%s\n", repo->module_link); 499 fprintf(f, "repo.module-link=%s\n", repo->module_link);
495 if (repo->section) 500 if (repo->section)
496 fprintf(f, "repo.section=%s\n", repo->section); 501 fprintf(f, "repo.section=%s\n", repo->section);
497 if (repo->clone_url) 502 if (repo->clone_url)
498 fprintf(f, "repo.clone-url=%s\n", repo->clone_url); 503 fprintf(f, "repo.clone-url=%s\n", repo->clone_url);
499 fprintf(f, "repo.enable-log-filecount=%d\n", 504 fprintf(f, "repo.enable-log-filecount=%d\n",
500 repo->enable_log_filecount); 505 repo->enable_log_filecount);
501 fprintf(f, "repo.enable-log-linecount=%d\n", 506 fprintf(f, "repo.enable-log-linecount=%d\n",
502 repo->enable_log_linecount); 507 repo->enable_log_linecount);
503 if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) 508 if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter)
504 fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd); 509 fprintf(f, "repo.about-filter=%s\n", repo->about_filter->cmd);
505 if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) 510 if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter)
506 fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd); 511 fprintf(f, "repo.commit-filter=%s\n", repo->commit_filter->cmd);
507 if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter) 512 if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter)
508 fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd); 513 fprintf(f, "repo.source-filter=%s\n", repo->source_filter->cmd);
509 if (repo->snapshots != ctx.cfg.snapshots) { 514 if (repo->snapshots != ctx.cfg.snapshots) {
510 char *tmp = build_snapshot_setting(repo->snapshots); 515 char *tmp = build_snapshot_setting(repo->snapshots);
511 fprintf(f, "repo.snapshots=%s\n", tmp); 516 fprintf(f, "repo.snapshots=%s\n", tmp);
512 free(tmp); 517 free(tmp);
513 } 518 }
514 if (repo->max_stats != ctx.cfg.max_stats) 519 if (repo->max_stats != ctx.cfg.max_stats)
515 fprintf(f, "repo.max-stats=%s\n", 520 fprintf(f, "repo.max-stats=%s\n",
516 cgit_find_stats_periodname(repo->max_stats)); 521 cgit_find_stats_periodname(repo->max_stats));
517 fprintf(f, "\n"); 522 fprintf(f, "\n");
518} 523}
519 524
520void print_repolist(FILE *f, struct cgit_repolist *list, int start) 525void print_repolist(FILE *f, struct cgit_repolist *list, int start)
521{ 526{
522 int i; 527 int i;
523 528
524 for(i = start; i < list->count; i++) 529 for(i = start; i < list->count; i++)
525 print_repo(f, &list->repos[i]); 530 print_repo(f, &list->repos[i]);
526} 531}
527 532
528/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc' 533/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc'
529 * and return 0 on success. 534 * and return 0 on success.
530 */ 535 */
531static int generate_cached_repolist(const char *path, const char *cached_rc) 536static int generate_cached_repolist(const char *path, const char *cached_rc)
532{ 537{
533 char *locked_rc; 538 char *locked_rc;
534 int idx; 539 int idx;
535 FILE *f; 540 FILE *f;
536 541
537 locked_rc = xstrdup(fmt("%s.lock", cached_rc)); 542 locked_rc = xstrdup(fmt("%s.lock", cached_rc));
538 f = fopen(locked_rc, "wx"); 543 f = fopen(locked_rc, "wx");
539 if (!f) { 544 if (!f) {
540 /* Inform about the error unless the lockfile already existed, 545 /* Inform about the error unless the lockfile already existed,
541 * since that only means we've got concurrent requests. 546 * since that only means we've got concurrent requests.
542 */ 547 */
543 if (errno != EEXIST) 548 if (errno != EEXIST)
544 fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n", 549 fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n",
545 locked_rc, strerror(errno), errno); 550 locked_rc, strerror(errno), errno);
546 return errno; 551 return errno;
547 } 552 }
548 idx = cgit_repolist.count; 553 idx = cgit_repolist.count;
549 scan_tree(path, repo_config); 554 scan_tree(path, repo_config);
550 print_repolist(f, &cgit_repolist, idx); 555 print_repolist(f, &cgit_repolist, idx);
551 if (rename(locked_rc, cached_rc)) 556 if (rename(locked_rc, cached_rc))
552 fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n", 557 fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n",
553 locked_rc, cached_rc, strerror(errno), errno); 558 locked_rc, cached_rc, strerror(errno), errno);
554 fclose(f); 559 fclose(f);
555 return 0; 560 return 0;
556} 561}
557 562
558static void process_cached_repolist(const char *path) 563static void process_cached_repolist(const char *path)
559{ 564{
560 struct stat st; 565 struct stat st;
561 char *cached_rc; 566 char *cached_rc;
562 time_t age; 567 time_t age;
563 568
564 cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root, 569 cached_rc = xstrdup(fmt("%s/rc-%8x", ctx.cfg.cache_root,
565 hash_str(path))); 570 hash_str(path)));
566 571
567 if (stat(cached_rc, &st)) { 572 if (stat(cached_rc, &st)) {
568 /* Nothing is cached, we need to scan without forking. And 573 /* Nothing is cached, we need to scan without forking. And
569 * if we fail to generate a cached repolist, we need to 574 * if we fail to generate a cached repolist, we need to
570 * invoke scan_tree manually. 575 * invoke scan_tree manually.
571 */ 576 */
572 if (generate_cached_repolist(path, cached_rc)) 577 if (generate_cached_repolist(path, cached_rc))
573 scan_tree(path, repo_config); 578 scan_tree(path, repo_config);
574 return; 579 return;
575 } 580 }
576 581
577 parse_configfile(cached_rc, config_cb); 582 parse_configfile(cached_rc, config_cb);
578 583
579 /* If the cached configfile hasn't expired, lets exit now */ 584 /* If the cached configfile hasn't expired, lets exit now */
580 age = time(NULL) - st.st_mtime; 585 age = time(NULL) - st.st_mtime;
581 if (age <= (ctx.cfg.cache_scanrc_ttl * 60)) 586 if (age <= (ctx.cfg.cache_scanrc_ttl * 60))
582 return; 587 return;
583 588
584 /* The cached repolist has been parsed, but it was old. So lets 589 /* The cached repolist has been parsed, but it was old. So lets
585 * rescan the specified path and generate a new cached repolist 590 * rescan the specified path and generate a new cached repolist
586 * in a child-process to avoid latency for the current request. 591 * in a child-process to avoid latency for the current request.
587 */ 592 */
588 if (fork()) 593 if (fork())
589 return; 594 return;
590 595
591 exit(generate_cached_repolist(path, cached_rc)); 596 exit(generate_cached_repolist(path, cached_rc));
592} 597}
593 598
594static void cgit_parse_args(int argc, const char **argv) 599static void cgit_parse_args(int argc, const char **argv)
595{ 600{
596 int i; 601 int i;
597 int scan = 0; 602 int scan = 0;
598 603
599 for (i = 1; i < argc; i++) { 604 for (i = 1; i < argc; i++) {
600 if (!strncmp(argv[i], "--cache=", 8)) { 605 if (!strncmp(argv[i], "--cache=", 8)) {
601 ctx.cfg.cache_root = xstrdup(argv[i]+8); 606 ctx.cfg.cache_root = xstrdup(argv[i]+8);
602 } 607 }
603 if (!strcmp(argv[i], "--nocache")) { 608 if (!strcmp(argv[i], "--nocache")) {
604 ctx.cfg.nocache = 1; 609 ctx.cfg.nocache = 1;
605 } 610 }
606 if (!strcmp(argv[i], "--nohttp")) { 611 if (!strcmp(argv[i], "--nohttp")) {
607 ctx.env.no_http = "1"; 612 ctx.env.no_http = "1";
608 } 613 }
609 if (!strncmp(argv[i], "--query=", 8)) { 614 if (!strncmp(argv[i], "--query=", 8)) {
610 ctx.qry.raw = xstrdup(argv[i]+8); 615 ctx.qry.raw = xstrdup(argv[i]+8);
611 } 616 }
612 if (!strncmp(argv[i], "--repo=", 7)) { 617 if (!strncmp(argv[i], "--repo=", 7)) {
613 ctx.qry.repo = xstrdup(argv[i]+7); 618 ctx.qry.repo = xstrdup(argv[i]+7);
614 } 619 }
615 if (!strncmp(argv[i], "--page=", 7)) { 620 if (!strncmp(argv[i], "--page=", 7)) {
616 ctx.qry.page = xstrdup(argv[i]+7); 621 ctx.qry.page = xstrdup(argv[i]+7);
617 } 622 }
618 if (!strncmp(argv[i], "--head=", 7)) { 623 if (!strncmp(argv[i], "--head=", 7)) {
619 ctx.qry.head = xstrdup(argv[i]+7); 624 ctx.qry.head = xstrdup(argv[i]+7);
620 ctx.qry.has_symref = 1; 625 ctx.qry.has_symref = 1;
621 } 626 }
622 if (!strncmp(argv[i], "--sha1=", 7)) { 627 if (!strncmp(argv[i], "--sha1=", 7)) {
623 ctx.qry.sha1 = xstrdup(argv[i]+7); 628 ctx.qry.sha1 = xstrdup(argv[i]+7);
624 ctx.qry.has_sha1 = 1; 629 ctx.qry.has_sha1 = 1;
625 } 630 }
626 if (!strncmp(argv[i], "--ofs=", 6)) { 631 if (!strncmp(argv[i], "--ofs=", 6)) {
627 ctx.qry.ofs = atoi(argv[i]+6); 632 ctx.qry.ofs = atoi(argv[i]+6);
628 } 633 }
629 if (!strncmp(argv[i], "--scan-tree=", 12) || 634 if (!strncmp(argv[i], "--scan-tree=", 12) ||
630 !strncmp(argv[i], "--scan-path=", 12)) { 635 !strncmp(argv[i], "--scan-path=", 12)) {
631 /* HACK: the global snapshot bitmask defines the 636 /* HACK: the global snapshot bitmask defines the
632 * set of allowed snapshot formats, but the config 637 * set of allowed snapshot formats, but the config
633 * file hasn't been parsed yet so the mask is 638 * file hasn't been parsed yet so the mask is
634 * currently 0. By setting all bits high before 639 * currently 0. By setting all bits high before
635 * scanning we make sure that any in-repo cgitrc 640 * scanning we make sure that any in-repo cgitrc
636 * snapshot setting is respected by scan_tree(). 641 * snapshot setting is respected by scan_tree().
637 * BTW: we assume that there'll never be more than 642 * BTW: we assume that there'll never be more than
638 * 255 different snapshot formats supported by cgit... 643 * 255 different snapshot formats supported by cgit...
639 */ 644 */
640 ctx.cfg.snapshots = 0xFF; 645 ctx.cfg.snapshots = 0xFF;
641 scan++; 646 scan++;
642 scan_tree(argv[i] + 12, repo_config); 647 scan_tree(argv[i] + 12, repo_config);
643 } 648 }
644 } 649 }
645 if (scan) { 650 if (scan) {
646 qsort(cgit_repolist.repos, cgit_repolist.count, 651 qsort(cgit_repolist.repos, cgit_repolist.count,
647 sizeof(struct cgit_repo), cmp_repos); 652 sizeof(struct cgit_repo), cmp_repos);
648 print_repolist(stdout, &cgit_repolist, 0); 653 print_repolist(stdout, &cgit_repolist, 0);
649 exit(0); 654 exit(0);
650 } 655 }
651} 656}
652 657
653static int calc_ttl() 658static int calc_ttl()
654{ 659{
655 if (!ctx.repo) 660 if (!ctx.repo)
656 return ctx.cfg.cache_root_ttl; 661 return ctx.cfg.cache_root_ttl;
657 662
658 if (!ctx.qry.page) 663 if (!ctx.qry.page)
659 return ctx.cfg.cache_repo_ttl; 664 return ctx.cfg.cache_repo_ttl;
660 665
661 if (ctx.qry.has_symref) 666 if (ctx.qry.has_symref)
662 return ctx.cfg.cache_dynamic_ttl; 667 return ctx.cfg.cache_dynamic_ttl;
663 668
664 if (ctx.qry.has_sha1) 669 if (ctx.qry.has_sha1)
665 return ctx.cfg.cache_static_ttl; 670 return ctx.cfg.cache_static_ttl;
666 671
667 return ctx.cfg.cache_repo_ttl; 672 return ctx.cfg.cache_repo_ttl;
668} 673}
669 674
670int main(int argc, const char **argv) 675int main(int argc, const char **argv)
671{ 676{
672 const char *path; 677 const char *path;
673 char *qry; 678 char *qry;
674 int err, ttl; 679 int err, ttl;
675 680
676 prepare_context(&ctx); 681 prepare_context(&ctx);
677 cgit_repolist.length = 0; 682 cgit_repolist.length = 0;
678 cgit_repolist.count = 0; 683 cgit_repolist.count = 0;
679 cgit_repolist.repos = NULL; 684 cgit_repolist.repos = NULL;
680 685
681 cgit_parse_args(argc, argv); 686 cgit_parse_args(argc, argv);
682 parse_configfile(ctx.env.cgit_config, config_cb); 687 parse_configfile(ctx.env.cgit_config, config_cb);
683 ctx.repo = NULL; 688 ctx.repo = NULL;
684 http_parse_querystring(ctx.qry.raw, querystring_cb); 689 http_parse_querystring(ctx.qry.raw, querystring_cb);
685 690
686 /* If virtual-root isn't specified in cgitrc, lets pretend 691 /* If virtual-root isn't specified in cgitrc, lets pretend
687 * that virtual-root equals SCRIPT_NAME. 692 * that virtual-root equals SCRIPT_NAME.
688 */ 693 */
689 if (!ctx.cfg.virtual_root) 694 if (!ctx.cfg.virtual_root)
690 ctx.cfg.virtual_root = ctx.cfg.script_name; 695 ctx.cfg.virtual_root = ctx.cfg.script_name;
691 696
692 /* If no url parameter is specified on the querystring, lets 697 /* If no url parameter is specified on the querystring, lets
693 * use PATH_INFO as url. This allows cgit to work with virtual 698 * use PATH_INFO as url. This allows cgit to work with virtual
694 * urls without the need for rewriterules in the webserver (as 699 * urls without the need for rewriterules in the webserver (as
695 * long as PATH_INFO is included in the cache lookup key). 700 * long as PATH_INFO is included in the cache lookup key).
696 */ 701 */
697 path = ctx.env.path_info; 702 path = ctx.env.path_info;
698 if (!ctx.qry.url && path) { 703 if (!ctx.qry.url && path) {
699 if (path[0] == '/') 704 if (path[0] == '/')
700 path++; 705 path++;
701 ctx.qry.url = xstrdup(path); 706 ctx.qry.url = xstrdup(path);
702 if (ctx.qry.raw) { 707 if (ctx.qry.raw) {
703 qry = ctx.qry.raw; 708 qry = ctx.qry.raw;
704 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry)); 709 ctx.qry.raw = xstrdup(fmt("%s?%s", path, qry));
705 free(qry); 710 free(qry);
706 } else 711 } else
707 ctx.qry.raw = xstrdup(ctx.qry.url); 712 ctx.qry.raw = xstrdup(ctx.qry.url);
708 cgit_parse_url(ctx.qry.url); 713 cgit_parse_url(ctx.qry.url);
709 } 714 }
710 715
711 ttl = calc_ttl(); 716 ttl = calc_ttl();
712 ctx.page.expires += ttl*60; 717 ctx.page.expires += ttl*60;
713 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD")) 718 if (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))
714 ctx.cfg.nocache = 1; 719 ctx.cfg.nocache = 1;
715 if (ctx.cfg.nocache) 720 if (ctx.cfg.nocache)
716 ctx.cfg.cache_size = 0; 721 ctx.cfg.cache_size = 0;
717 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, 722 err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root,
718 ctx.qry.raw, ttl, process_request, &ctx); 723 ctx.qry.raw, ttl, process_request, &ctx);
719 if (err) 724 if (err)
720 cgit_print_error(fmt("Error processing page: %s (%d)", 725 cgit_print_error(fmt("Error processing page: %s (%d)",
721 strerror(err), err)); 726 strerror(err), err));
722 return err; 727 return err;
723} 728}
diff --git a/cgit.css b/cgit.css
index ef7d3c1..0cb894a 100644
--- a/cgit.css
+++ b/cgit.css
@@ -1,608 +1,707 @@
1body, table, form { 1body, table, form {
2 padding: 0em; 2 padding: 0em;
3 margin: 0em; 3 margin: 0em;
4} 4}
5 5
6body { 6body {
7 font-family: sans-serif; 7 font-family: sans-serif;
8 font-size: 10pt; 8 font-size: 10pt;
9 color: #333; 9 color: #333;
10 background: white; 10 background: white;
11 padding: 4px; 11 padding: 4px;
12} 12}
13 13
14a { 14a {
15 color: blue; 15 color: blue;
16 text-decoration: none; 16 text-decoration: none;
17} 17}
18 18
19a:hover { 19a:hover {
20 text-decoration: underline; 20 text-decoration: underline;
21} 21}
22 22
23table { 23table {
24 border-collapse: collapse; 24 border-collapse: collapse;
25} 25}
26 26
27table#header { 27table#header {
28 width: 100%; 28 width: 100%;
29 margin-bottom: 1em; 29 margin-bottom: 1em;
30} 30}
31 31
32table#header td.logo { 32table#header td.logo {
33 width: 96px; 33 width: 96px;
34} 34}
35 35
36table#header td.main { 36table#header td.main {
37 font-size: 250%; 37 font-size: 250%;
38 padding-left: 10px; 38 padding-left: 10px;
39 white-space: nowrap; 39 white-space: nowrap;
40} 40}
41 41
42table#header td.main a { 42table#header td.main a {
43 color: #000; 43 color: #000;
44} 44}
45 45
46table#header td.form { 46table#header td.form {
47 text-align: right; 47 text-align: right;
48 vertical-align: bottom; 48 vertical-align: bottom;
49 padding-right: 1em; 49 padding-right: 1em;
50 padding-bottom: 2px; 50 padding-bottom: 2px;
51 white-space: nowrap; 51 white-space: nowrap;
52} 52}
53 53
54table#header td.form form, 54table#header td.form form,
55table#header td.form input, 55table#header td.form input,
56table#header td.form select { 56table#header td.form select {
57 font-size: 90%; 57 font-size: 90%;
58} 58}
59 59
60table#header td.sub { 60table#header td.sub {
61 color: #777; 61 color: #777;
62 border-top: solid 1px #ccc; 62 border-top: solid 1px #ccc;
63 padding-left: 10px; 63 padding-left: 10px;
64} 64}
65 65
66table.tabs { 66table.tabs {
67 /* border-bottom: solid 2px #ccc; */ 67 /* border-bottom: solid 2px #ccc; */
68 border-collapse: collapse; 68 border-collapse: collapse;
69 margin-top: 2em; 69 margin-top: 2em;
70 margin-bottom: 0px; 70 margin-bottom: 0px;
71 width: 100%; 71 width: 100%;
72} 72}
73 73
74table.tabs td { 74table.tabs td {
75 padding: 0px 1em; 75 padding: 0px 1em;
76 vertical-align: bottom; 76 vertical-align: bottom;
77} 77}
78 78
79table.tabs td a { 79table.tabs td a {
80 padding: 2px 0.75em; 80 padding: 2px 0.75em;
81 color: #777; 81 color: #777;
82 font-size: 110%; 82 font-size: 110%;
83} 83}
84 84
85table.tabs td a.active { 85table.tabs td a.active {
86 color: #000; 86 color: #000;
87 background-color: #ccc; 87 background-color: #ccc;
88} 88}
89 89
90table.tabs td.form { 90table.tabs td.form {
91 text-align: right; 91 text-align: right;
92} 92}
93 93
94table.tabs td.form form { 94table.tabs td.form form {
95 padding-bottom: 2px; 95 padding-bottom: 2px;
96 font-size: 90%; 96 font-size: 90%;
97 white-space: nowrap; 97 white-space: nowrap;
98} 98}
99 99
100table.tabs td.form input, 100table.tabs td.form input,
101table.tabs td.form select { 101table.tabs td.form select {
102 font-size: 90%; 102 font-size: 90%;
103} 103}
104 104
105div.content { 105div.content {
106 margin: 0px; 106 margin: 0px;
107 padding: 2em; 107 padding: 2em;
108 border-top: solid 3px #ccc; 108 border-top: solid 3px #ccc;
109 border-bottom: solid 3px #ccc; 109 border-bottom: solid 3px #ccc;
110} 110}
111 111
112 112
113table.list { 113table.list {
114 width: 100%; 114 width: 100%;
115 border: none; 115 border: none;
116 border-collapse: collapse; 116 border-collapse: collapse;
117} 117}
118 118
119table.list tr { 119table.list tr {
120 background: white; 120 background: white;
121} 121}
122 122
123table.list tr.logheader { 123table.list tr.logheader {
124 background: #eee; 124 background: #eee;
125} 125}
126 126
127table.list tr:hover { 127table.list tr:hover {
128 background: #eee; 128 background: #eee;
129} 129}
130 130
131table.list tr.nohover:hover { 131table.list tr.nohover:hover {
132 background: white; 132 background: white;
133} 133}
134 134
135table.list th { 135table.list th {
136 font-weight: bold; 136 font-weight: bold;
137 /* color: #888; 137 /* color: #888;
138 border-top: dashed 1px #888; 138 border-top: dashed 1px #888;
139 border-bottom: dashed 1px #888; 139 border-bottom: dashed 1px #888;
140 */ 140 */
141 padding: 0.1em 0.5em 0.05em 0.5em; 141 padding: 0.1em 0.5em 0.05em 0.5em;
142 vertical-align: baseline; 142 vertical-align: baseline;
143} 143}
144 144
145table.list td { 145table.list td {
146 border: none; 146 border: none;
147 padding: 0.1em 0.5em 0.1em 0.5em; 147 padding: 0.1em 0.5em 0.1em 0.5em;
148} 148}
149 149
150table.list td.logsubject { 150table.list td.logsubject {
151 font-family: monospace; 151 font-family: monospace;
152 font-weight: bold; 152 font-weight: bold;
153} 153}
154 154
155table.list td.logmsg { 155table.list td.logmsg {
156 font-family: monospace; 156 font-family: monospace;
157 white-space: pre; 157 white-space: pre;
158 padding: 1em 0.5em 2em 0.5em; 158 padding: 1em 0.5em 2em 0.5em;
159} 159}
160 160
161table.list td a { 161table.list td a {
162 color: black; 162 color: black;
163} 163}
164 164
165table.list td a.ls-dir { 165table.list td a.ls-dir {
166 font-weight: bold; 166 font-weight: bold;
167 color: #00f; 167 color: #00f;
168} 168}
169 169
170table.list td a:hover { 170table.list td a:hover {
171 color: #00f; 171 color: #00f;
172} 172}
173 173
174img { 174img {
175 border: none; 175 border: none;
176} 176}
177 177
178input#switch-btn { 178input#switch-btn {
179 margin: 2px 0px 0px 0px; 179 margin: 2px 0px 0px 0px;
180} 180}
181 181
182td#sidebar input.txt { 182td#sidebar input.txt {
183 width: 100%; 183 width: 100%;
184 margin: 2px 0px 0px 0px; 184 margin: 2px 0px 0px 0px;
185} 185}
186 186
187table#grid { 187table#grid {
188 margin: 0px; 188 margin: 0px;
189} 189}
190 190
191td#content { 191td#content {
192 vertical-align: top; 192 vertical-align: top;
193 padding: 1em 2em 1em 1em; 193 padding: 1em 2em 1em 1em;
194 border: none; 194 border: none;
195} 195}
196 196
197div#summary { 197div#summary {
198 vertical-align: top; 198 vertical-align: top;
199 margin-bottom: 1em; 199 margin-bottom: 1em;
200} 200}
201 201
202table#downloads { 202table#downloads {
203 float: right; 203 float: right;
204 border-collapse: collapse; 204 border-collapse: collapse;
205 border: solid 1px #777; 205 border: solid 1px #777;
206 margin-left: 0.5em; 206 margin-left: 0.5em;
207 margin-bottom: 0.5em; 207 margin-bottom: 0.5em;
208} 208}
209 209
210table#downloads th { 210table#downloads th {
211 background-color: #ccc; 211 background-color: #ccc;
212} 212}
213 213
214div#blob { 214div#blob {
215 border: solid 1px black; 215 border: solid 1px black;
216} 216}
217 217
218div.error { 218div.error {
219 color: red; 219 color: red;
220 font-weight: bold; 220 font-weight: bold;
221 margin: 1em 2em; 221 margin: 1em 2em;
222} 222}
223 223
224a.ls-blob, a.ls-dir, a.ls-mod { 224a.ls-blob, a.ls-dir, a.ls-mod {
225 font-family: monospace; 225 font-family: monospace;
226} 226}
227 227
228td.ls-size { 228td.ls-size {
229 text-align: right; 229 text-align: right;
230 font-family: monospace; 230 font-family: monospace;
231 width: 10em; 231 width: 10em;
232} 232}
233 233
234td.ls-mode { 234td.ls-mode {
235 font-family: monospace; 235 font-family: monospace;
236 width: 10em; 236 width: 10em;
237} 237}
238 238
239table.blob { 239table.blob {
240 margin-top: 0.5em; 240 margin-top: 0.5em;
241 border-top: solid 1px black; 241 border-top: solid 1px black;
242} 242}
243 243
244table.blob td.lines { 244table.blob td.lines {
245 margin: 0; padding: 0 0 0 0.5em; 245 margin: 0; padding: 0 0 0 0.5em;
246 vertical-align: top; 246 vertical-align: top;
247 color: black; 247 color: black;
248} 248}
249 249
250table.blob td.linenumbers { 250table.blob td.linenumbers {
251 margin: 0; padding: 0 0.5em 0 0.5em; 251 margin: 0; padding: 0 0.5em 0 0.5em;
252 vertical-align: top; 252 vertical-align: top;
253 text-align: right; 253 text-align: right;
254 border-right: 1px solid gray; 254 border-right: 1px solid gray;
255} 255}
256 256
257table.blob pre { 257table.blob pre {
258 padding: 0; margin: 0; 258 padding: 0; margin: 0;
259} 259}
260 260
261table.blob a.no { 261table.blob a.no {
262 color: gray; 262 color: gray;
263 text-align: right; 263 text-align: right;
264 text-decoration: none; 264 text-decoration: none;
265} 265}
266 266
267table.blob a.no a:hover { 267table.blob a.no a:hover {
268 color: black; 268 color: black;
269} 269}
270 270
271table.bin-blob { 271table.bin-blob {
272 margin-top: 0.5em; 272 margin-top: 0.5em;
273 border: solid 1px black; 273 border: solid 1px black;
274} 274}
275 275
276table.bin-blob th { 276table.bin-blob th {
277 font-family: monospace; 277 font-family: monospace;
278 white-space: pre; 278 white-space: pre;
279 border: solid 1px #777; 279 border: solid 1px #777;
280 padding: 0.5em 1em; 280 padding: 0.5em 1em;
281} 281}
282 282
283table.bin-blob td { 283table.bin-blob td {
284 font-family: monospace; 284 font-family: monospace;
285 white-space: pre; 285 white-space: pre;
286 border-left: solid 1px #777; 286 border-left: solid 1px #777;
287 padding: 0em 1em; 287 padding: 0em 1em;
288} 288}
289 289
290table.nowrap td { 290table.nowrap td {
291 white-space: nowrap; 291 white-space: nowrap;
292} 292}
293 293
294table.commit-info { 294table.commit-info {
295 border-collapse: collapse; 295 border-collapse: collapse;
296 margin-top: 1.5em; 296 margin-top: 1.5em;
297} 297}
298 298
299table.commit-info th { 299table.commit-info th {
300 text-align: left; 300 text-align: left;
301 font-weight: normal; 301 font-weight: normal;
302 padding: 0.1em 1em 0.1em 0.1em; 302 padding: 0.1em 1em 0.1em 0.1em;
303 vertical-align: top; 303 vertical-align: top;
304} 304}
305 305
306table.commit-info td { 306table.commit-info td {
307 font-weight: normal; 307 font-weight: normal;
308 padding: 0.1em 1em 0.1em 0.1em; 308 padding: 0.1em 1em 0.1em 0.1em;
309} 309}
310 310
311div.commit-subject { 311div.commit-subject {
312 font-weight: bold; 312 font-weight: bold;
313 font-size: 125%; 313 font-size: 125%;
314 margin: 1.5em 0em 0.5em 0em; 314 margin: 1.5em 0em 0.5em 0em;
315 padding: 0em; 315 padding: 0em;
316} 316}
317 317
318div.commit-msg { 318div.commit-msg {
319 white-space: pre; 319 white-space: pre;
320 font-family: monospace; 320 font-family: monospace;
321} 321}
322 322
323div.diffstat-header { 323div.diffstat-header {
324 font-weight: bold; 324 font-weight: bold;
325 padding-top: 1.5em; 325 padding-top: 1.5em;
326} 326}
327 327
328table.diffstat { 328table.diffstat {
329 border-collapse: collapse; 329 border-collapse: collapse;
330 border: solid 1px #aaa; 330 border: solid 1px #aaa;
331 background-color: #eee; 331 background-color: #eee;
332} 332}
333 333
334table.diffstat th { 334table.diffstat th {
335 font-weight: normal; 335 font-weight: normal;
336 text-align: left; 336 text-align: left;
337 text-decoration: underline; 337 text-decoration: underline;
338 padding: 0.1em 1em 0.1em 0.1em; 338 padding: 0.1em 1em 0.1em 0.1em;
339 font-size: 100%; 339 font-size: 100%;
340} 340}
341 341
342table.diffstat td { 342table.diffstat td {
343 padding: 0.2em 0.2em 0.1em 0.1em; 343 padding: 0.2em 0.2em 0.1em 0.1em;
344 font-size: 100%; 344 font-size: 100%;
345 border: none; 345 border: none;
346} 346}
347 347
348table.diffstat td.mode { 348table.diffstat td.mode {
349 white-space: nowrap; 349 white-space: nowrap;
350} 350}
351 351
352table.diffstat td span.modechange { 352table.diffstat td span.modechange {
353 padding-left: 1em; 353 padding-left: 1em;
354 color: red; 354 color: red;
355} 355}
356 356
357table.diffstat td.add a { 357table.diffstat td.add a {
358 color: green; 358 color: green;
359} 359}
360 360
361table.diffstat td.del a { 361table.diffstat td.del a {
362 color: red; 362 color: red;
363} 363}
364 364
365table.diffstat td.upd a { 365table.diffstat td.upd a {
366 color: blue; 366 color: blue;
367} 367}
368 368
369table.diffstat td.graph { 369table.diffstat td.graph {
370 width: 500px; 370 width: 500px;
371 vertical-align: middle; 371 vertical-align: middle;
372} 372}
373 373
374table.diffstat td.graph table { 374table.diffstat td.graph table {
375 border: none; 375 border: none;
376} 376}
377 377
378table.diffstat td.graph td { 378table.diffstat td.graph td {
379 padding: 0px; 379 padding: 0px;
380 border: 0px; 380 border: 0px;
381 height: 7pt; 381 height: 7pt;
382} 382}
383 383
384table.diffstat td.graph td.add { 384table.diffstat td.graph td.add {
385 background-color: #5c5; 385 background-color: #5c5;
386} 386}
387 387
388table.diffstat td.graph td.rem { 388table.diffstat td.graph td.rem {
389 background-color: #c55; 389 background-color: #c55;
390} 390}
391 391
392div.diffstat-summary { 392div.diffstat-summary {
393 color: #888; 393 color: #888;
394 padding-top: 0.5em; 394 padding-top: 0.5em;
395} 395}
396 396
397table.diff { 397table.diff {
398 width: 100%; 398 width: 100%;
399} 399}
400 400
401table.diff td { 401table.diff td {
402 font-family: monospace; 402 font-family: monospace;
403 white-space: pre; 403 white-space: pre;
404} 404}
405 405
406table.diff td div.head { 406table.diff td div.head {
407 font-weight: bold; 407 font-weight: bold;
408 margin-top: 1em; 408 margin-top: 1em;
409 color: black; 409 color: black;
410} 410}
411 411
412table.diff td div.hunk { 412table.diff td div.hunk {
413 color: #009; 413 color: #009;
414} 414}
415 415
416table.diff td div.add { 416table.diff td div.add {
417 color: green; 417 color: green;
418} 418}
419 419
420table.diff td div.del { 420table.diff td div.del {
421 color: red; 421 color: red;
422} 422}
423 423
424.sha1 { 424.sha1 {
425 font-family: monospace; 425 font-family: monospace;
426 font-size: 90%; 426 font-size: 90%;
427} 427}
428 428
429.left { 429.left {
430 text-align: left; 430 text-align: left;
431} 431}
432 432
433.right { 433.right {
434 text-align: right; 434 text-align: right;
435} 435}
436 436
437table.list td.reposection { 437table.list td.reposection {
438 font-style: italic; 438 font-style: italic;
439 color: #888; 439 color: #888;
440} 440}
441 441
442a.button { 442a.button {
443 font-size: 80%; 443 font-size: 80%;
444 padding: 0em 0.5em; 444 padding: 0em 0.5em;
445} 445}
446 446
447a.primary { 447a.primary {
448 font-size: 100%; 448 font-size: 100%;
449} 449}
450 450
451a.secondary { 451a.secondary {
452 font-size: 90%; 452 font-size: 90%;
453} 453}
454 454
455td.toplevel-repo { 455td.toplevel-repo {
456 456
457} 457}
458 458
459table.list td.sublevel-repo { 459table.list td.sublevel-repo {
460 padding-left: 1.5em; 460 padding-left: 1.5em;
461} 461}
462 462
463div.pager { 463div.pager {
464 text-align: center; 464 text-align: center;
465 margin: 1em 0em 0em 0em; 465 margin: 1em 0em 0em 0em;
466} 466}
467 467
468div.pager a { 468div.pager a {
469 color: #777; 469 color: #777;
470 margin: 0em 0.5em; 470 margin: 0em 0.5em;
471} 471}
472 472
473span.age-mins { 473span.age-mins {
474 font-weight: bold; 474 font-weight: bold;
475 color: #080; 475 color: #080;
476} 476}
477 477
478span.age-hours { 478span.age-hours {
479 color: #080; 479 color: #080;
480} 480}
481 481
482span.age-days { 482span.age-days {
483 color: #040; 483 color: #040;
484} 484}
485 485
486span.age-weeks { 486span.age-weeks {
487 color: #444; 487 color: #444;
488} 488}
489 489
490span.age-months { 490span.age-months {
491 color: #888; 491 color: #888;
492} 492}
493 493
494span.age-years { 494span.age-years {
495 color: #bbb; 495 color: #bbb;
496} 496}
497div.footer { 497div.footer {
498 margin-top: 0.5em; 498 margin-top: 0.5em;
499 text-align: center; 499 text-align: center;
500 font-size: 80%; 500 font-size: 80%;
501 color: #ccc; 501 color: #ccc;
502} 502}
503a.branch-deco { 503a.branch-deco {
504 margin: 0px 0.5em; 504 margin: 0px 0.5em;
505 padding: 0px 0.25em; 505 padding: 0px 0.25em;
506 background-color: #88ff88; 506 background-color: #88ff88;
507 border: solid 1px #007700; 507 border: solid 1px #007700;
508} 508}
509a.tag-deco { 509a.tag-deco {
510 margin: 0px 0.5em; 510 margin: 0px 0.5em;
511 padding: 0px 0.25em; 511 padding: 0px 0.25em;
512 background-color: #ffff88; 512 background-color: #ffff88;
513 border: solid 1px #777700; 513 border: solid 1px #777700;
514} 514}
515a.remote-deco { 515a.remote-deco {
516 margin: 0px 0.5em; 516 margin: 0px 0.5em;
517 padding: 0px 0.25em; 517 padding: 0px 0.25em;
518 background-color: #ccccff; 518 background-color: #ccccff;
519 border: solid 1px #000077; 519 border: solid 1px #000077;
520} 520}
521a.deco { 521a.deco {
522 margin: 0px 0.5em; 522 margin: 0px 0.5em;
523 padding: 0px 0.25em; 523 padding: 0px 0.25em;
524 background-color: #ff8888; 524 background-color: #ff8888;
525 border: solid 1px #770000; 525 border: solid 1px #770000;
526} 526}
527 527
528div.commit-subject a { 528div.commit-subject a {
529 margin-left: 1em; 529 margin-left: 1em;
530 font-size: 75%; 530 font-size: 75%;
531} 531}
532 532
533table.stats { 533table.stats {
534 border: solid 1px black; 534 border: solid 1px black;
535 border-collapse: collapse; 535 border-collapse: collapse;
536} 536}
537 537
538table.stats th { 538table.stats th {
539 text-align: left; 539 text-align: left;
540 padding: 1px 0.5em; 540 padding: 1px 0.5em;
541 background-color: #eee; 541 background-color: #eee;
542 border: solid 1px black; 542 border: solid 1px black;
543} 543}
544 544
545table.stats td { 545table.stats td {
546 text-align: right; 546 text-align: right;
547 padding: 1px 0.5em; 547 padding: 1px 0.5em;
548 border: solid 1px black; 548 border: solid 1px black;
549} 549}
550 550
551table.stats td.total { 551table.stats td.total {
552 font-weight: bold; 552 font-weight: bold;
553 text-align: left; 553 text-align: left;
554} 554}
555 555
556table.stats td.sum { 556table.stats td.sum {
557 color: #c00; 557 color: #c00;
558 font-weight: bold; 558 font-weight: bold;
559 /*background-color: #eee; */ 559 /*background-color: #eee; */
560} 560}
561 561
562table.stats td.left { 562table.stats td.left {
563 text-align: left; 563 text-align: left;
564} 564}
565 565
566table.vgraph { 566table.vgraph {
567 border-collapse: separate; 567 border-collapse: separate;
568 border: solid 1px black; 568 border: solid 1px black;
569 height: 200px; 569 height: 200px;
570} 570}
571 571
572table.vgraph th { 572table.vgraph th {
573 background-color: #eee; 573 background-color: #eee;
574 font-weight: bold; 574 font-weight: bold;
575 border: solid 1px white; 575 border: solid 1px white;
576 padding: 1px 0.5em; 576 padding: 1px 0.5em;
577} 577}
578 578
579table.vgraph td { 579table.vgraph td {
580 vertical-align: bottom; 580 vertical-align: bottom;
581 padding: 0px 10px; 581 padding: 0px 10px;
582} 582}
583 583
584table.vgraph div.bar { 584table.vgraph div.bar {
585 background-color: #eee; 585 background-color: #eee;
586} 586}
587 587
588table.hgraph { 588table.hgraph {
589 border: solid 1px black; 589 border: solid 1px black;
590 width: 800px; 590 width: 800px;
591} 591}
592 592
593table.hgraph th { 593table.hgraph th {
594 background-color: #eee; 594 background-color: #eee;
595 font-weight: bold; 595 font-weight: bold;
596 border: solid 1px black; 596 border: solid 1px black;
597 padding: 1px 0.5em; 597 padding: 1px 0.5em;
598} 598}
599 599
600table.hgraph td { 600table.hgraph td {
601 vertical-align: center; 601 vertical-align: center;
602 padding: 2px 2px; 602 padding: 2px 2px;
603} 603}
604 604
605table.hgraph div.bar { 605table.hgraph div.bar {
606 background-color: #eee; 606 background-color: #eee;
607 height: 1em; 607 height: 1em;
608} 608}
609
610table.ssdiff {
611 width: 100%;
612}
613
614table.ssdiff td {
615 font-size: 75%;
616 font-family: monospace;
617 white-space: pre;
618 padding: 1px 4px 1px 4px;
619 border-left: solid 1px #aaa;
620 border-right: solid 1px #aaa;
621}
622
623table.ssdiff td.add {
624 color: black;
625 background: #cfc;
626 min-width: 50%;
627}
628
629table.ssdiff td.add_dark {
630 color: black;
631 background: #aca;
632 min-width: 50%;
633}
634
635table.ssdiff span.add {
636 background: #cfc;
637 font-weight: bold;
638}
639
640table.ssdiff td.del {
641 color: black;
642 background: #fcc;
643 min-width: 50%;
644}
645
646table.ssdiff td.del_dark {
647 color: black;
648 background: #caa;
649 min-width: 50%;
650}
651
652table.ssdiff span.del {
653 background: #fcc;
654 font-weight: bold;
655}
656
657table.ssdiff td.changed {
658 color: black;
659 background: #ffc;
660 min-width: 50%;
661}
662
663table.ssdiff td.changed_dark {
664 color: black;
665 background: #cca;
666 min-width: 50%;
667}
668
669table.ssdiff td.lineno {
670 color: black;
671 background: #eee;
672 text-align: right;
673 width: 3em;
674 min-width: 3em;
675}
676
677table.ssdiff td.hunk {
678 color: #black;
679 background: #ccf;
680 border-top: solid 1px #aaa;
681 border-bottom: solid 1px #aaa;
682}
683
684table.ssdiff td.head {
685 border-top: solid 1px #aaa;
686 border-bottom: solid 1px #aaa;
687}
688
689table.ssdiff td.head div.head {
690 font-weight: bold;
691 color: black;
692}
693
694table.ssdiff td.foot {
695 border-top: solid 1px #aaa;
696 border-left: none;
697 border-right: none;
698 border-bottom: none;
699}
700
701table.ssdiff td.space {
702 border: none;
703}
704
705table.ssdiff td.space div {
706 min-height: 3em;
707} \ No newline at end of file
diff --git a/cgit.h b/cgit.h
index 39853df..5941ec0 100644
--- a/cgit.h
+++ b/cgit.h
@@ -1,295 +1,297 @@
1#ifndef CGIT_H 1#ifndef CGIT_H
2#define CGIT_H 2#define CGIT_H
3 3
4 4
5#include <git-compat-util.h> 5#include <git-compat-util.h>
6#include <cache.h> 6#include <cache.h>
7#include <grep.h> 7#include <grep.h>
8#include <object.h> 8#include <object.h>
9#include <tree.h> 9#include <tree.h>
10#include <commit.h> 10#include <commit.h>
11#include <tag.h> 11#include <tag.h>
12#include <diff.h> 12#include <diff.h>
13#include <diffcore.h> 13#include <diffcore.h>
14#include <refs.h> 14#include <refs.h>
15#include <revision.h> 15#include <revision.h>
16#include <log-tree.h> 16#include <log-tree.h>
17#include <archive.h> 17#include <archive.h>
18#include <string-list.h> 18#include <string-list.h>
19#include <xdiff-interface.h> 19#include <xdiff-interface.h>
20#include <xdiff/xdiff.h> 20#include <xdiff/xdiff.h>
21#include <utf8.h> 21#include <utf8.h>
22 22
23 23
24/* 24/*
25 * Dateformats used on misc. pages 25 * Dateformats used on misc. pages
26 */ 26 */
27#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)" 27#define FMT_LONGDATE "%Y-%m-%d %H:%M:%S (%Z)"
28#define FMT_SHORTDATE "%Y-%m-%d" 28#define FMT_SHORTDATE "%Y-%m-%d"
29#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ" 29#define FMT_ATOMDATE "%Y-%m-%dT%H:%M:%SZ"
30 30
31 31
32/* 32/*
33 * Limits used for relative dates 33 * Limits used for relative dates
34 */ 34 */
35#define TM_MIN 60 35#define TM_MIN 60
36#define TM_HOUR (TM_MIN * 60) 36#define TM_HOUR (TM_MIN * 60)
37#define TM_DAY (TM_HOUR * 24) 37#define TM_DAY (TM_HOUR * 24)
38#define TM_WEEK (TM_DAY * 7) 38#define TM_WEEK (TM_DAY * 7)
39#define TM_YEAR (TM_DAY * 365) 39#define TM_YEAR (TM_DAY * 365)
40#define TM_MONTH (TM_YEAR / 12.0) 40#define TM_MONTH (TM_YEAR / 12.0)
41 41
42 42
43/* 43/*
44 * Default encoding 44 * Default encoding
45 */ 45 */
46#define PAGE_ENCODING "UTF-8" 46#define PAGE_ENCODING "UTF-8"
47 47
48typedef void (*configfn)(const char *name, const char *value); 48typedef void (*configfn)(const char *name, const char *value);
49typedef void (*filepair_fn)(struct diff_filepair *pair); 49typedef void (*filepair_fn)(struct diff_filepair *pair);
50typedef void (*linediff_fn)(char *line, int len); 50typedef void (*linediff_fn)(char *line, int len);
51 51
52struct cgit_filter { 52struct cgit_filter {
53 char *cmd; 53 char *cmd;
54 char **argv; 54 char **argv;
55 int old_stdout; 55 int old_stdout;
56 int pipe_fh[2]; 56 int pipe_fh[2];
57 int pid; 57 int pid;
58 int exitstatus; 58 int exitstatus;
59}; 59};
60 60
61struct cgit_repo { 61struct cgit_repo {
62 char *url; 62 char *url;
63 char *name; 63 char *name;
64 char *path; 64 char *path;
65 char *desc; 65 char *desc;
66 char *owner; 66 char *owner;
67 char *defbranch; 67 char *defbranch;
68 char *module_link; 68 char *module_link;
69 char *readme; 69 char *readme;
70 char *section; 70 char *section;
71 char *clone_url; 71 char *clone_url;
72 int snapshots; 72 int snapshots;
73 int enable_log_filecount; 73 int enable_log_filecount;
74 int enable_log_linecount; 74 int enable_log_linecount;
75 int max_stats; 75 int max_stats;
76 time_t mtime; 76 time_t mtime;
77 struct cgit_filter *about_filter; 77 struct cgit_filter *about_filter;
78 struct cgit_filter *commit_filter; 78 struct cgit_filter *commit_filter;
79 struct cgit_filter *source_filter; 79 struct cgit_filter *source_filter;
80}; 80};
81 81
82typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name, 82typedef void (*repo_config_fn)(struct cgit_repo *repo, const char *name,
83 const char *value); 83 const char *value);
84 84
85struct cgit_repolist { 85struct cgit_repolist {
86 int length; 86 int length;
87 int count; 87 int count;
88 struct cgit_repo *repos; 88 struct cgit_repo *repos;
89}; 89};
90 90
91struct commitinfo { 91struct commitinfo {
92 struct commit *commit; 92 struct commit *commit;
93 char *author; 93 char *author;
94 char *author_email; 94 char *author_email;
95 unsigned long author_date; 95 unsigned long author_date;
96 char *committer; 96 char *committer;
97 char *committer_email; 97 char *committer_email;
98 unsigned long committer_date; 98 unsigned long committer_date;
99 char *subject; 99 char *subject;
100 char *msg; 100 char *msg;
101 char *msg_encoding; 101 char *msg_encoding;
102}; 102};
103 103
104struct taginfo { 104struct taginfo {
105 char *tagger; 105 char *tagger;
106 char *tagger_email; 106 char *tagger_email;
107 unsigned long tagger_date; 107 unsigned long tagger_date;
108 char *msg; 108 char *msg;
109}; 109};
110 110
111struct refinfo { 111struct refinfo {
112 const char *refname; 112 const char *refname;
113 struct object *object; 113 struct object *object;
114 union { 114 union {
115 struct taginfo *tag; 115 struct taginfo *tag;
116 struct commitinfo *commit; 116 struct commitinfo *commit;
117 }; 117 };
118}; 118};
119 119
120struct reflist { 120struct reflist {
121 struct refinfo **refs; 121 struct refinfo **refs;
122 int alloc; 122 int alloc;
123 int count; 123 int count;
124}; 124};
125 125
126struct cgit_query { 126struct cgit_query {
127 int has_symref; 127 int has_symref;
128 int has_sha1; 128 int has_sha1;
129 char *raw; 129 char *raw;
130 char *repo; 130 char *repo;
131 char *page; 131 char *page;
132 char *search; 132 char *search;
133 char *grep; 133 char *grep;
134 char *head; 134 char *head;
135 char *sha1; 135 char *sha1;
136 char *sha2; 136 char *sha2;
137 char *path; 137 char *path;
138 char *name; 138 char *name;
139 char *mimetype; 139 char *mimetype;
140 char *url; 140 char *url;
141 char *period; 141 char *period;
142 int ofs; 142 int ofs;
143 int nohead; 143 int nohead;
144 char *sort; 144 char *sort;
145 int showmsg; 145 int showmsg;
146 int ssdiff;
146}; 147};
147 148
148struct cgit_config { 149struct cgit_config {
149 char *agefile; 150 char *agefile;
150 char *cache_root; 151 char *cache_root;
151 char *clone_prefix; 152 char *clone_prefix;
152 char *css; 153 char *css;
153 char *favicon; 154 char *favicon;
154 char *footer; 155 char *footer;
155 char *head_include; 156 char *head_include;
156 char *header; 157 char *header;
157 char *index_header; 158 char *index_header;
158 char *index_info; 159 char *index_info;
159 char *logo; 160 char *logo;
160 char *logo_link; 161 char *logo_link;
161 char *module_link; 162 char *module_link;
162 char *robots; 163 char *robots;
163 char *root_title; 164 char *root_title;
164 char *root_desc; 165 char *root_desc;
165 char *root_readme; 166 char *root_readme;
166 char *script_name; 167 char *script_name;
167 char *section; 168 char *section;
168 char *virtual_root; 169 char *virtual_root;
169 int cache_size; 170 int cache_size;
170 int cache_dynamic_ttl; 171 int cache_dynamic_ttl;
171 int cache_max_create_time; 172 int cache_max_create_time;
172 int cache_repo_ttl; 173 int cache_repo_ttl;
173 int cache_root_ttl; 174 int cache_root_ttl;
174 int cache_scanrc_ttl; 175 int cache_scanrc_ttl;
175 int cache_static_ttl; 176 int cache_static_ttl;
176 int embedded; 177 int embedded;
177 int enable_filter_overrides; 178 int enable_filter_overrides;
178 int enable_index_links; 179 int enable_index_links;
179 int enable_log_filecount; 180 int enable_log_filecount;
180 int enable_log_linecount; 181 int enable_log_linecount;
181 int enable_tree_linenumbers; 182 int enable_tree_linenumbers;
182 int local_time; 183 int local_time;
183 int max_repo_count; 184 int max_repo_count;
184 int max_commit_count; 185 int max_commit_count;
185 int max_lock_attempts; 186 int max_lock_attempts;
186 int max_msg_len; 187 int max_msg_len;
187 int max_repodesc_len; 188 int max_repodesc_len;
188 int max_blob_size; 189 int max_blob_size;
189 int max_stats; 190 int max_stats;
190 int nocache; 191 int nocache;
191 int noplainemail; 192 int noplainemail;
192 int noheader; 193 int noheader;
193 int renamelimit; 194 int renamelimit;
194 int snapshots; 195 int snapshots;
195 int summary_branches; 196 int summary_branches;
196 int summary_log; 197 int summary_log;
197 int summary_tags; 198 int summary_tags;
199 int ssdiff;
198 struct string_list mimetypes; 200 struct string_list mimetypes;
199 struct cgit_filter *about_filter; 201 struct cgit_filter *about_filter;
200 struct cgit_filter *commit_filter; 202 struct cgit_filter *commit_filter;
201 struct cgit_filter *source_filter; 203 struct cgit_filter *source_filter;
202}; 204};
203 205
204struct cgit_page { 206struct cgit_page {
205 time_t modified; 207 time_t modified;
206 time_t expires; 208 time_t expires;
207 size_t size; 209 size_t size;
208 char *mimetype; 210 char *mimetype;
209 char *charset; 211 char *charset;
210 char *filename; 212 char *filename;
211 char *etag; 213 char *etag;
212 char *title; 214 char *title;
213 int status; 215 int status;
214 char *statusmsg; 216 char *statusmsg;
215}; 217};
216 218
217struct cgit_environment { 219struct cgit_environment {
218 char *cgit_config; 220 char *cgit_config;
219 char *http_host; 221 char *http_host;
220 char *https; 222 char *https;
221 char *no_http; 223 char *no_http;
222 char *path_info; 224 char *path_info;
223 char *query_string; 225 char *query_string;
224 char *request_method; 226 char *request_method;
225 char *script_name; 227 char *script_name;
226 char *server_name; 228 char *server_name;
227 char *server_port; 229 char *server_port;
228}; 230};
229 231
230struct cgit_context { 232struct cgit_context {
231 struct cgit_environment env; 233 struct cgit_environment env;
232 struct cgit_query qry; 234 struct cgit_query qry;
233 struct cgit_config cfg; 235 struct cgit_config cfg;
234 struct cgit_repo *repo; 236 struct cgit_repo *repo;
235 struct cgit_page page; 237 struct cgit_page page;
236}; 238};
237 239
238struct cgit_snapshot_format { 240struct cgit_snapshot_format {
239 const char *suffix; 241 const char *suffix;
240 const char *mimetype; 242 const char *mimetype;
241 write_archive_fn_t write_func; 243 write_archive_fn_t write_func;
242 int bit; 244 int bit;
243}; 245};
244 246
245extern const char *cgit_version; 247extern const char *cgit_version;
246 248
247extern struct cgit_repolist cgit_repolist; 249extern struct cgit_repolist cgit_repolist;
248extern struct cgit_context ctx; 250extern struct cgit_context ctx;
249extern const struct cgit_snapshot_format cgit_snapshot_formats[]; 251extern const struct cgit_snapshot_format cgit_snapshot_formats[];
250 252
251extern struct cgit_repo *cgit_add_repo(const char *url); 253extern struct cgit_repo *cgit_add_repo(const char *url);
252extern struct cgit_repo *cgit_get_repoinfo(const char *url); 254extern struct cgit_repo *cgit_get_repoinfo(const char *url);
253extern void cgit_repo_config_cb(const char *name, const char *value); 255extern void cgit_repo_config_cb(const char *name, const char *value);
254 256
255extern int chk_zero(int result, char *msg); 257extern int chk_zero(int result, char *msg);
256extern int chk_positive(int result, char *msg); 258extern int chk_positive(int result, char *msg);
257extern int chk_non_negative(int result, char *msg); 259extern int chk_non_negative(int result, char *msg);
258 260
259extern char *trim_end(const char *str, char c); 261extern char *trim_end(const char *str, char c);
260extern char *strlpart(char *txt, int maxlen); 262extern char *strlpart(char *txt, int maxlen);
261extern char *strrpart(char *txt, int maxlen); 263extern char *strrpart(char *txt, int maxlen);
262 264
263extern void cgit_add_ref(struct reflist *list, struct refinfo *ref); 265extern void cgit_add_ref(struct reflist *list, struct refinfo *ref);
264extern int cgit_refs_cb(const char *refname, const unsigned char *sha1, 266extern int cgit_refs_cb(const char *refname, const unsigned char *sha1,
265 int flags, void *cb_data); 267 int flags, void *cb_data);
266 268
267extern void *cgit_free_commitinfo(struct commitinfo *info); 269extern void *cgit_free_commitinfo(struct commitinfo *info);
268 270
269extern int cgit_diff_files(const unsigned char *old_sha1, 271extern int cgit_diff_files(const unsigned char *old_sha1,
270 const unsigned char *new_sha1, 272 const unsigned char *new_sha1,
271 unsigned long *old_size, unsigned long *new_size, 273 unsigned long *old_size, unsigned long *new_size,
272 int *binary, linediff_fn fn); 274 int *binary, linediff_fn fn);
273 275
274extern void cgit_diff_tree(const unsigned char *old_sha1, 276extern void cgit_diff_tree(const unsigned char *old_sha1,
275 const unsigned char *new_sha1, 277 const unsigned char *new_sha1,
276 filepair_fn fn, const char *prefix); 278 filepair_fn fn, const char *prefix);
277 279
278extern void cgit_diff_commit(struct commit *commit, filepair_fn fn); 280extern void cgit_diff_commit(struct commit *commit, filepair_fn fn);
279 281
280extern char *fmt(const char *format,...); 282extern char *fmt(const char *format,...);
281 283
282extern struct commitinfo *cgit_parse_commit(struct commit *commit); 284extern struct commitinfo *cgit_parse_commit(struct commit *commit);
283extern struct taginfo *cgit_parse_tag(struct tag *tag); 285extern struct taginfo *cgit_parse_tag(struct tag *tag);
284extern void cgit_parse_url(const char *url); 286extern void cgit_parse_url(const char *url);
285 287
286extern const char *cgit_repobasename(const char *reponame); 288extern const char *cgit_repobasename(const char *reponame);
287 289
288extern int cgit_parse_snapshots_mask(const char *str); 290extern int cgit_parse_snapshots_mask(const char *str);
289 291
290extern int cgit_open_filter(struct cgit_filter *filter); 292extern int cgit_open_filter(struct cgit_filter *filter);
291extern int cgit_close_filter(struct cgit_filter *filter); 293extern int cgit_close_filter(struct cgit_filter *filter);
292 294
293extern int readfile(const char *path, char **buf, size_t *size); 295extern int readfile(const char *path, char **buf, size_t *size);
294 296
295#endif /* CGIT_H */ 297#endif /* CGIT_H */
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index e69140b..70e4c78 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -1,505 +1,509 @@
1:man source: cgit 1:man source: cgit
2:man manual: cgit 2:man manual: cgit
3 3
4CGITRC(5) 4CGITRC(5)
5======== 5========
6 6
7 7
8NAME 8NAME
9---- 9----
10cgitrc - runtime configuration for cgit 10cgitrc - runtime configuration for cgit
11 11
12 12
13SYNOPSIS 13SYNOPSIS
14-------- 14--------
15Cgitrc contains all runtime settings for cgit, including the list of git 15Cgitrc contains all runtime settings for cgit, including the list of git
16repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank 16repositories, formatted as a line-separated list of NAME=VALUE pairs. Blank
17lines, and lines starting with '#', are ignored. 17lines, and lines starting with '#', are ignored.
18 18
19 19
20LOCATION 20LOCATION
21-------- 21--------
22The default location of cgitrc, defined at compile time, is /etc/cgitrc. At 22The default location of cgitrc, defined at compile time, is /etc/cgitrc. At
23runtime, cgit will consult the environment variable CGIT_CONFIG and, if 23runtime, cgit will consult the environment variable CGIT_CONFIG and, if
24defined, use its value instead. 24defined, use its value instead.
25 25
26 26
27GLOBAL SETTINGS 27GLOBAL SETTINGS
28--------------- 28---------------
29about-filter:: 29about-filter::
30 Specifies a command which will be invoked to format the content of 30 Specifies a command which will be invoked to format the content of
31 about pages (both top-level and for each repository). The command will 31 about pages (both top-level and for each repository). The command will
32 get the content of the about-file on its STDIN, and the STDOUT from the 32 get the content of the about-file on its STDIN, and the STDOUT from the
33 command will be included verbatim on the about page. Default value: 33 command will be included verbatim on the about page. Default value:
34 none. 34 none.
35 35
36agefile:: 36agefile::
37 Specifies a path, relative to each repository path, which can be used 37 Specifies a path, relative to each repository path, which can be used
38 to specify the date and time of the youngest commit in the repository. 38 to specify the date and time of the youngest commit in the repository.
39 The first line in the file is used as input to the "parse_date" 39 The first line in the file is used as input to the "parse_date"
40 function in libgit. Recommended timestamp-format is "yyyy-mm-dd 40 function in libgit. Recommended timestamp-format is "yyyy-mm-dd
41 hh:mm:ss". Default value: "info/web/last-modified". 41 hh:mm:ss". Default value: "info/web/last-modified".
42 42
43cache-root:: 43cache-root::
44 Path used to store the cgit cache entries. Default value: 44 Path used to store the cgit cache entries. Default value:
45 "/var/cache/cgit". 45 "/var/cache/cgit".
46 46
47cache-dynamic-ttl:: 47cache-dynamic-ttl::
48 Number which specifies the time-to-live, in minutes, for the cached 48 Number which specifies the time-to-live, in minutes, for the cached
49 version of repository pages accessed without a fixed SHA1. Default 49 version of repository pages accessed without a fixed SHA1. Default
50 value: "5". 50 value: "5".
51 51
52cache-repo-ttl:: 52cache-repo-ttl::
53 Number which specifies the time-to-live, in minutes, for the cached 53 Number which specifies the time-to-live, in minutes, for the cached
54 version of the repository summary page. Default value: "5". 54 version of the repository summary page. Default value: "5".
55 55
56cache-root-ttl:: 56cache-root-ttl::
57 Number which specifies the time-to-live, in minutes, for the cached 57 Number which specifies the time-to-live, in minutes, for the cached
58 version of the repository index page. Default value: "5". 58 version of the repository index page. Default value: "5".
59 59
60cache-scanrc-ttl:: 60cache-scanrc-ttl::
61 Number which specifies the time-to-live, in minutes, for the result 61 Number which specifies the time-to-live, in minutes, for the result
62 of scanning a path for git repositories. Default value: "15". 62 of scanning a path for git repositories. Default value: "15".
63 63
64cache-size:: 64cache-size::
65 The maximum number of entries in the cgit cache. Default value: "0" 65 The maximum number of entries in the cgit cache. Default value: "0"
66 (i.e. caching is disabled). 66 (i.e. caching is disabled).
67 67
68cache-static-ttl:: 68cache-static-ttl::
69 Number which specifies the time-to-live, in minutes, for the cached 69 Number which specifies the time-to-live, in minutes, for the cached
70 version of repository pages accessed with a fixed SHA1. Default value: 70 version of repository pages accessed with a fixed SHA1. Default value:
71 "5". 71 "5".
72 72
73clone-prefix:: 73clone-prefix::
74 Space-separated list of common prefixes which, when combined with a 74 Space-separated list of common prefixes which, when combined with a
75 repository url, generates valid clone urls for the repository. This 75 repository url, generates valid clone urls for the repository. This
76 setting is only used if `repo.clone-url` is unspecified. Default value: 76 setting is only used if `repo.clone-url` is unspecified. Default value:
77 none. 77 none.
78 78
79commit-filter:: 79commit-filter::
80 Specifies a command which will be invoked to format commit messages. 80 Specifies a command which will be invoked to format commit messages.
81 The command will get the message on its STDIN, and the STDOUT from the 81 The command will get the message on its STDIN, and the STDOUT from the
82 command will be included verbatim as the commit message, i.e. this can 82 command will be included verbatim as the commit message, i.e. this can
83 be used to implement bugtracker integration. Default value: none. 83 be used to implement bugtracker integration. Default value: none.
84 84
85css:: 85css::
86 Url which specifies the css document to include in all cgit pages. 86 Url which specifies the css document to include in all cgit pages.
87 Default value: "/cgit.css". 87 Default value: "/cgit.css".
88 88
89embedded:: 89embedded::
90 Flag which, when set to "1", will make cgit generate a html fragment 90 Flag which, when set to "1", will make cgit generate a html fragment
91 suitable for embedding in other html pages. Default value: none. See 91 suitable for embedding in other html pages. Default value: none. See
92 also: "noheader". 92 also: "noheader".
93 93
94enable-filter-overrides:: 94enable-filter-overrides::
95 Flag which, when set to "1", allows all filter settings to be 95 Flag which, when set to "1", allows all filter settings to be
96 overridden in repository-specific cgitrc files. Default value: none. 96 overridden in repository-specific cgitrc files. Default value: none.
97 97
98enable-index-links:: 98enable-index-links::
99 Flag which, when set to "1", will make cgit generate extra links for 99 Flag which, when set to "1", will make cgit generate extra links for
100 each repo in the repository index (specifically, to the "summary", 100 each repo in the repository index (specifically, to the "summary",
101 "commit" and "tree" pages). Default value: "0". 101 "commit" and "tree" pages). Default value: "0".
102 102
103enable-log-filecount:: 103enable-log-filecount::
104 Flag which, when set to "1", will make cgit print the number of 104 Flag which, when set to "1", will make cgit print the number of
105 modified files for each commit on the repository log page. Default 105 modified files for each commit on the repository log page. Default
106 value: "0". 106 value: "0".
107 107
108enable-log-linecount:: 108enable-log-linecount::
109 Flag which, when set to "1", will make cgit print the number of added 109 Flag which, when set to "1", will make cgit print the number of added
110 and removed lines for each commit on the repository log page. Default 110 and removed lines for each commit on the repository log page. Default
111 value: "0". 111 value: "0".
112 112
113enable-tree-linenumbers:: 113enable-tree-linenumbers::
114 Flag which, when set to "1", will make cgit generate linenumber links 114 Flag which, when set to "1", will make cgit generate linenumber links
115 for plaintext blobs printed in the tree view. Default value: "1". 115 for plaintext blobs printed in the tree view. Default value: "1".
116 116
117favicon:: 117favicon::
118 Url used as link to a shortcut icon for cgit. If specified, it is 118 Url used as link to a shortcut icon for cgit. If specified, it is
119 suggested to use the value "/favicon.ico" since certain browsers will 119 suggested to use the value "/favicon.ico" since certain browsers will
120 ignore other values. Default value: none. 120 ignore other values. Default value: none.
121 121
122footer:: 122footer::
123 The content of the file specified with this option will be included 123 The content of the file specified with this option will be included
124 verbatim at the bottom of all pages (i.e. it replaces the standard 124 verbatim at the bottom of all pages (i.e. it replaces the standard
125 "generated by..." message. Default value: none. 125 "generated by..." message. Default value: none.
126 126
127head-include:: 127head-include::
128 The content of the file specified with this option will be included 128 The content of the file specified with this option will be included
129 verbatim in the html HEAD section on all pages. Default value: none. 129 verbatim in the html HEAD section on all pages. Default value: none.
130 130
131header:: 131header::
132 The content of the file specified with this option will be included 132 The content of the file specified with this option will be included
133 verbatim at the top of all pages. Default value: none. 133 verbatim at the top of all pages. Default value: none.
134 134
135include:: 135include::
136 Name of a configfile to include before the rest of the current config- 136 Name of a configfile to include before the rest of the current config-
137 file is parsed. Default value: none. 137 file is parsed. Default value: none.
138 138
139index-header:: 139index-header::
140 The content of the file specified with this option will be included 140 The content of the file specified with this option will be included
141 verbatim above the repository index. This setting is deprecated, and 141 verbatim above the repository index. This setting is deprecated, and
142 will not be supported by cgit-1.0 (use root-readme instead). Default 142 will not be supported by cgit-1.0 (use root-readme instead). Default
143 value: none. 143 value: none.
144 144
145index-info:: 145index-info::
146 The content of the file specified with this option will be included 146 The content of the file specified with this option will be included
147 verbatim below the heading on the repository index page. This setting 147 verbatim below the heading on the repository index page. This setting
148 is deprecated, and will not be supported by cgit-1.0 (use root-desc 148 is deprecated, and will not be supported by cgit-1.0 (use root-desc
149 instead). Default value: none. 149 instead). Default value: none.
150 150
151local-time:: 151local-time::
152 Flag which, if set to "1", makes cgit print commit and tag times in the 152 Flag which, if set to "1", makes cgit print commit and tag times in the
153 servers timezone. Default value: "0". 153 servers timezone. Default value: "0".
154 154
155logo:: 155logo::
156 Url which specifies the source of an image which will be used as a logo 156 Url which specifies the source of an image which will be used as a logo
157 on all cgit pages. Default value: "/cgit.png". 157 on all cgit pages. Default value: "/cgit.png".
158 158
159logo-link:: 159logo-link::
160 Url loaded when clicking on the cgit logo image. If unspecified the 160 Url loaded when clicking on the cgit logo image. If unspecified the
161 calculated url of the repository index page will be used. Default 161 calculated url of the repository index page will be used. Default
162 value: none. 162 value: none.
163 163
164max-commit-count:: 164max-commit-count::
165 Specifies the number of entries to list per page in "log" view. Default 165 Specifies the number of entries to list per page in "log" view. Default
166 value: "50". 166 value: "50".
167 167
168max-message-length:: 168max-message-length::
169 Specifies the maximum number of commit message characters to display in 169 Specifies the maximum number of commit message characters to display in
170 "log" view. Default value: "80". 170 "log" view. Default value: "80".
171 171
172max-repo-count:: 172max-repo-count::
173 Specifies the number of entries to list per page on therepository 173 Specifies the number of entries to list per page on therepository
174 index page. Default value: "50". 174 index page. Default value: "50".
175 175
176max-repodesc-length:: 176max-repodesc-length::
177 Specifies the maximum number of repo description characters to display 177 Specifies the maximum number of repo description characters to display
178 on the repository index page. Default value: "80". 178 on the repository index page. Default value: "80".
179 179
180max-blob-size:: 180max-blob-size::
181 Specifies the maximum size of a blob to display HTML for in KBytes. 181 Specifies the maximum size of a blob to display HTML for in KBytes.
182 Default value: "0" (limit disabled). 182 Default value: "0" (limit disabled).
183 183
184max-stats:: 184max-stats::
185 Set the default maximum statistics period. Valid values are "week", 185 Set the default maximum statistics period. Valid values are "week",
186 "month", "quarter" and "year". If unspecified, statistics are 186 "month", "quarter" and "year". If unspecified, statistics are
187 disabled. Default value: none. See also: "repo.max-stats". 187 disabled. Default value: none. See also: "repo.max-stats".
188 188
189mimetype.<ext>:: 189mimetype.<ext>::
190 Set the mimetype for the specified filename extension. This is used 190 Set the mimetype for the specified filename extension. This is used
191 by the `plain` command when returning blob content. 191 by the `plain` command when returning blob content.
192 192
193module-link:: 193module-link::
194 Text which will be used as the formatstring for a hyperlink when a 194 Text which will be used as the formatstring for a hyperlink when a
195 submodule is printed in a directory listing. The arguments for the 195 submodule is printed in a directory listing. The arguments for the
196 formatstring are the path and SHA1 of the submodule commit. Default 196 formatstring are the path and SHA1 of the submodule commit. Default
197 value: "./?repo=%s&page=commit&id=%s" 197 value: "./?repo=%s&page=commit&id=%s"
198 198
199nocache:: 199nocache::
200 If set to the value "1" caching will be disabled. This settings is 200 If set to the value "1" caching will be disabled. This settings is
201 deprecated, and will not be honored starting with cgit-1.0. Default 201 deprecated, and will not be honored starting with cgit-1.0. Default
202 value: "0". 202 value: "0".
203 203
204noplainemail:: 204noplainemail::
205 If set to "1" showing full author email adresses will be disabled. 205 If set to "1" showing full author email adresses will be disabled.
206 Default value: "0". 206 Default value: "0".
207 207
208noheader:: 208noheader::
209 Flag which, when set to "1", will make cgit omit the standard header 209 Flag which, when set to "1", will make cgit omit the standard header
210 on all pages. Default value: none. See also: "embedded". 210 on all pages. Default value: none. See also: "embedded".
211 211
212renamelimit:: 212renamelimit::
213 Maximum number of files to consider when detecting renames. The value 213 Maximum number of files to consider when detecting renames. The value
214 "-1" uses the compiletime value in git (for further info, look at 214 "-1" uses the compiletime value in git (for further info, look at
215 `man git-diff`). Default value: "-1". 215 `man git-diff`). Default value: "-1".
216 216
217repo.group:: 217repo.group::
218 Legacy alias for "section". This option is deprecated and will not be 218 Legacy alias for "section". This option is deprecated and will not be
219 supported in cgit-1.0. 219 supported in cgit-1.0.
220 220
221robots:: 221robots::
222 Text used as content for the "robots" meta-tag. Default value: 222 Text used as content for the "robots" meta-tag. Default value:
223 "index, nofollow". 223 "index, nofollow".
224 224
225root-desc:: 225root-desc::
226 Text printed below the heading on the repository index page. Default 226 Text printed below the heading on the repository index page. Default
227 value: "a fast webinterface for the git dscm". 227 value: "a fast webinterface for the git dscm".
228 228
229root-readme:: 229root-readme::
230 The content of the file specified with this option will be included 230 The content of the file specified with this option will be included
231 verbatim below the "about" link on the repository index page. Default 231 verbatim below the "about" link on the repository index page. Default
232 value: none. 232 value: none.
233 233
234root-title:: 234root-title::
235 Text printed as heading on the repository index page. Default value: 235 Text printed as heading on the repository index page. Default value:
236 "Git Repository Browser". 236 "Git Repository Browser".
237 237
238scan-path:: 238scan-path::
239 A path which will be scanned for repositories. If caching is enabled, 239 A path which will be scanned for repositories. If caching is enabled,
240 the result will be cached as a cgitrc include-file in the cache 240 the result will be cached as a cgitrc include-file in the cache
241 directory. Default value: none. See also: cache-scanrc-ttl. 241 directory. Default value: none. See also: cache-scanrc-ttl.
242 242
243section:: 243section::
244 The name of the current repository section - all repositories defined 244 The name of the current repository section - all repositories defined
245 after this option will inherit the current section name. Default value: 245 after this option will inherit the current section name. Default value:
246 none. 246 none.
247 247
248side-by-side-diffs::
249 If set to "1" shows side-by-side diffs instead of unidiffs per
250 default. Default value: "0".
251
248snapshots:: 252snapshots::
249 Text which specifies the default set of snapshot formats generated by 253 Text which specifies the default set of snapshot formats generated by
250 cgit. The value is a space-separated list of zero or more of the 254 cgit. The value is a space-separated list of zero or more of the
251 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none. 255 values "tar", "tar.gz", "tar.bz2" and "zip". Default value: none.
252 256
253source-filter:: 257source-filter::
254 Specifies a command which will be invoked to format plaintext blobs 258 Specifies a command which will be invoked to format plaintext blobs
255 in the tree view. The command will get the blob content on its STDIN 259 in the tree view. The command will get the blob content on its STDIN
256 and the name of the blob as its only command line argument. The STDOUT 260 and the name of the blob as its only command line argument. The STDOUT
257 from the command will be included verbatim as the blob contents, i.e. 261 from the command will be included verbatim as the blob contents, i.e.
258 this can be used to implement e.g. syntax highlighting. Default value: 262 this can be used to implement e.g. syntax highlighting. Default value:
259 none. 263 none.
260 264
261summary-branches:: 265summary-branches::
262 Specifies the number of branches to display in the repository "summary" 266 Specifies the number of branches to display in the repository "summary"
263 view. Default value: "10". 267 view. Default value: "10".
264 268
265summary-log:: 269summary-log::
266 Specifies the number of log entries to display in the repository 270 Specifies the number of log entries to display in the repository
267 "summary" view. Default value: "10". 271 "summary" view. Default value: "10".
268 272
269summary-tags:: 273summary-tags::
270 Specifies the number of tags to display in the repository "summary" 274 Specifies the number of tags to display in the repository "summary"
271 view. Default value: "10". 275 view. Default value: "10".
272 276
273virtual-root:: 277virtual-root::
274 Url which, if specified, will be used as root for all cgit links. It 278 Url which, if specified, will be used as root for all cgit links. It
275 will also cause cgit to generate 'virtual urls', i.e. urls like 279 will also cause cgit to generate 'virtual urls', i.e. urls like
276 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default 280 '/cgit/tree/README' as opposed to '?r=cgit&p=tree&path=README'. Default
277 value: none. 281 value: none.
278 NOTE: cgit has recently learned how to use PATH_INFO to achieve the 282 NOTE: cgit has recently learned how to use PATH_INFO to achieve the
279 same kind of virtual urls, so this option will probably be deprecated. 283 same kind of virtual urls, so this option will probably be deprecated.
280 284
281REPOSITORY SETTINGS 285REPOSITORY SETTINGS
282------------------- 286-------------------
283repo.about-filter:: 287repo.about-filter::
284 Override the default about-filter. Default value: none. See also: 288 Override the default about-filter. Default value: none. See also:
285 "enable-filter-overrides". 289 "enable-filter-overrides".
286 290
287repo.clone-url:: 291repo.clone-url::
288 A list of space-separated urls which can be used to clone this repo. 292 A list of space-separated urls which can be used to clone this repo.
289 Default value: none. 293 Default value: none.
290 294
291repo.commit-filter:: 295repo.commit-filter::
292 Override the default commit-filter. Default value: none. See also: 296 Override the default commit-filter. Default value: none. See also:
293 "enable-filter-overrides". 297 "enable-filter-overrides".
294 298
295repo.defbranch:: 299repo.defbranch::
296 The name of the default branch for this repository. If no such branch 300 The name of the default branch for this repository. If no such branch
297 exists in the repository, the first branch name (when sorted) is used 301 exists in the repository, the first branch name (when sorted) is used
298 as default instead. Default value: "master". 302 as default instead. Default value: "master".
299 303
300repo.desc:: 304repo.desc::
301 The value to show as repository description. Default value: none. 305 The value to show as repository description. Default value: none.
302 306
303repo.enable-log-filecount:: 307repo.enable-log-filecount::
304 A flag which can be used to disable the global setting 308 A flag which can be used to disable the global setting
305 `enable-log-filecount'. Default value: none. 309 `enable-log-filecount'. Default value: none.
306 310
307repo.enable-log-linecount:: 311repo.enable-log-linecount::
308 A flag which can be used to disable the global setting 312 A flag which can be used to disable the global setting
309 `enable-log-linecount'. Default value: none. 313 `enable-log-linecount'. Default value: none.
310 314
311repo.max-stats:: 315repo.max-stats::
312 Override the default maximum statistics period. Valid values are equal 316 Override the default maximum statistics period. Valid values are equal
313 to the values specified for the global "max-stats" setting. Default 317 to the values specified for the global "max-stats" setting. Default
314 value: none. 318 value: none.
315 319
316repo.name:: 320repo.name::
317 The value to show as repository name. Default value: <repo.url>. 321 The value to show as repository name. Default value: <repo.url>.
318 322
319repo.owner:: 323repo.owner::
320 A value used to identify the owner of the repository. Default value: 324 A value used to identify the owner of the repository. Default value:
321 none. 325 none.
322 326
323repo.path:: 327repo.path::
324 An absolute path to the repository directory. For non-bare repositories 328 An absolute path to the repository directory. For non-bare repositories
325 this is the .git-directory. Default value: none. 329 this is the .git-directory. Default value: none.
326 330
327repo.readme:: 331repo.readme::
328 A path (relative to <repo.path>) which specifies a file to include 332 A path (relative to <repo.path>) which specifies a file to include
329 verbatim as the "About" page for this repo. Default value: none. 333 verbatim as the "About" page for this repo. Default value: none.
330 334
331repo.snapshots:: 335repo.snapshots::
332 A mask of allowed snapshot-formats for this repo, restricted by the 336 A mask of allowed snapshot-formats for this repo, restricted by the
333 "snapshots" global setting. Default value: <snapshots>. 337 "snapshots" global setting. Default value: <snapshots>.
334 338
335repo.section:: 339repo.section::
336 Override the current section name for this repository. Default value: 340 Override the current section name for this repository. Default value:
337 none. 341 none.
338 342
339repo.source-filter:: 343repo.source-filter::
340 Override the default source-filter. Default value: none. See also: 344 Override the default source-filter. Default value: none. See also:
341 "enable-filter-overrides". 345 "enable-filter-overrides".
342 346
343repo.url:: 347repo.url::
344 The relative url used to access the repository. This must be the first 348 The relative url used to access the repository. This must be the first
345 setting specified for each repo. Default value: none. 349 setting specified for each repo. Default value: none.
346 350
347 351
348REPOSITORY-SPECIFIC CGITRC FILE 352REPOSITORY-SPECIFIC CGITRC FILE
349------------------------------- 353-------------------------------
350When the option "scan-path" is used to auto-discover git repositories, cgit 354When the option "scan-path" is used to auto-discover git repositories, cgit
351will try to parse the file "cgitrc" within any found repository. Such a 355will try to parse the file "cgitrc" within any found repository. Such a
352repo-specific config file may contain any of the repo-specific options 356repo-specific config file may contain any of the repo-specific options
353described above, except "repo.url" and "repo.path". Additionally, the "filter" 357described above, except "repo.url" and "repo.path". Additionally, the "filter"
354options are only acknowledged in repo-specific config files when 358options are only acknowledged in repo-specific config files when
355"enable-filter-overrides" is set to "1". 359"enable-filter-overrides" is set to "1".
356 360
357Note: the "repo." prefix is dropped from the option names in repo-specific 361Note: the "repo." prefix is dropped from the option names in repo-specific
358config files, e.g. "repo.desc" becomes "desc". 362config files, e.g. "repo.desc" becomes "desc".
359 363
360 364
361EXAMPLE CGITRC FILE 365EXAMPLE CGITRC FILE
362------------------- 366-------------------
363 367
364.... 368....
365# Enable caching of up to 1000 output entriess 369# Enable caching of up to 1000 output entriess
366cache-size=1000 370cache-size=1000
367 371
368 372
369# Specify some default clone prefixes 373# Specify some default clone prefixes
370clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git 374clone-prefix=git://foobar.com ssh://foobar.com/pub/git http://foobar.com/git
371 375
372# Specify the css url 376# Specify the css url
373css=/css/cgit.css 377css=/css/cgit.css
374 378
375 379
376# Show extra links for each repository on the index page 380# Show extra links for each repository on the index page
377enable-index-links=1 381enable-index-links=1
378 382
379 383
380# Show number of affected files per commit on the log pages 384# Show number of affected files per commit on the log pages
381enable-log-filecount=1 385enable-log-filecount=1
382 386
383 387
384# Show number of added/removed lines per commit on the log pages 388# Show number of added/removed lines per commit on the log pages
385enable-log-linecount=1 389enable-log-linecount=1
386 390
387 391
388# Add a cgit favicon 392# Add a cgit favicon
389favicon=/favicon.ico 393favicon=/favicon.ico
390 394
391 395
392# Use a custom logo 396# Use a custom logo
393logo=/img/mylogo.png 397logo=/img/mylogo.png
394 398
395 399
396# Enable statistics per week, month and quarter 400# Enable statistics per week, month and quarter
397max-stats=quarter 401max-stats=quarter
398 402
399 403
400# Set the title and heading of the repository index page 404# Set the title and heading of the repository index page
401root-title=foobar.com git repositories 405root-title=foobar.com git repositories
402 406
403 407
404# Set a subheading for the repository index page 408# Set a subheading for the repository index page
405root-desc=tracking the foobar development 409root-desc=tracking the foobar development
406 410
407 411
408# Include some more info about foobar.com on the index page 412# Include some more info about foobar.com on the index page
409root-readme=/var/www/htdocs/about.html 413root-readme=/var/www/htdocs/about.html
410 414
411 415
412# Allow download of tar.gz, tar.bz2 and zip-files 416# Allow download of tar.gz, tar.bz2 and zip-files
413snapshots=tar.gz tar.bz2 zip 417snapshots=tar.gz tar.bz2 zip
414 418
415 419
416## 420##
417## List of common mimetypes 421## List of common mimetypes
418## 422##
419 423
420mimetype.git=image/git 424mimetype.git=image/git
421mimetype.html=text/html 425mimetype.html=text/html
422mimetype.jpg=image/jpeg 426mimetype.jpg=image/jpeg
423mimetype.jpeg=image/jpeg 427mimetype.jpeg=image/jpeg
424mimetype.pdf=application/pdf 428mimetype.pdf=application/pdf
425mimetype.png=image/png 429mimetype.png=image/png
426mimetype.svg=image/svg+xml 430mimetype.svg=image/svg+xml
427 431
428 432
429## 433##
430## List of repositories. 434## List of repositories.
431## PS: Any repositories listed when section is unset will not be 435## PS: Any repositories listed when section is unset will not be
432## displayed under a section heading 436## displayed under a section heading
433## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos') 437## PPS: This list could be kept in a different file (e.g. '/etc/cgitrepos')
434## and included like this: 438## and included like this:
435## include=/etc/cgitrepos 439## include=/etc/cgitrepos
436## 440##
437 441
438 442
439repo.url=foo 443repo.url=foo
440repo.path=/pub/git/foo.git 444repo.path=/pub/git/foo.git
441repo.desc=the master foo repository 445repo.desc=the master foo repository
442repo.owner=fooman@foobar.com 446repo.owner=fooman@foobar.com
443repo.readme=info/web/about.html 447repo.readme=info/web/about.html
444 448
445 449
446repo.url=bar 450repo.url=bar
447repo.path=/pub/git/bar.git 451repo.path=/pub/git/bar.git
448repo.desc=the bars for your foo 452repo.desc=the bars for your foo
449repo.owner=barman@foobar.com 453repo.owner=barman@foobar.com
450repo.readme=info/web/about.html 454repo.readme=info/web/about.html
451 455
452 456
453# The next repositories will be displayed under the 'extras' heading 457# The next repositories will be displayed under the 'extras' heading
454section=extras 458section=extras
455 459
456 460
457repo.url=baz 461repo.url=baz
458repo.path=/pub/git/baz.git 462repo.path=/pub/git/baz.git
459repo.desc=a set of extensions for bar users 463repo.desc=a set of extensions for bar users
460 464
461repo.url=wiz 465repo.url=wiz
462repo.path=/pub/git/wiz.git 466repo.path=/pub/git/wiz.git
463repo.desc=the wizard of foo 467repo.desc=the wizard of foo
464 468
465 469
466# Add some mirrored repositories 470# Add some mirrored repositories
467section=mirrors 471section=mirrors
468 472
469 473
470repo.url=git 474repo.url=git
471repo.path=/pub/git/git.git 475repo.path=/pub/git/git.git
472repo.desc=the dscm 476repo.desc=the dscm
473 477
474 478
475repo.url=linux 479repo.url=linux
476repo.path=/pub/git/linux.git 480repo.path=/pub/git/linux.git
477repo.desc=the kernel 481repo.desc=the kernel
478 482
479# Disable adhoc downloads of this repo 483# Disable adhoc downloads of this repo
480repo.snapshots=0 484repo.snapshots=0
481 485
482# Disable line-counts for this repo 486# Disable line-counts for this repo
483repo.enable-log-linecount=0 487repo.enable-log-linecount=0
484 488
485# Restrict the max statistics period for this repo 489# Restrict the max statistics period for this repo
486repo.max-stats=month 490repo.max-stats=month
487.... 491....
488 492
489 493
490BUGS 494BUGS
491---- 495----
492Comments currently cannot appear on the same line as a setting; the comment 496Comments currently cannot appear on the same line as a setting; the comment
493will be included as part of the value. E.g. this line: 497will be included as part of the value. E.g. this line:
494 498
495 robots=index # allow indexing 499 robots=index # allow indexing
496 500
497will generate the following html element: 501will generate the following html element:
498 502
499 <meta name='robots' content='index # allow indexing'/> 503 <meta name='robots' content='index # allow indexing'/>
500 504
501 505
502 506
503AUTHOR 507AUTHOR
504------ 508------
505Lars Hjemli <hjemli@gmail.com> 509Lars Hjemli <hjemli@gmail.com>
diff --git a/ui-commit.c b/ui-commit.c
index f5b0ae5..b5e3c01 100644
--- a/ui-commit.c
+++ b/ui-commit.c
@@ -1,118 +1,123 @@
1/* ui-commit.c: generate commit view 1/* ui-commit.c: generate commit view
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12#include "ui-diff.h" 12#include "ui-diff.h"
13#include "ui-log.h" 13#include "ui-log.h"
14 14
15void cgit_print_commit(char *hex) 15void cgit_print_commit(char *hex)
16{ 16{
17 struct commit *commit, *parent; 17 struct commit *commit, *parent;
18 struct commitinfo *info; 18 struct commitinfo *info;
19 struct commit_list *p; 19 struct commit_list *p;
20 unsigned char sha1[20]; 20 unsigned char sha1[20];
21 char *tmp; 21 char *tmp;
22 int parents = 0; 22 int parents = 0;
23 23
24 if (!hex) 24 if (!hex)
25 hex = ctx.qry.head; 25 hex = ctx.qry.head;
26 26
27 if (get_sha1(hex, sha1)) { 27 if (get_sha1(hex, sha1)) {
28 cgit_print_error(fmt("Bad object id: %s", hex)); 28 cgit_print_error(fmt("Bad object id: %s", hex));
29 return; 29 return;
30 } 30 }
31 commit = lookup_commit_reference(sha1); 31 commit = lookup_commit_reference(sha1);
32 if (!commit) { 32 if (!commit) {
33 cgit_print_error(fmt("Bad commit reference: %s", hex)); 33 cgit_print_error(fmt("Bad commit reference: %s", hex));
34 return; 34 return;
35 } 35 }
36 info = cgit_parse_commit(commit); 36 info = cgit_parse_commit(commit);
37 37
38 load_ref_decorations(DECORATE_FULL_REFS); 38 load_ref_decorations(DECORATE_FULL_REFS);
39 39
40 html("<table summary='commit info' class='commit-info'>\n"); 40 html("<table summary='commit info' class='commit-info'>\n");
41 html("<tr><th>author</th><td>"); 41 html("<tr><th>author</th><td>");
42 html_txt(info->author); 42 html_txt(info->author);
43 if (!ctx.cfg.noplainemail) { 43 if (!ctx.cfg.noplainemail) {
44 html(" "); 44 html(" ");
45 html_txt(info->author_email); 45 html_txt(info->author_email);
46 } 46 }
47 html("</td><td class='right'>"); 47 html("</td><td class='right'>");
48 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time); 48 cgit_print_date(info->author_date, FMT_LONGDATE, ctx.cfg.local_time);
49 html("</td></tr>\n"); 49 html("</td></tr>\n");
50 html("<tr><th>committer</th><td>"); 50 html("<tr><th>committer</th><td>");
51 html_txt(info->committer); 51 html_txt(info->committer);
52 if (!ctx.cfg.noplainemail) { 52 if (!ctx.cfg.noplainemail) {
53 html(" "); 53 html(" ");
54 html_txt(info->committer_email); 54 html_txt(info->committer_email);
55 } 55 }
56 html("</td><td class='right'>"); 56 html("</td><td class='right'>");
57 cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time); 57 cgit_print_date(info->committer_date, FMT_LONGDATE, ctx.cfg.local_time);
58 html("</td></tr>\n"); 58 html("</td></tr>\n");
59 html("<tr><th>commit</th><td colspan='2' class='sha1'>"); 59 html("<tr><th>commit</th><td colspan='2' class='sha1'>");
60 tmp = sha1_to_hex(commit->object.sha1); 60 tmp = sha1_to_hex(commit->object.sha1);
61 cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp); 61 cgit_commit_link(tmp, NULL, NULL, ctx.qry.head, tmp, 0);
62 html(" ("); 62 html(" (");
63 cgit_patch_link("patch", NULL, NULL, NULL, tmp); 63 cgit_patch_link("patch", NULL, NULL, NULL, tmp);
64 html(") (");
65 if ((ctx.qry.ssdiff && !ctx.cfg.ssdiff) || (!ctx.qry.ssdiff && ctx.cfg.ssdiff))
66 cgit_commit_link("unidiff", NULL, NULL, ctx.qry.head, tmp, 1);
67 else
68 cgit_commit_link("side-by-side diff", NULL, NULL, ctx.qry.head, tmp, 1);
64 html(")</td></tr>\n"); 69 html(")</td></tr>\n");
65 html("<tr><th>tree</th><td colspan='2' class='sha1'>"); 70 html("<tr><th>tree</th><td colspan='2' class='sha1'>");
66 tmp = xstrdup(hex); 71 tmp = xstrdup(hex);
67 cgit_tree_link(sha1_to_hex(commit->tree->object.sha1), NULL, NULL, 72 cgit_tree_link(sha1_to_hex(commit->tree->object.sha1), NULL, NULL,
68 ctx.qry.head, tmp, NULL); 73 ctx.qry.head, tmp, NULL);
69 html("</td></tr>\n"); 74 html("</td></tr>\n");
70 for (p = commit->parents; p ; p = p->next) { 75 for (p = commit->parents; p ; p = p->next) {
71 parent = lookup_commit_reference(p->item->object.sha1); 76 parent = lookup_commit_reference(p->item->object.sha1);
72 if (!parent) { 77 if (!parent) {
73 html("<tr><td colspan='3'>"); 78 html("<tr><td colspan='3'>");
74 cgit_print_error("Error reading parent commit"); 79 cgit_print_error("Error reading parent commit");
75 html("</td></tr>"); 80 html("</td></tr>");
76 continue; 81 continue;
77 } 82 }
78 html("<tr><th>parent</th>" 83 html("<tr><th>parent</th>"
79 "<td colspan='2' class='sha1'>"); 84 "<td colspan='2' class='sha1'>");
80 cgit_commit_link(sha1_to_hex(p->item->object.sha1), NULL, NULL, 85 cgit_commit_link(sha1_to_hex(p->item->object.sha1), NULL, NULL,
81 ctx.qry.head, sha1_to_hex(p->item->object.sha1)); 86 ctx.qry.head, sha1_to_hex(p->item->object.sha1), 0);
82 html(" ("); 87 html(" (");
83 cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex, 88 cgit_diff_link("diff", NULL, NULL, ctx.qry.head, hex,
84 sha1_to_hex(p->item->object.sha1), NULL); 89 sha1_to_hex(p->item->object.sha1), NULL, 0);
85 html(")</td></tr>"); 90 html(")</td></tr>");
86 parents++; 91 parents++;
87 } 92 }
88 if (ctx.repo->snapshots) { 93 if (ctx.repo->snapshots) {
89 html("<tr><th>download</th><td colspan='2' class='sha1'>"); 94 html("<tr><th>download</th><td colspan='2' class='sha1'>");
90 cgit_print_snapshot_links(ctx.qry.repo, ctx.qry.head, 95 cgit_print_snapshot_links(ctx.qry.repo, ctx.qry.head,
91 hex, ctx.repo->snapshots); 96 hex, ctx.repo->snapshots);
92 html("</td></tr>"); 97 html("</td></tr>");
93 } 98 }
94 html("</table>\n"); 99 html("</table>\n");
95 html("<div class='commit-subject'>"); 100 html("<div class='commit-subject'>");
96 if (ctx.repo->commit_filter) 101 if (ctx.repo->commit_filter)
97 cgit_open_filter(ctx.repo->commit_filter); 102 cgit_open_filter(ctx.repo->commit_filter);
98 html_txt(info->subject); 103 html_txt(info->subject);
99 if (ctx.repo->commit_filter) 104 if (ctx.repo->commit_filter)
100 cgit_close_filter(ctx.repo->commit_filter); 105 cgit_close_filter(ctx.repo->commit_filter);
101 show_commit_decorations(commit); 106 show_commit_decorations(commit);
102 html("</div>"); 107 html("</div>");
103 html("<div class='commit-msg'>"); 108 html("<div class='commit-msg'>");
104 if (ctx.repo->commit_filter) 109 if (ctx.repo->commit_filter)
105 cgit_open_filter(ctx.repo->commit_filter); 110 cgit_open_filter(ctx.repo->commit_filter);
106 html_txt(info->msg); 111 html_txt(info->msg);
107 if (ctx.repo->commit_filter) 112 if (ctx.repo->commit_filter)
108 cgit_close_filter(ctx.repo->commit_filter); 113 cgit_close_filter(ctx.repo->commit_filter);
109 html("</div>"); 114 html("</div>");
110 if (parents < 3) { 115 if (parents < 3) {
111 if (parents) 116 if (parents)
112 tmp = sha1_to_hex(commit->parents->item->object.sha1); 117 tmp = sha1_to_hex(commit->parents->item->object.sha1);
113 else 118 else
114 tmp = NULL; 119 tmp = NULL;
115 cgit_print_diff(ctx.qry.sha1, tmp, NULL); 120 cgit_print_diff(ctx.qry.sha1, tmp, NULL);
116 } 121 }
117 cgit_free_commitinfo(info); 122 cgit_free_commitinfo(info);
118} 123}
diff --git a/ui-diff.c b/ui-diff.c
index 2196745..a92a768 100644
--- a/ui-diff.c
+++ b/ui-diff.c
@@ -1,313 +1,353 @@
1/* ui-diff.c: show diff between two blobs 1/* ui-diff.c: show diff between two blobs
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12#include "ui-ssdiff.h"
12 13
13unsigned char old_rev_sha1[20]; 14unsigned char old_rev_sha1[20];
14unsigned char new_rev_sha1[20]; 15unsigned char new_rev_sha1[20];
15 16
16static int files, slots; 17static int files, slots;
17static int total_adds, total_rems, max_changes; 18static int total_adds, total_rems, max_changes;
18static int lines_added, lines_removed; 19static int lines_added, lines_removed;
19 20
20static struct fileinfo { 21static struct fileinfo {
21 char status; 22 char status;
22 unsigned char old_sha1[20]; 23 unsigned char old_sha1[20];
23 unsigned char new_sha1[20]; 24 unsigned char new_sha1[20];
24 unsigned short old_mode; 25 unsigned short old_mode;
25 unsigned short new_mode; 26 unsigned short new_mode;
26 char *old_path; 27 char *old_path;
27 char *new_path; 28 char *new_path;
28 unsigned int added; 29 unsigned int added;
29 unsigned int removed; 30 unsigned int removed;
30 unsigned long old_size; 31 unsigned long old_size;
31 unsigned long new_size; 32 unsigned long new_size;
32 int binary:1; 33 int binary:1;
33} *items; 34} *items;
34 35
36static int use_ssdiff = 0;
35 37
36static void print_fileinfo(struct fileinfo *info) 38static void print_fileinfo(struct fileinfo *info)
37{ 39{
38 char *class; 40 char *class;
39 41
40 switch (info->status) { 42 switch (info->status) {
41 case DIFF_STATUS_ADDED: 43 case DIFF_STATUS_ADDED:
42 class = "add"; 44 class = "add";
43 break; 45 break;
44 case DIFF_STATUS_COPIED: 46 case DIFF_STATUS_COPIED:
45 class = "cpy"; 47 class = "cpy";
46 break; 48 break;
47 case DIFF_STATUS_DELETED: 49 case DIFF_STATUS_DELETED:
48 class = "del"; 50 class = "del";
49 break; 51 break;
50 case DIFF_STATUS_MODIFIED: 52 case DIFF_STATUS_MODIFIED:
51 class = "upd"; 53 class = "upd";
52 break; 54 break;
53 case DIFF_STATUS_RENAMED: 55 case DIFF_STATUS_RENAMED:
54 class = "mov"; 56 class = "mov";
55 break; 57 break;
56 case DIFF_STATUS_TYPE_CHANGED: 58 case DIFF_STATUS_TYPE_CHANGED:
57 class = "typ"; 59 class = "typ";
58 break; 60 break;
59 case DIFF_STATUS_UNKNOWN: 61 case DIFF_STATUS_UNKNOWN:
60 class = "unk"; 62 class = "unk";
61 break; 63 break;
62 case DIFF_STATUS_UNMERGED: 64 case DIFF_STATUS_UNMERGED:
63 class = "stg"; 65 class = "stg";
64 break; 66 break;
65 default: 67 default:
66 die("bug: unhandled diff status %c", info->status); 68 die("bug: unhandled diff status %c", info->status);
67 } 69 }
68 70
69 html("<tr>"); 71 html("<tr>");
70 htmlf("<td class='mode'>"); 72 htmlf("<td class='mode'>");
71 if (is_null_sha1(info->new_sha1)) { 73 if (is_null_sha1(info->new_sha1)) {
72 cgit_print_filemode(info->old_mode); 74 cgit_print_filemode(info->old_mode);
73 } else { 75 } else {
74 cgit_print_filemode(info->new_mode); 76 cgit_print_filemode(info->new_mode);
75 } 77 }
76 78
77 if (info->old_mode != info->new_mode && 79 if (info->old_mode != info->new_mode &&
78 !is_null_sha1(info->old_sha1) && 80 !is_null_sha1(info->old_sha1) &&
79 !is_null_sha1(info->new_sha1)) { 81 !is_null_sha1(info->new_sha1)) {
80 html("<span class='modechange'>["); 82 html("<span class='modechange'>[");
81 cgit_print_filemode(info->old_mode); 83 cgit_print_filemode(info->old_mode);
82 html("]</span>"); 84 html("]</span>");
83 } 85 }
84 htmlf("</td><td class='%s'>", class); 86 htmlf("</td><td class='%s'>", class);
85 cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1, 87 cgit_diff_link(info->new_path, NULL, NULL, ctx.qry.head, ctx.qry.sha1,
86 ctx.qry.sha2, info->new_path); 88 ctx.qry.sha2, info->new_path, 0);
87 if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED) 89 if (info->status == DIFF_STATUS_COPIED || info->status == DIFF_STATUS_RENAMED)
88 htmlf(" (%s from %s)", 90 htmlf(" (%s from %s)",
89 info->status == DIFF_STATUS_COPIED ? "copied" : "renamed", 91 info->status == DIFF_STATUS_COPIED ? "copied" : "renamed",
90 info->old_path); 92 info->old_path);
91 html("</td><td class='right'>"); 93 html("</td><td class='right'>");
92 if (info->binary) { 94 if (info->binary) {
93 htmlf("bin</td><td class='graph'>%d -> %d bytes", 95 htmlf("bin</td><td class='graph'>%d -> %d bytes",
94 info->old_size, info->new_size); 96 info->old_size, info->new_size);
95 return; 97 return;
96 } 98 }
97 htmlf("%d", info->added + info->removed); 99 htmlf("%d", info->added + info->removed);
98 html("</td><td class='graph'>"); 100 html("</td><td class='graph'>");
99 htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes)); 101 htmlf("<table summary='file diffstat' width='%d%%'><tr>", (max_changes > 100 ? 100 : max_changes));
100 htmlf("<td class='add' style='width: %.1f%%;'/>", 102 htmlf("<td class='add' style='width: %.1f%%;'/>",
101 info->added * 100.0 / max_changes); 103 info->added * 100.0 / max_changes);
102 htmlf("<td class='rem' style='width: %.1f%%;'/>", 104 htmlf("<td class='rem' style='width: %.1f%%;'/>",
103 info->removed * 100.0 / max_changes); 105 info->removed * 100.0 / max_changes);
104 htmlf("<td class='none' style='width: %.1f%%;'/>", 106 htmlf("<td class='none' style='width: %.1f%%;'/>",
105 (max_changes - info->removed - info->added) * 100.0 / max_changes); 107 (max_changes - info->removed - info->added) * 100.0 / max_changes);
106 html("</tr></table></td></tr>\n"); 108 html("</tr></table></td></tr>\n");
107} 109}
108 110
109static void count_diff_lines(char *line, int len) 111static void count_diff_lines(char *line, int len)
110{ 112{
111 if (line && (len > 0)) { 113 if (line && (len > 0)) {
112 if (line[0] == '+') 114 if (line[0] == '+')
113 lines_added++; 115 lines_added++;
114 else if (line[0] == '-') 116 else if (line[0] == '-')
115 lines_removed++; 117 lines_removed++;
116 } 118 }
117} 119}
118 120
119static void inspect_filepair(struct diff_filepair *pair) 121static void inspect_filepair(struct diff_filepair *pair)
120{ 122{
121 int binary = 0; 123 int binary = 0;
122 unsigned long old_size = 0; 124 unsigned long old_size = 0;
123 unsigned long new_size = 0; 125 unsigned long new_size = 0;
124 files++; 126 files++;
125 lines_added = 0; 127 lines_added = 0;
126 lines_removed = 0; 128 lines_removed = 0;
127 cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size, &new_size, 129 cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size, &new_size,
128 &binary, count_diff_lines); 130 &binary, count_diff_lines);
129 if (files >= slots) { 131 if (files >= slots) {
130 if (slots == 0) 132 if (slots == 0)
131 slots = 4; 133 slots = 4;
132 else 134 else
133 slots = slots * 2; 135 slots = slots * 2;
134 items = xrealloc(items, slots * sizeof(struct fileinfo)); 136 items = xrealloc(items, slots * sizeof(struct fileinfo));
135 } 137 }
136 items[files-1].status = pair->status; 138 items[files-1].status = pair->status;
137 hashcpy(items[files-1].old_sha1, pair->one->sha1); 139 hashcpy(items[files-1].old_sha1, pair->one->sha1);
138 hashcpy(items[files-1].new_sha1, pair->two->sha1); 140 hashcpy(items[files-1].new_sha1, pair->two->sha1);
139 items[files-1].old_mode = pair->one->mode; 141 items[files-1].old_mode = pair->one->mode;
140 items[files-1].new_mode = pair->two->mode; 142 items[files-1].new_mode = pair->two->mode;
141 items[files-1].old_path = xstrdup(pair->one->path); 143 items[files-1].old_path = xstrdup(pair->one->path);
142 items[files-1].new_path = xstrdup(pair->two->path); 144 items[files-1].new_path = xstrdup(pair->two->path);
143 items[files-1].added = lines_added; 145 items[files-1].added = lines_added;
144 items[files-1].removed = lines_removed; 146 items[files-1].removed = lines_removed;
145 items[files-1].old_size = old_size; 147 items[files-1].old_size = old_size;
146 items[files-1].new_size = new_size; 148 items[files-1].new_size = new_size;
147 items[files-1].binary = binary; 149 items[files-1].binary = binary;
148 if (lines_added + lines_removed > max_changes) 150 if (lines_added + lines_removed > max_changes)
149 max_changes = lines_added + lines_removed; 151 max_changes = lines_added + lines_removed;
150 total_adds += lines_added; 152 total_adds += lines_added;
151 total_rems += lines_removed; 153 total_rems += lines_removed;
152} 154}
153 155
154void cgit_print_diffstat(const unsigned char *old_sha1, 156void cgit_print_diffstat(const unsigned char *old_sha1,
155 const unsigned char *new_sha1) 157 const unsigned char *new_sha1)
156{ 158{
157 int i; 159 int i;
158 160
159 html("<div class='diffstat-header'>"); 161 html("<div class='diffstat-header'>");
160 cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1, 162 cgit_diff_link("Diffstat", NULL, NULL, ctx.qry.head, ctx.qry.sha1,
161 ctx.qry.sha2, NULL); 163 ctx.qry.sha2, NULL, 0);
162 html("</div>"); 164 html("</div>");
163 html("<table summary='diffstat' class='diffstat'>"); 165 html("<table summary='diffstat' class='diffstat'>");
164 max_changes = 0; 166 max_changes = 0;
165 cgit_diff_tree(old_sha1, new_sha1, inspect_filepair, NULL); 167 cgit_diff_tree(old_sha1, new_sha1, inspect_filepair, NULL);
166 for(i = 0; i<files; i++) 168 for(i = 0; i<files; i++)
167 print_fileinfo(&items[i]); 169 print_fileinfo(&items[i]);
168 html("</table>"); 170 html("</table>");
169 html("<div class='diffstat-summary'>"); 171 html("<div class='diffstat-summary'>");
170 htmlf("%d files changed, %d insertions, %d deletions", 172 htmlf("%d files changed, %d insertions, %d deletions",
171 files, total_adds, total_rems); 173 files, total_adds, total_rems);
172 html("</div>"); 174 html("</div>");
173} 175}
174 176
175 177
176/* 178/*
177 * print a single line returned from xdiff 179 * print a single line returned from xdiff
178 */ 180 */
179static void print_line(char *line, int len) 181static void print_line(char *line, int len)
180{ 182{
181 char *class = "ctx"; 183 char *class = "ctx";
182 char c = line[len-1]; 184 char c = line[len-1];
183 185
184 if (line[0] == '+') 186 if (line[0] == '+')
185 class = "add"; 187 class = "add";
186 else if (line[0] == '-') 188 else if (line[0] == '-')
187 class = "del"; 189 class = "del";
188 else if (line[0] == '@') 190 else if (line[0] == '@')
189 class = "hunk"; 191 class = "hunk";
190 192
191 htmlf("<div class='%s'>", class); 193 htmlf("<div class='%s'>", class);
192 line[len-1] = '\0'; 194 line[len-1] = '\0';
193 html_txt(line); 195 html_txt(line);
194 html("</div>"); 196 html("</div>");
195 line[len-1] = c; 197 line[len-1] = c;
196} 198}
197 199
198static void header(unsigned char *sha1, char *path1, int mode1, 200static void header(unsigned char *sha1, char *path1, int mode1,
199 unsigned char *sha2, char *path2, int mode2) 201 unsigned char *sha2, char *path2, int mode2)
200{ 202{
201 char *abbrev1, *abbrev2; 203 char *abbrev1, *abbrev2;
202 int subproject; 204 int subproject;
203 205
204 subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2)); 206 subproject = (S_ISGITLINK(mode1) || S_ISGITLINK(mode2));
205 html("<div class='head'>"); 207 html("<div class='head'>");
206 html("diff --git a/"); 208 html("diff --git a/");
207 html_txt(path1); 209 html_txt(path1);
208 html(" b/"); 210 html(" b/");
209 html_txt(path2); 211 html_txt(path2);
210 212
211 if (is_null_sha1(sha1)) 213 if (is_null_sha1(sha1))
212 path1 = "dev/null"; 214 path1 = "dev/null";
213 if (is_null_sha1(sha2)) 215 if (is_null_sha1(sha2))
214 path2 = "dev/null"; 216 path2 = "dev/null";
215 217
216 if (mode1 == 0) 218 if (mode1 == 0)
217 htmlf("<br/>new file mode %.6o", mode2); 219 htmlf("<br/>new file mode %.6o", mode2);
218 220
219 if (mode2 == 0) 221 if (mode2 == 0)
220 htmlf("<br/>deleted file mode %.6o", mode1); 222 htmlf("<br/>deleted file mode %.6o", mode1);
221 223
222 if (!subproject) { 224 if (!subproject) {
223 abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV)); 225 abbrev1 = xstrdup(find_unique_abbrev(sha1, DEFAULT_ABBREV));
224 abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV)); 226 abbrev2 = xstrdup(find_unique_abbrev(sha2, DEFAULT_ABBREV));
225 htmlf("<br/>index %s..%s", abbrev1, abbrev2); 227 htmlf("<br/>index %s..%s", abbrev1, abbrev2);
226 free(abbrev1); 228 free(abbrev1);
227 free(abbrev2); 229 free(abbrev2);
228 if (mode1 != 0 && mode2 != 0) { 230 if (mode1 != 0 && mode2 != 0) {
229 htmlf(" %.6o", mode1); 231 htmlf(" %.6o", mode1);
230 if (mode2 != mode1) 232 if (mode2 != mode1)
231 htmlf("..%.6o", mode2); 233 htmlf("..%.6o", mode2);
232 } 234 }
233 html("<br/>--- a/"); 235 html("<br/>--- a/");
234 if (mode1 != 0) 236 if (mode1 != 0)
235 cgit_tree_link(path1, NULL, NULL, ctx.qry.head, 237 cgit_tree_link(path1, NULL, NULL, ctx.qry.head,
236 sha1_to_hex(old_rev_sha1), path1); 238 sha1_to_hex(old_rev_sha1), path1);
237 else 239 else
238 html_txt(path1); 240 html_txt(path1);
239 html("<br/>+++ b/"); 241 html("<br/>+++ b/");
240 if (mode2 != 0) 242 if (mode2 != 0)
241 cgit_tree_link(path2, NULL, NULL, ctx.qry.head, 243 cgit_tree_link(path2, NULL, NULL, ctx.qry.head,
242 sha1_to_hex(new_rev_sha1), path2); 244 sha1_to_hex(new_rev_sha1), path2);
243 else 245 else
244 html_txt(path2); 246 html_txt(path2);
245 } 247 }
246 html("</div>"); 248 html("</div>");
247} 249}
248 250
251static void print_ssdiff_link()
252{
253 if (!strcmp(ctx.qry.page, "diff")) {
254 if (use_ssdiff)
255 cgit_diff_link("Unidiff", NULL, NULL, ctx.qry.head,
256 ctx.qry.sha1, ctx.qry.sha2, ctx.qry.path, 1);
257 else
258 cgit_diff_link("Side-by-side diff", NULL, NULL,
259 ctx.qry.head, ctx.qry.sha1,
260 ctx.qry.sha2, ctx.qry.path, 1);
261 }
262}
263
249static void filepair_cb(struct diff_filepair *pair) 264static void filepair_cb(struct diff_filepair *pair)
250{ 265{
251 unsigned long old_size = 0; 266 unsigned long old_size = 0;
252 unsigned long new_size = 0; 267 unsigned long new_size = 0;
253 int binary = 0; 268 int binary = 0;
269 linediff_fn print_line_fn = print_line;
254 270
271 if (use_ssdiff) {
272 cgit_ssdiff_header_begin();
273 print_line_fn = cgit_ssdiff_line_cb;
274 }
255 header(pair->one->sha1, pair->one->path, pair->one->mode, 275 header(pair->one->sha1, pair->one->path, pair->one->mode,
256 pair->two->sha1, pair->two->path, pair->two->mode); 276 pair->two->sha1, pair->two->path, pair->two->mode);
277 if (use_ssdiff)
278 cgit_ssdiff_header_end();
257 if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) { 279 if (S_ISGITLINK(pair->one->mode) || S_ISGITLINK(pair->two->mode)) {
258 if (S_ISGITLINK(pair->one->mode)) 280 if (S_ISGITLINK(pair->one->mode))
259 print_line(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52); 281 print_line_fn(fmt("-Subproject %s", sha1_to_hex(pair->one->sha1)), 52);
260 if (S_ISGITLINK(pair->two->mode)) 282 if (S_ISGITLINK(pair->two->mode))
261 print_line(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52); 283 print_line_fn(fmt("+Subproject %s", sha1_to_hex(pair->two->sha1)), 52);
284 if (use_ssdiff)
285 cgit_ssdiff_footer();
262 return; 286 return;
263 } 287 }
264 if (cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size, 288 if (cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size,
265 &new_size, &binary, print_line)) 289 &new_size, &binary, print_line_fn))
266 cgit_print_error("Error running diff"); 290 cgit_print_error("Error running diff");
267 if (binary) 291 if (binary) {
268 html("Binary files differ"); 292 if (use_ssdiff)
293 html("<tr><td colspan='4'>Binary files differ</td></tr>");
294 else
295 html("Binary files differ");
296 }
297 if (use_ssdiff)
298 cgit_ssdiff_footer();
269} 299}
270 300
271void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix) 301void cgit_print_diff(const char *new_rev, const char *old_rev, const char *prefix)
272{ 302{
273 enum object_type type; 303 enum object_type type;
274 unsigned long size; 304 unsigned long size;
275 struct commit *commit, *commit2; 305 struct commit *commit, *commit2;
276 306
277 if (!new_rev) 307 if (!new_rev)
278 new_rev = ctx.qry.head; 308 new_rev = ctx.qry.head;
279 get_sha1(new_rev, new_rev_sha1); 309 get_sha1(new_rev, new_rev_sha1);
280 type = sha1_object_info(new_rev_sha1, &size); 310 type = sha1_object_info(new_rev_sha1, &size);
281 if (type == OBJ_BAD) { 311 if (type == OBJ_BAD) {
282 cgit_print_error(fmt("Bad object name: %s", new_rev)); 312 cgit_print_error(fmt("Bad object name: %s", new_rev));
283 return; 313 return;
284 } 314 }
285 commit = lookup_commit_reference(new_rev_sha1); 315 commit = lookup_commit_reference(new_rev_sha1);
286 if (!commit || parse_commit(commit)) 316 if (!commit || parse_commit(commit))
287 cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(new_rev_sha1))); 317 cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(new_rev_sha1)));
288 318
289 if (old_rev) 319 if (old_rev)
290 get_sha1(old_rev, old_rev_sha1); 320 get_sha1(old_rev, old_rev_sha1);
291 else if (commit->parents && commit->parents->item) 321 else if (commit->parents && commit->parents->item)
292 hashcpy(old_rev_sha1, commit->parents->item->object.sha1); 322 hashcpy(old_rev_sha1, commit->parents->item->object.sha1);
293 else 323 else
294 hashclr(old_rev_sha1); 324 hashclr(old_rev_sha1);
295 325
296 if (!is_null_sha1(old_rev_sha1)) { 326 if (!is_null_sha1(old_rev_sha1)) {
297 type = sha1_object_info(old_rev_sha1, &size); 327 type = sha1_object_info(old_rev_sha1, &size);
298 if (type == OBJ_BAD) { 328 if (type == OBJ_BAD) {
299 cgit_print_error(fmt("Bad object name: %s", sha1_to_hex(old_rev_sha1))); 329 cgit_print_error(fmt("Bad object name: %s", sha1_to_hex(old_rev_sha1)));
300 return; 330 return;
301 } 331 }
302 commit2 = lookup_commit_reference(old_rev_sha1); 332 commit2 = lookup_commit_reference(old_rev_sha1);
303 if (!commit2 || parse_commit(commit2)) 333 if (!commit2 || parse_commit(commit2))
304 cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(old_rev_sha1))); 334 cgit_print_error(fmt("Bad commit: %s", sha1_to_hex(old_rev_sha1)));
305 } 335 }
336
337 if ((ctx.qry.ssdiff && !ctx.cfg.ssdiff) || (!ctx.qry.ssdiff && ctx.cfg.ssdiff))
338 use_ssdiff = 1;
339
340 print_ssdiff_link();
306 cgit_print_diffstat(old_rev_sha1, new_rev_sha1); 341 cgit_print_diffstat(old_rev_sha1, new_rev_sha1);
307 342
308 html("<table summary='diff' class='diff'>"); 343 if (use_ssdiff) {
309 html("<tr><td>"); 344 html("<table summary='ssdiff' class='ssdiff'>");
345 } else {
346 html("<table summary='diff' class='diff'>");
347 html("<tr><td>");
348 }
310 cgit_diff_tree(old_rev_sha1, new_rev_sha1, filepair_cb, prefix); 349 cgit_diff_tree(old_rev_sha1, new_rev_sha1, filepair_cb, prefix);
311 html("</td></tr>"); 350 if (!use_ssdiff)
351 html("</td></tr>");
312 html("</table>"); 352 html("</table>");
313} 353}
diff --git a/ui-log.c b/ui-log.c
index f3132c9..0947604 100644
--- a/ui-log.c
+++ b/ui-log.c
@@ -1,234 +1,234 @@
1/* ui-log.c: functions for log output 1/* ui-log.c: functions for log output
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13int files, add_lines, rem_lines; 13int files, add_lines, rem_lines;
14 14
15void count_lines(char *line, int size) 15void count_lines(char *line, int size)
16{ 16{
17 if (size <= 0) 17 if (size <= 0)
18 return; 18 return;
19 19
20 if (line[0] == '+') 20 if (line[0] == '+')
21 add_lines++; 21 add_lines++;
22 22
23 else if (line[0] == '-') 23 else if (line[0] == '-')
24 rem_lines++; 24 rem_lines++;
25} 25}
26 26
27void inspect_files(struct diff_filepair *pair) 27void inspect_files(struct diff_filepair *pair)
28{ 28{
29 unsigned long old_size = 0; 29 unsigned long old_size = 0;
30 unsigned long new_size = 0; 30 unsigned long new_size = 0;
31 int binary = 0; 31 int binary = 0;
32 32
33 files++; 33 files++;
34 if (ctx.repo->enable_log_linecount) 34 if (ctx.repo->enable_log_linecount)
35 cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size, 35 cgit_diff_files(pair->one->sha1, pair->two->sha1, &old_size,
36 &new_size, &binary, count_lines); 36 &new_size, &binary, count_lines);
37} 37}
38 38
39void show_commit_decorations(struct commit *commit) 39void show_commit_decorations(struct commit *commit)
40{ 40{
41 struct name_decoration *deco; 41 struct name_decoration *deco;
42 static char buf[1024]; 42 static char buf[1024];
43 43
44 buf[sizeof(buf) - 1] = 0; 44 buf[sizeof(buf) - 1] = 0;
45 deco = lookup_decoration(&name_decoration, &commit->object); 45 deco = lookup_decoration(&name_decoration, &commit->object);
46 while (deco) { 46 while (deco) {
47 if (!prefixcmp(deco->name, "refs/heads/")) { 47 if (!prefixcmp(deco->name, "refs/heads/")) {
48 strncpy(buf, deco->name + 11, sizeof(buf) - 1); 48 strncpy(buf, deco->name + 11, sizeof(buf) - 1);
49 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, NULL, 49 cgit_log_link(buf, NULL, "branch-deco", buf, NULL, NULL,
50 0, NULL, NULL, ctx.qry.showmsg); 50 0, NULL, NULL, ctx.qry.showmsg);
51 } 51 }
52 else if (!prefixcmp(deco->name, "tag: refs/tags/")) { 52 else if (!prefixcmp(deco->name, "tag: refs/tags/")) {
53 strncpy(buf, deco->name + 15, sizeof(buf) - 1); 53 strncpy(buf, deco->name + 15, sizeof(buf) - 1);
54 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf); 54 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
55 } 55 }
56 else if (!prefixcmp(deco->name, "refs/tags/")) { 56 else if (!prefixcmp(deco->name, "refs/tags/")) {
57 strncpy(buf, deco->name + 10, sizeof(buf) - 1); 57 strncpy(buf, deco->name + 10, sizeof(buf) - 1);
58 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf); 58 cgit_tag_link(buf, NULL, "tag-deco", ctx.qry.head, buf);
59 } 59 }
60 else if (!prefixcmp(deco->name, "refs/remotes/")) { 60 else if (!prefixcmp(deco->name, "refs/remotes/")) {
61 strncpy(buf, deco->name + 13, sizeof(buf) - 1); 61 strncpy(buf, deco->name + 13, sizeof(buf) - 1);
62 cgit_log_link(buf, NULL, "remote-deco", NULL, 62 cgit_log_link(buf, NULL, "remote-deco", NULL,
63 sha1_to_hex(commit->object.sha1), NULL, 63 sha1_to_hex(commit->object.sha1), NULL,
64 0, NULL, NULL, ctx.qry.showmsg); 64 0, NULL, NULL, ctx.qry.showmsg);
65 } 65 }
66 else { 66 else {
67 strncpy(buf, deco->name, sizeof(buf) - 1); 67 strncpy(buf, deco->name, sizeof(buf) - 1);
68 cgit_commit_link(buf, NULL, "deco", ctx.qry.head, 68 cgit_commit_link(buf, NULL, "deco", ctx.qry.head,
69 sha1_to_hex(commit->object.sha1)); 69 sha1_to_hex(commit->object.sha1), 0);
70 } 70 }
71 deco = deco->next; 71 deco = deco->next;
72 } 72 }
73} 73}
74 74
75void print_commit(struct commit *commit) 75void print_commit(struct commit *commit)
76{ 76{
77 struct commitinfo *info; 77 struct commitinfo *info;
78 char *tmp; 78 char *tmp;
79 int cols = 2; 79 int cols = 2;
80 80
81 info = cgit_parse_commit(commit); 81 info = cgit_parse_commit(commit);
82 htmlf("<tr%s><td>", 82 htmlf("<tr%s><td>",
83 ctx.qry.showmsg ? " class='logheader'" : ""); 83 ctx.qry.showmsg ? " class='logheader'" : "");
84 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1)); 84 tmp = fmt("id=%s", sha1_to_hex(commit->object.sha1));
85 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp); 85 tmp = cgit_pageurl(ctx.repo->url, "commit", tmp);
86 html_link_open(tmp, NULL, NULL); 86 html_link_open(tmp, NULL, NULL);
87 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE); 87 cgit_print_age(commit->date, TM_WEEK * 2, FMT_SHORTDATE);
88 html_link_close(); 88 html_link_close();
89 htmlf("</td><td%s>", 89 htmlf("</td><td%s>",
90 ctx.qry.showmsg ? " class='logsubject'" : ""); 90 ctx.qry.showmsg ? " class='logsubject'" : "");
91 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head, 91 cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
92 sha1_to_hex(commit->object.sha1)); 92 sha1_to_hex(commit->object.sha1), 0);
93 show_commit_decorations(commit); 93 show_commit_decorations(commit);
94 html("</td><td>"); 94 html("</td><td>");
95 html_txt(info->author); 95 html_txt(info->author);
96 if (ctx.repo->enable_log_filecount) { 96 if (ctx.repo->enable_log_filecount) {
97 files = 0; 97 files = 0;
98 add_lines = 0; 98 add_lines = 0;
99 rem_lines = 0; 99 rem_lines = 0;
100 cgit_diff_commit(commit, inspect_files); 100 cgit_diff_commit(commit, inspect_files);
101 html("</td><td>"); 101 html("</td><td>");
102 htmlf("%d", files); 102 htmlf("%d", files);
103 if (ctx.repo->enable_log_linecount) { 103 if (ctx.repo->enable_log_linecount) {
104 html("</td><td>"); 104 html("</td><td>");
105 htmlf("-%d/+%d", rem_lines, add_lines); 105 htmlf("-%d/+%d", rem_lines, add_lines);
106 } 106 }
107 } 107 }
108 html("</td></tr>\n"); 108 html("</td></tr>\n");
109 if (ctx.qry.showmsg) { 109 if (ctx.qry.showmsg) {
110 if (ctx.repo->enable_log_filecount) { 110 if (ctx.repo->enable_log_filecount) {
111 cols++; 111 cols++;
112 if (ctx.repo->enable_log_linecount) 112 if (ctx.repo->enable_log_linecount)
113 cols++; 113 cols++;
114 } 114 }
115 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>", 115 htmlf("<tr class='nohover'><td/><td colspan='%d' class='logmsg'>",
116 cols); 116 cols);
117 html_txt(info->msg); 117 html_txt(info->msg);
118 html("</td></tr>\n"); 118 html("</td></tr>\n");
119 } 119 }
120 cgit_free_commitinfo(info); 120 cgit_free_commitinfo(info);
121} 121}
122 122
123static const char *disambiguate_ref(const char *ref) 123static const char *disambiguate_ref(const char *ref)
124{ 124{
125 unsigned char sha1[20]; 125 unsigned char sha1[20];
126 const char *longref; 126 const char *longref;
127 127
128 longref = fmt("refs/heads/%s", ref); 128 longref = fmt("refs/heads/%s", ref);
129 if (get_sha1(longref, sha1) == 0) 129 if (get_sha1(longref, sha1) == 0)
130 return longref; 130 return longref;
131 131
132 return ref; 132 return ref;
133} 133}
134 134
135void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern, 135void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern,
136 char *path, int pager) 136 char *path, int pager)
137{ 137{
138 struct rev_info rev; 138 struct rev_info rev;
139 struct commit *commit; 139 struct commit *commit;
140 const char *argv[] = {NULL, NULL, NULL, NULL, NULL}; 140 const char *argv[] = {NULL, NULL, NULL, NULL, NULL};
141 int argc = 2; 141 int argc = 2;
142 int i, columns = 3; 142 int i, columns = 3;
143 143
144 if (!tip) 144 if (!tip)
145 tip = ctx.qry.head; 145 tip = ctx.qry.head;
146 146
147 argv[1] = disambiguate_ref(tip); 147 argv[1] = disambiguate_ref(tip);
148 148
149 if (grep && pattern && (!strcmp(grep, "grep") || 149 if (grep && pattern && (!strcmp(grep, "grep") ||
150 !strcmp(grep, "author") || 150 !strcmp(grep, "author") ||
151 !strcmp(grep, "committer"))) 151 !strcmp(grep, "committer")))
152 argv[argc++] = fmt("--%s=%s", grep, pattern); 152 argv[argc++] = fmt("--%s=%s", grep, pattern);
153 153
154 if (path) { 154 if (path) {
155 argv[argc++] = "--"; 155 argv[argc++] = "--";
156 argv[argc++] = path; 156 argv[argc++] = path;
157 } 157 }
158 init_revisions(&rev, NULL); 158 init_revisions(&rev, NULL);
159 rev.abbrev = DEFAULT_ABBREV; 159 rev.abbrev = DEFAULT_ABBREV;
160 rev.commit_format = CMIT_FMT_DEFAULT; 160 rev.commit_format = CMIT_FMT_DEFAULT;
161 rev.verbose_header = 1; 161 rev.verbose_header = 1;
162 rev.show_root_diff = 0; 162 rev.show_root_diff = 0;
163 setup_revisions(argc, argv, &rev, NULL); 163 setup_revisions(argc, argv, &rev, NULL);
164 load_ref_decorations(DECORATE_FULL_REFS); 164 load_ref_decorations(DECORATE_FULL_REFS);
165 rev.show_decorations = 1; 165 rev.show_decorations = 1;
166 rev.grep_filter.regflags |= REG_ICASE; 166 rev.grep_filter.regflags |= REG_ICASE;
167 compile_grep_patterns(&rev.grep_filter); 167 compile_grep_patterns(&rev.grep_filter);
168 prepare_revision_walk(&rev); 168 prepare_revision_walk(&rev);
169 169
170 if (pager) 170 if (pager)
171 html("<table class='list nowrap'>"); 171 html("<table class='list nowrap'>");
172 172
173 html("<tr class='nohover'><th class='left'>Age</th>" 173 html("<tr class='nohover'><th class='left'>Age</th>"
174 "<th class='left'>Commit message"); 174 "<th class='left'>Commit message");
175 if (pager) { 175 if (pager) {
176 html(" ("); 176 html(" (");
177 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL, 177 cgit_log_link(ctx.qry.showmsg ? "Collapse" : "Expand", NULL,
178 NULL, ctx.qry.head, ctx.qry.sha1, 178 NULL, ctx.qry.head, ctx.qry.sha1,
179 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep, 179 ctx.qry.path, ctx.qry.ofs, ctx.qry.grep,
180 ctx.qry.search, ctx.qry.showmsg ? 0 : 1); 180 ctx.qry.search, ctx.qry.showmsg ? 0 : 1);
181 html(")"); 181 html(")");
182 } 182 }
183 html("</th><th class='left'>Author</th>"); 183 html("</th><th class='left'>Author</th>");
184 if (ctx.repo->enable_log_filecount) { 184 if (ctx.repo->enable_log_filecount) {
185 html("<th class='left'>Files</th>"); 185 html("<th class='left'>Files</th>");
186 columns++; 186 columns++;
187 if (ctx.repo->enable_log_linecount) { 187 if (ctx.repo->enable_log_linecount) {
188 html("<th class='left'>Lines</th>"); 188 html("<th class='left'>Lines</th>");
189 columns++; 189 columns++;
190 } 190 }
191 } 191 }
192 html("</tr>\n"); 192 html("</tr>\n");
193 193
194 if (ofs<0) 194 if (ofs<0)
195 ofs = 0; 195 ofs = 0;
196 196
197 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) { 197 for (i = 0; i < ofs && (commit = get_revision(&rev)) != NULL; i++) {
198 free(commit->buffer); 198 free(commit->buffer);
199 commit->buffer = NULL; 199 commit->buffer = NULL;
200 free_commit_list(commit->parents); 200 free_commit_list(commit->parents);
201 commit->parents = NULL; 201 commit->parents = NULL;
202 } 202 }
203 203
204 for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) { 204 for (i = 0; i < cnt && (commit = get_revision(&rev)) != NULL; i++) {
205 print_commit(commit); 205 print_commit(commit);
206 free(commit->buffer); 206 free(commit->buffer);
207 commit->buffer = NULL; 207 commit->buffer = NULL;
208 free_commit_list(commit->parents); 208 free_commit_list(commit->parents);
209 commit->parents = NULL; 209 commit->parents = NULL;
210 } 210 }
211 if (pager) { 211 if (pager) {
212 htmlf("</table><div class='pager'>", 212 htmlf("</table><div class='pager'>",
213 columns); 213 columns);
214 if (ofs > 0) { 214 if (ofs > 0) {
215 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head, 215 cgit_log_link("[prev]", NULL, NULL, ctx.qry.head,
216 ctx.qry.sha1, ctx.qry.path, 216 ctx.qry.sha1, ctx.qry.path,
217 ofs - cnt, ctx.qry.grep, 217 ofs - cnt, ctx.qry.grep,
218 ctx.qry.search, ctx.qry.showmsg); 218 ctx.qry.search, ctx.qry.showmsg);
219 html("&nbsp;"); 219 html("&nbsp;");
220 } 220 }
221 if ((commit = get_revision(&rev)) != NULL) { 221 if ((commit = get_revision(&rev)) != NULL) {
222 cgit_log_link("[next]", NULL, NULL, ctx.qry.head, 222 cgit_log_link("[next]", NULL, NULL, ctx.qry.head,
223 ctx.qry.sha1, ctx.qry.path, 223 ctx.qry.sha1, ctx.qry.path,
224 ofs + cnt, ctx.qry.grep, 224 ofs + cnt, ctx.qry.grep,
225 ctx.qry.search, ctx.qry.showmsg); 225 ctx.qry.search, ctx.qry.showmsg);
226 } 226 }
227 html("</div>"); 227 html("</div>");
228 } else if ((commit = get_revision(&rev)) != NULL) { 228 } else if ((commit = get_revision(&rev)) != NULL) {
229 html("<tr class='nohover'><td colspan='3'>"); 229 html("<tr class='nohover'><td colspan='3'>");
230 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0, 230 cgit_log_link("[...]", NULL, NULL, ctx.qry.head, NULL, NULL, 0,
231 NULL, NULL, ctx.qry.showmsg); 231 NULL, NULL, ctx.qry.showmsg);
232 html("</td></tr>\n"); 232 html("</td></tr>\n");
233 } 233 }
234} 234}
diff --git a/ui-refs.c b/ui-refs.c
index d3b4f6e..33d9bec 100644
--- a/ui-refs.c
+++ b/ui-refs.c
@@ -1,245 +1,245 @@
1/* ui-refs.c: browse symbolic refs 1/* ui-refs.c: browse symbolic refs
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "html.h" 10#include "html.h"
11#include "ui-shared.h" 11#include "ui-shared.h"
12 12
13static int header; 13static int header;
14 14
15static int cmp_age(int age1, int age2) 15static int cmp_age(int age1, int age2)
16{ 16{
17 if (age1 != 0 && age2 != 0) 17 if (age1 != 0 && age2 != 0)
18 return age2 - age1; 18 return age2 - age1;
19 19
20 if (age1 == 0 && age2 == 0) 20 if (age1 == 0 && age2 == 0)
21 return 0; 21 return 0;
22 22
23 if (age1 == 0) 23 if (age1 == 0)
24 return +1; 24 return +1;
25 25
26 return -1; 26 return -1;
27} 27}
28 28
29static int cmp_ref_name(const void *a, const void *b) 29static int cmp_ref_name(const void *a, const void *b)
30{ 30{
31 struct refinfo *r1 = *(struct refinfo **)a; 31 struct refinfo *r1 = *(struct refinfo **)a;
32 struct refinfo *r2 = *(struct refinfo **)b; 32 struct refinfo *r2 = *(struct refinfo **)b;
33 33
34 return strcmp(r1->refname, r2->refname); 34 return strcmp(r1->refname, r2->refname);
35} 35}
36 36
37static int cmp_branch_age(const void *a, const void *b) 37static int cmp_branch_age(const void *a, const void *b)
38{ 38{
39 struct refinfo *r1 = *(struct refinfo **)a; 39 struct refinfo *r1 = *(struct refinfo **)a;
40 struct refinfo *r2 = *(struct refinfo **)b; 40 struct refinfo *r2 = *(struct refinfo **)b;
41 41
42 return cmp_age(r1->commit->committer_date, r2->commit->committer_date); 42 return cmp_age(r1->commit->committer_date, r2->commit->committer_date);
43} 43}
44 44
45static int cmp_tag_age(const void *a, const void *b) 45static int cmp_tag_age(const void *a, const void *b)
46{ 46{
47 struct refinfo *r1 = *(struct refinfo **)a; 47 struct refinfo *r1 = *(struct refinfo **)a;
48 struct refinfo *r2 = *(struct refinfo **)b; 48 struct refinfo *r2 = *(struct refinfo **)b;
49 int r1date, r2date; 49 int r1date, r2date;
50 50
51 if (r1->object->type != OBJ_COMMIT) 51 if (r1->object->type != OBJ_COMMIT)
52 r1date = r1->tag->tagger_date; 52 r1date = r1->tag->tagger_date;
53 else 53 else
54 r1date = r1->commit->committer_date; 54 r1date = r1->commit->committer_date;
55 55
56 if (r2->object->type != OBJ_COMMIT) 56 if (r2->object->type != OBJ_COMMIT)
57 r2date = r2->tag->tagger_date; 57 r2date = r2->tag->tagger_date;
58 else 58 else
59 r2date = r2->commit->committer_date; 59 r2date = r2->commit->committer_date;
60 60
61 return cmp_age(r1date, r2date); 61 return cmp_age(r1date, r2date);
62} 62}
63 63
64static int print_branch(struct refinfo *ref) 64static int print_branch(struct refinfo *ref)
65{ 65{
66 struct commitinfo *info = ref->commit; 66 struct commitinfo *info = ref->commit;
67 char *name = (char *)ref->refname; 67 char *name = (char *)ref->refname;
68 68
69 if (!info) 69 if (!info)
70 return 1; 70 return 1;
71 html("<tr><td>"); 71 html("<tr><td>");
72 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL, 72 cgit_log_link(name, NULL, NULL, name, NULL, NULL, 0, NULL, NULL,
73 ctx.qry.showmsg); 73 ctx.qry.showmsg);
74 html("</td><td>"); 74 html("</td><td>");
75 75
76 if (ref->object->type == OBJ_COMMIT) { 76 if (ref->object->type == OBJ_COMMIT) {
77 cgit_commit_link(info->subject, NULL, NULL, name, NULL); 77 cgit_commit_link(info->subject, NULL, NULL, name, NULL, 0);
78 html("</td><td>"); 78 html("</td><td>");
79 html_txt(info->author); 79 html_txt(info->author);
80 html("</td><td colspan='2'>"); 80 html("</td><td colspan='2'>");
81 cgit_print_age(info->commit->date, -1, NULL); 81 cgit_print_age(info->commit->date, -1, NULL);
82 } else { 82 } else {
83 html("</td><td></td><td>"); 83 html("</td><td></td><td>");
84 cgit_object_link(ref->object); 84 cgit_object_link(ref->object);
85 } 85 }
86 html("</td></tr>\n"); 86 html("</td></tr>\n");
87 return 0; 87 return 0;
88} 88}
89 89
90static void print_tag_header() 90static void print_tag_header()
91{ 91{
92 html("<tr class='nohover'><th class='left'>Tag</th>" 92 html("<tr class='nohover'><th class='left'>Tag</th>"
93 "<th class='left'>Download</th>" 93 "<th class='left'>Download</th>"
94 "<th class='left'>Author</th>" 94 "<th class='left'>Author</th>"
95 "<th class='left' colspan='2'>Age</th></tr>\n"); 95 "<th class='left' colspan='2'>Age</th></tr>\n");
96 header = 1; 96 header = 1;
97} 97}
98 98
99static void print_tag_downloads(const struct cgit_repo *repo, const char *ref) 99static void print_tag_downloads(const struct cgit_repo *repo, const char *ref)
100{ 100{
101 const struct cgit_snapshot_format* f; 101 const struct cgit_snapshot_format* f;
102 char *filename; 102 char *filename;
103 const char *basename; 103 const char *basename;
104 104
105 if (!ref || strlen(ref) < 2) 105 if (!ref || strlen(ref) < 2)
106 return; 106 return;
107 107
108 basename = cgit_repobasename(repo->url); 108 basename = cgit_repobasename(repo->url);
109 if (prefixcmp(ref, basename) != 0) { 109 if (prefixcmp(ref, basename) != 0) {
110 if ((ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1])) 110 if ((ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]))
111 ref++; 111 ref++;
112 if (isdigit(ref[0])) 112 if (isdigit(ref[0]))
113 ref = xstrdup(fmt("%s-%s", basename, ref)); 113 ref = xstrdup(fmt("%s-%s", basename, ref));
114 } 114 }
115 115
116 for (f = cgit_snapshot_formats; f->suffix; f++) { 116 for (f = cgit_snapshot_formats; f->suffix; f++) {
117 if (!(repo->snapshots & f->bit)) 117 if (!(repo->snapshots & f->bit))
118 continue; 118 continue;
119 filename = fmt("%s%s", ref, f->suffix); 119 filename = fmt("%s%s", ref, f->suffix);
120 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename); 120 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
121 html("&nbsp;&nbsp;"); 121 html("&nbsp;&nbsp;");
122 } 122 }
123} 123}
124static int print_tag(struct refinfo *ref) 124static int print_tag(struct refinfo *ref)
125{ 125{
126 struct tag *tag; 126 struct tag *tag;
127 struct taginfo *info; 127 struct taginfo *info;
128 char *name = (char *)ref->refname; 128 char *name = (char *)ref->refname;
129 129
130 if (ref->object->type == OBJ_TAG) { 130 if (ref->object->type == OBJ_TAG) {
131 tag = (struct tag *)ref->object; 131 tag = (struct tag *)ref->object;
132 info = ref->tag; 132 info = ref->tag;
133 if (!tag || !info) 133 if (!tag || !info)
134 return 1; 134 return 1;
135 html("<tr><td>"); 135 html("<tr><td>");
136 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 136 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
137 html("</td><td>"); 137 html("</td><td>");
138 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT)) 138 if (ctx.repo->snapshots && (tag->tagged->type == OBJ_COMMIT))
139 print_tag_downloads(ctx.repo, name); 139 print_tag_downloads(ctx.repo, name);
140 else 140 else
141 cgit_object_link(tag->tagged); 141 cgit_object_link(tag->tagged);
142 html("</td><td>"); 142 html("</td><td>");
143 if (info->tagger) 143 if (info->tagger)
144 html(info->tagger); 144 html(info->tagger);
145 html("</td><td colspan='2'>"); 145 html("</td><td colspan='2'>");
146 if (info->tagger_date > 0) 146 if (info->tagger_date > 0)
147 cgit_print_age(info->tagger_date, -1, NULL); 147 cgit_print_age(info->tagger_date, -1, NULL);
148 html("</td></tr>\n"); 148 html("</td></tr>\n");
149 } else { 149 } else {
150 if (!header) 150 if (!header)
151 print_tag_header(); 151 print_tag_header();
152 html("<tr><td>"); 152 html("<tr><td>");
153 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name); 153 cgit_tag_link(name, NULL, NULL, ctx.qry.head, name);
154 html("</td><td>"); 154 html("</td><td>");
155 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT)) 155 if (ctx.repo->snapshots && (ref->object->type == OBJ_COMMIT))
156 print_tag_downloads(ctx.repo, name); 156 print_tag_downloads(ctx.repo, name);
157 else 157 else
158 cgit_object_link(ref->object); 158 cgit_object_link(ref->object);
159 html("</td><td>"); 159 html("</td><td>");
160 if (ref->object->type == OBJ_COMMIT) 160 if (ref->object->type == OBJ_COMMIT)
161 html(ref->commit->author); 161 html(ref->commit->author);
162 html("</td><td colspan='2'>"); 162 html("</td><td colspan='2'>");
163 if (ref->object->type == OBJ_COMMIT) 163 if (ref->object->type == OBJ_COMMIT)
164 cgit_print_age(ref->commit->commit->date, -1, NULL); 164 cgit_print_age(ref->commit->commit->date, -1, NULL);
165 html("</td></tr>\n"); 165 html("</td></tr>\n");
166 } 166 }
167 return 0; 167 return 0;
168} 168}
169 169
170static void print_refs_link(char *path) 170static void print_refs_link(char *path)
171{ 171{
172 html("<tr class='nohover'><td colspan='4'>"); 172 html("<tr class='nohover'><td colspan='4'>");
173 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path); 173 cgit_refs_link("[...]", NULL, NULL, ctx.qry.head, NULL, path);
174 html("</td></tr>"); 174 html("</td></tr>");
175} 175}
176 176
177void cgit_print_branches(int maxcount) 177void cgit_print_branches(int maxcount)
178{ 178{
179 struct reflist list; 179 struct reflist list;
180 int i; 180 int i;
181 181
182 html("<tr class='nohover'><th class='left'>Branch</th>" 182 html("<tr class='nohover'><th class='left'>Branch</th>"
183 "<th class='left'>Commit message</th>" 183 "<th class='left'>Commit message</th>"
184 "<th class='left'>Author</th>" 184 "<th class='left'>Author</th>"
185 "<th class='left' colspan='2'>Age</th></tr>\n"); 185 "<th class='left' colspan='2'>Age</th></tr>\n");
186 186
187 list.refs = NULL; 187 list.refs = NULL;
188 list.alloc = list.count = 0; 188 list.alloc = list.count = 0;
189 for_each_branch_ref(cgit_refs_cb, &list); 189 for_each_branch_ref(cgit_refs_cb, &list);
190 190
191 if (maxcount == 0 || maxcount > list.count) 191 if (maxcount == 0 || maxcount > list.count)
192 maxcount = list.count; 192 maxcount = list.count;
193 193
194 if (maxcount < list.count) { 194 if (maxcount < list.count) {
195 qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age); 195 qsort(list.refs, list.count, sizeof(*list.refs), cmp_branch_age);
196 qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name); 196 qsort(list.refs, maxcount, sizeof(*list.refs), cmp_ref_name);
197 } 197 }
198 198
199 for(i=0; i<maxcount; i++) 199 for(i=0; i<maxcount; i++)
200 print_branch(list.refs[i]); 200 print_branch(list.refs[i]);
201 201
202 if (maxcount < list.count) 202 if (maxcount < list.count)
203 print_refs_link("heads"); 203 print_refs_link("heads");
204} 204}
205 205
206void cgit_print_tags(int maxcount) 206void cgit_print_tags(int maxcount)
207{ 207{
208 struct reflist list; 208 struct reflist list;
209 int i; 209 int i;
210 210
211 header = 0; 211 header = 0;
212 list.refs = NULL; 212 list.refs = NULL;
213 list.alloc = list.count = 0; 213 list.alloc = list.count = 0;
214 for_each_tag_ref(cgit_refs_cb, &list); 214 for_each_tag_ref(cgit_refs_cb, &list);
215 if (list.count == 0) 215 if (list.count == 0)
216 return; 216 return;
217 qsort(list.refs, list.count, sizeof(*list.refs), cmp_tag_age); 217 qsort(list.refs, list.count, sizeof(*list.refs), cmp_tag_age);
218 if (!maxcount) 218 if (!maxcount)
219 maxcount = list.count; 219 maxcount = list.count;
220 else if (maxcount > list.count) 220 else if (maxcount > list.count)
221 maxcount = list.count; 221 maxcount = list.count;
222 print_tag_header(); 222 print_tag_header();
223 for(i=0; i<maxcount; i++) 223 for(i=0; i<maxcount; i++)
224 print_tag(list.refs[i]); 224 print_tag(list.refs[i]);
225 225
226 if (maxcount < list.count) 226 if (maxcount < list.count)
227 print_refs_link("tags"); 227 print_refs_link("tags");
228} 228}
229 229
230void cgit_print_refs() 230void cgit_print_refs()
231{ 231{
232 232
233 html("<table class='list nowrap'>"); 233 html("<table class='list nowrap'>");
234 234
235 if (ctx.qry.path && !strncmp(ctx.qry.path, "heads", 5)) 235 if (ctx.qry.path && !strncmp(ctx.qry.path, "heads", 5))
236 cgit_print_branches(0); 236 cgit_print_branches(0);
237 else if (ctx.qry.path && !strncmp(ctx.qry.path, "tags", 4)) 237 else if (ctx.qry.path && !strncmp(ctx.qry.path, "tags", 4))
238 cgit_print_tags(0); 238 cgit_print_tags(0);
239 else { 239 else {
240 cgit_print_branches(0); 240 cgit_print_branches(0);
241 html("<tr class='nohover'><td colspan='4'>&nbsp;</td></tr>"); 241 html("<tr class='nohover'><td colspan='4'>&nbsp;</td></tr>");
242 cgit_print_tags(0); 242 cgit_print_tags(0);
243 } 243 }
244 html("</table>"); 244 html("</table>");
245} 245}
diff --git a/ui-shared.c b/ui-shared.c
index 3a9e67b..08ea003 100644
--- a/ui-shared.c
+++ b/ui-shared.c
@@ -1,778 +1,800 @@
1/* ui-shared.c: common web output functions 1/* ui-shared.c: common web output functions
2 * 2 *
3 * Copyright (C) 2006 Lars Hjemli 3 * Copyright (C) 2006 Lars Hjemli
4 * 4 *
5 * Licensed under GNU General Public License v2 5 * Licensed under GNU General Public License v2
6 * (see COPYING for full license text) 6 * (see COPYING for full license text)
7 */ 7 */
8 8
9#include "cgit.h" 9#include "cgit.h"
10#include "cmd.h" 10#include "cmd.h"
11#include "html.h" 11#include "html.h"
12 12
13const char cgit_doctype[] = 13const char cgit_doctype[] =
14"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n" 14"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
15" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"; 15" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n";
16 16
17static char *http_date(time_t t) 17static char *http_date(time_t t)
18{ 18{
19 static char day[][4] = 19 static char day[][4] =
20 {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; 20 {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
21 static char month[][4] = 21 static char month[][4] =
22 {"Jan", "Feb", "Mar", "Apr", "May", "Jun", 22 {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
23 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; 23 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
24 struct tm *tm = gmtime(&t); 24 struct tm *tm = gmtime(&t);
25 return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday], 25 return fmt("%s, %02d %s %04d %02d:%02d:%02d GMT", day[tm->tm_wday],
26 tm->tm_mday, month[tm->tm_mon], 1900+tm->tm_year, 26 tm->tm_mday, month[tm->tm_mon], 1900+tm->tm_year,
27 tm->tm_hour, tm->tm_min, tm->tm_sec); 27 tm->tm_hour, tm->tm_min, tm->tm_sec);
28} 28}
29 29
30void cgit_print_error(char *msg) 30void cgit_print_error(char *msg)
31{ 31{
32 html("<div class='error'>"); 32 html("<div class='error'>");
33 html_txt(msg); 33 html_txt(msg);
34 html("</div>\n"); 34 html("</div>\n");
35} 35}
36 36
37char *cgit_httpscheme() 37char *cgit_httpscheme()
38{ 38{
39 if (ctx.env.https && !strcmp(ctx.env.https, "on")) 39 if (ctx.env.https && !strcmp(ctx.env.https, "on"))
40 return "https://"; 40 return "https://";
41 else 41 else
42 return "http://"; 42 return "http://";
43} 43}
44 44
45char *cgit_hosturl() 45char *cgit_hosturl()
46{ 46{
47 if (ctx.env.http_host) 47 if (ctx.env.http_host)
48 return ctx.env.http_host; 48 return ctx.env.http_host;
49 if (!ctx.env.server_name) 49 if (!ctx.env.server_name)
50 return NULL; 50 return NULL;
51 if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80) 51 if (!ctx.env.server_port || atoi(ctx.env.server_port) == 80)
52 return ctx.env.server_name; 52 return ctx.env.server_name;
53 return xstrdup(fmt("%s:%s", ctx.env.server_name, ctx.env.server_port)); 53 return xstrdup(fmt("%s:%s", ctx.env.server_name, ctx.env.server_port));
54} 54}
55 55
56char *cgit_rooturl() 56char *cgit_rooturl()
57{ 57{
58 if (ctx.cfg.virtual_root) 58 if (ctx.cfg.virtual_root)
59 return fmt("%s/", ctx.cfg.virtual_root); 59 return fmt("%s/", ctx.cfg.virtual_root);
60 else 60 else
61 return ctx.cfg.script_name; 61 return ctx.cfg.script_name;
62} 62}
63 63
64char *cgit_repourl(const char *reponame) 64char *cgit_repourl(const char *reponame)
65{ 65{
66 if (ctx.cfg.virtual_root) { 66 if (ctx.cfg.virtual_root) {
67 return fmt("%s/%s/", ctx.cfg.virtual_root, reponame); 67 return fmt("%s/%s/", ctx.cfg.virtual_root, reponame);
68 } else { 68 } else {
69 return fmt("?r=%s", reponame); 69 return fmt("?r=%s", reponame);
70 } 70 }
71} 71}
72 72
73char *cgit_fileurl(const char *reponame, const char *pagename, 73char *cgit_fileurl(const char *reponame, const char *pagename,
74 const char *filename, const char *query) 74 const char *filename, const char *query)
75{ 75{
76 char *tmp; 76 char *tmp;
77 char *delim; 77 char *delim;
78 78
79 if (ctx.cfg.virtual_root) { 79 if (ctx.cfg.virtual_root) {
80 tmp = fmt("%s/%s/%s/%s", ctx.cfg.virtual_root, reponame, 80 tmp = fmt("%s/%s/%s/%s", ctx.cfg.virtual_root, reponame,
81 pagename, (filename ? filename:"")); 81 pagename, (filename ? filename:""));
82 delim = "?"; 82 delim = "?";
83 } else { 83 } else {
84 tmp = fmt("?url=%s/%s/%s", reponame, pagename, 84 tmp = fmt("?url=%s/%s/%s", reponame, pagename,
85 (filename ? filename : "")); 85 (filename ? filename : ""));
86 delim = "&"; 86 delim = "&";
87 } 87 }
88 if (query) 88 if (query)
89 tmp = fmt("%s%s%s", tmp, delim, query); 89 tmp = fmt("%s%s%s", tmp, delim, query);
90 return tmp; 90 return tmp;
91} 91}
92 92
93char *cgit_pageurl(const char *reponame, const char *pagename, 93char *cgit_pageurl(const char *reponame, const char *pagename,
94 const char *query) 94 const char *query)
95{ 95{
96 return cgit_fileurl(reponame,pagename,0,query); 96 return cgit_fileurl(reponame,pagename,0,query);
97} 97}
98 98
99const char *cgit_repobasename(const char *reponame) 99const char *cgit_repobasename(const char *reponame)
100{ 100{
101 /* I assume we don't need to store more than one repo basename */ 101 /* I assume we don't need to store more than one repo basename */
102 static char rvbuf[1024]; 102 static char rvbuf[1024];
103 int p; 103 int p;
104 const char *rv; 104 const char *rv;
105 strncpy(rvbuf,reponame,sizeof(rvbuf)); 105 strncpy(rvbuf,reponame,sizeof(rvbuf));
106 if(rvbuf[sizeof(rvbuf)-1]) 106 if(rvbuf[sizeof(rvbuf)-1])
107 die("cgit_repobasename: truncated repository name '%s'", reponame); 107 die("cgit_repobasename: truncated repository name '%s'", reponame);
108 p = strlen(rvbuf)-1; 108 p = strlen(rvbuf)-1;
109 /* strip trailing slashes */ 109 /* strip trailing slashes */
110 while(p && rvbuf[p]=='/') rvbuf[p--]=0; 110 while(p && rvbuf[p]=='/') rvbuf[p--]=0;
111 /* strip trailing .git */ 111 /* strip trailing .git */
112 if(p>=3 && !strncmp(&rvbuf[p-3],".git",4)) { 112 if(p>=3 && !strncmp(&rvbuf[p-3],".git",4)) {
113 p -= 3; rvbuf[p--] = 0; 113 p -= 3; rvbuf[p--] = 0;
114 } 114 }
115 /* strip more trailing slashes if any */ 115 /* strip more trailing slashes if any */
116 while( p && rvbuf[p]=='/') rvbuf[p--]=0; 116 while( p && rvbuf[p]=='/') rvbuf[p--]=0;
117 /* find last slash in the remaining string */ 117 /* find last slash in the remaining string */
118 rv = strrchr(rvbuf,'/'); 118 rv = strrchr(rvbuf,'/');
119 if(rv) 119 if(rv)
120 return ++rv; 120 return ++rv;
121 return rvbuf; 121 return rvbuf;
122} 122}
123 123
124char *cgit_currurl() 124char *cgit_currurl()
125{ 125{
126 if (!ctx.cfg.virtual_root) 126 if (!ctx.cfg.virtual_root)
127 return ctx.cfg.script_name; 127 return ctx.cfg.script_name;
128 else if (ctx.qry.page) 128 else if (ctx.qry.page)
129 return fmt("%s/%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo, ctx.qry.page); 129 return fmt("%s/%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo, ctx.qry.page);
130 else if (ctx.qry.repo) 130 else if (ctx.qry.repo)
131 return fmt("%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo); 131 return fmt("%s/%s/", ctx.cfg.virtual_root, ctx.qry.repo);
132 else 132 else
133 return fmt("%s/", ctx.cfg.virtual_root); 133 return fmt("%s/", ctx.cfg.virtual_root);
134} 134}
135 135
136static void site_url(char *page, char *search, int ofs) 136static void site_url(char *page, char *search, int ofs)
137{ 137{
138 char *delim = "?"; 138 char *delim = "?";
139 139
140 if (ctx.cfg.virtual_root) { 140 if (ctx.cfg.virtual_root) {
141 html_attr(ctx.cfg.virtual_root); 141 html_attr(ctx.cfg.virtual_root);
142 if (ctx.cfg.virtual_root[strlen(ctx.cfg.virtual_root) - 1] != '/') 142 if (ctx.cfg.virtual_root[strlen(ctx.cfg.virtual_root) - 1] != '/')
143 html("/"); 143 html("/");
144 } else 144 } else
145 html(ctx.cfg.script_name); 145 html(ctx.cfg.script_name);
146 146
147 if (page) { 147 if (page) {
148 htmlf("?p=%s", page); 148 htmlf("?p=%s", page);
149 delim = "&"; 149 delim = "&";
150 } 150 }
151 if (search) { 151 if (search) {
152 html(delim); 152 html(delim);
153 html("q="); 153 html("q=");
154 html_attr(search); 154 html_attr(search);
155 delim = "&"; 155 delim = "&";
156 } 156 }
157 if (ofs) { 157 if (ofs) {
158 html(delim); 158 html(delim);
159 htmlf("ofs=%d", ofs); 159 htmlf("ofs=%d", ofs);
160 } 160 }
161} 161}
162 162
163static void site_link(char *page, char *name, char *title, char *class, 163static void site_link(char *page, char *name, char *title, char *class,
164 char *search, int ofs) 164 char *search, int ofs)
165{ 165{
166 html("<a"); 166 html("<a");
167 if (title) { 167 if (title) {
168 html(" title='"); 168 html(" title='");
169 html_attr(title); 169 html_attr(title);
170 html("'"); 170 html("'");
171 } 171 }
172 if (class) { 172 if (class) {
173 html(" class='"); 173 html(" class='");
174 html_attr(class); 174 html_attr(class);
175 html("'"); 175 html("'");
176 } 176 }
177 html(" href='"); 177 html(" href='");
178 site_url(page, search, ofs); 178 site_url(page, search, ofs);
179 html("'>"); 179 html("'>");
180 html_txt(name); 180 html_txt(name);
181 html("</a>"); 181 html("</a>");
182} 182}
183 183
184void cgit_index_link(char *name, char *title, char *class, char *pattern, 184void cgit_index_link(char *name, char *title, char *class, char *pattern,
185 int ofs) 185 int ofs)
186{ 186{
187 site_link(NULL, name, title, class, pattern, ofs); 187 site_link(NULL, name, title, class, pattern, ofs);
188} 188}
189 189
190static char *repolink(char *title, char *class, char *page, char *head, 190static char *repolink(char *title, char *class, char *page, char *head,
191 char *path) 191 char *path)
192{ 192{
193 char *delim = "?"; 193 char *delim = "?";
194 194
195 html("<a"); 195 html("<a");
196 if (title) { 196 if (title) {
197 html(" title='"); 197 html(" title='");
198 html_attr(title); 198 html_attr(title);
199 html("'"); 199 html("'");
200 } 200 }
201 if (class) { 201 if (class) {
202 html(" class='"); 202 html(" class='");
203 html_attr(class); 203 html_attr(class);
204 html("'"); 204 html("'");
205 } 205 }
206 html(" href='"); 206 html(" href='");
207 if (ctx.cfg.virtual_root) { 207 if (ctx.cfg.virtual_root) {
208 html_url_path(ctx.cfg.virtual_root); 208 html_url_path(ctx.cfg.virtual_root);
209 if (ctx.cfg.virtual_root[strlen(ctx.cfg.virtual_root) - 1] != '/') 209 if (ctx.cfg.virtual_root[strlen(ctx.cfg.virtual_root) - 1] != '/')
210 html("/"); 210 html("/");
211 html_url_path(ctx.repo->url); 211 html_url_path(ctx.repo->url);
212 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') 212 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
213 html("/"); 213 html("/");
214 if (page) { 214 if (page) {
215 html_url_path(page); 215 html_url_path(page);
216 html("/"); 216 html("/");
217 if (path) 217 if (path)
218 html_url_path(path); 218 html_url_path(path);
219 } 219 }
220 } else { 220 } else {
221 html(ctx.cfg.script_name); 221 html(ctx.cfg.script_name);
222 html("?url="); 222 html("?url=");
223 html_url_arg(ctx.repo->url); 223 html_url_arg(ctx.repo->url);
224 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/') 224 if (ctx.repo->url[strlen(ctx.repo->url) - 1] != '/')
225 html("/"); 225 html("/");
226 if (page) { 226 if (page) {
227 html_url_arg(page); 227 html_url_arg(page);
228 html("/"); 228 html("/");
229 if (path) 229 if (path)
230 html_url_arg(path); 230 html_url_arg(path);
231 } 231 }
232 delim = "&amp;"; 232 delim = "&amp;";
233 } 233 }
234 if (head && strcmp(head, ctx.repo->defbranch)) { 234 if (head && strcmp(head, ctx.repo->defbranch)) {
235 html(delim); 235 html(delim);
236 html("h="); 236 html("h=");
237 html_url_arg(head); 237 html_url_arg(head);
238 delim = "&amp;"; 238 delim = "&amp;";
239 } 239 }
240 return fmt("%s", delim); 240 return fmt("%s", delim);
241} 241}
242 242
243static void reporevlink(char *page, char *name, char *title, char *class, 243static void reporevlink(char *page, char *name, char *title, char *class,
244 char *head, char *rev, char *path) 244 char *head, char *rev, char *path)
245{ 245{
246 char *delim; 246 char *delim;
247 247
248 delim = repolink(title, class, page, head, path); 248 delim = repolink(title, class, page, head, path);
249 if (rev && strcmp(rev, ctx.qry.head)) { 249 if (rev && strcmp(rev, ctx.qry.head)) {
250 html(delim); 250 html(delim);
251 html("id="); 251 html("id=");
252 html_url_arg(rev); 252 html_url_arg(rev);
253 } 253 }
254 html("'>"); 254 html("'>");
255 html_txt(name); 255 html_txt(name);
256 html("</a>"); 256 html("</a>");
257} 257}
258 258
259void cgit_summary_link(char *name, char *title, char *class, char *head) 259void cgit_summary_link(char *name, char *title, char *class, char *head)
260{ 260{
261 reporevlink(NULL, name, title, class, head, NULL, NULL); 261 reporevlink(NULL, name, title, class, head, NULL, NULL);
262} 262}
263 263
264void cgit_tag_link(char *name, char *title, char *class, char *head, 264void cgit_tag_link(char *name, char *title, char *class, char *head,
265 char *rev) 265 char *rev)
266{ 266{
267 reporevlink("tag", name, title, class, head, rev, NULL); 267 reporevlink("tag", name, title, class, head, rev, NULL);
268} 268}
269 269
270void cgit_tree_link(char *name, char *title, char *class, char *head, 270void cgit_tree_link(char *name, char *title, char *class, char *head,
271 char *rev, char *path) 271 char *rev, char *path)
272{ 272{
273 reporevlink("tree", name, title, class, head, rev, path); 273 reporevlink("tree", name, title, class, head, rev, path);
274} 274}
275 275
276void cgit_plain_link(char *name, char *title, char *class, char *head, 276void cgit_plain_link(char *name, char *title, char *class, char *head,
277 char *rev, char *path) 277 char *rev, char *path)
278{ 278{
279 reporevlink("plain", name, title, class, head, rev, path); 279 reporevlink("plain", name, title, class, head, rev, path);
280} 280}
281 281
282void cgit_log_link(char *name, char *title, char *class, char *head, 282void cgit_log_link(char *name, char *title, char *class, char *head,
283 char *rev, char *path, int ofs, char *grep, char *pattern, 283 char *rev, char *path, int ofs, char *grep, char *pattern,
284 int showmsg) 284 int showmsg)
285{ 285{
286 char *delim; 286 char *delim;
287 287
288 delim = repolink(title, class, "log", head, path); 288 delim = repolink(title, class, "log", head, path);
289 if (rev && strcmp(rev, ctx.qry.head)) { 289 if (rev && strcmp(rev, ctx.qry.head)) {
290 html(delim); 290 html(delim);
291 html("id="); 291 html("id=");
292 html_url_arg(rev); 292 html_url_arg(rev);
293 delim = "&"; 293 delim = "&";
294 } 294 }
295 if (grep && pattern) { 295 if (grep && pattern) {
296 html(delim); 296 html(delim);
297 html("qt="); 297 html("qt=");
298 html_url_arg(grep); 298 html_url_arg(grep);
299 delim = "&"; 299 delim = "&";
300 html(delim); 300 html(delim);
301 html("q="); 301 html("q=");
302 html_url_arg(pattern); 302 html_url_arg(pattern);
303 } 303 }
304 if (ofs > 0) { 304 if (ofs > 0) {
305 html(delim); 305 html(delim);
306 html("ofs="); 306 html("ofs=");
307 htmlf("%d", ofs); 307 htmlf("%d", ofs);
308 delim = "&"; 308 delim = "&";
309 } 309 }
310 if (showmsg) { 310 if (showmsg) {
311 html(delim); 311 html(delim);
312 html("showmsg=1"); 312 html("showmsg=1");
313 } 313 }
314 html("'>"); 314 html("'>");
315 html_txt(name); 315 html_txt(name);
316 html("</a>"); 316 html("</a>");
317} 317}
318 318
319void cgit_commit_link(char *name, char *title, char *class, char *head, 319void cgit_commit_link(char *name, char *title, char *class, char *head,
320 char *rev) 320 char *rev, int toggle_ssdiff)
321{ 321{
322 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) { 322 if (strlen(name) > ctx.cfg.max_msg_len && ctx.cfg.max_msg_len >= 15) {
323 name[ctx.cfg.max_msg_len] = '\0'; 323 name[ctx.cfg.max_msg_len] = '\0';
324 name[ctx.cfg.max_msg_len - 1] = '.'; 324 name[ctx.cfg.max_msg_len - 1] = '.';
325 name[ctx.cfg.max_msg_len - 2] = '.'; 325 name[ctx.cfg.max_msg_len - 2] = '.';
326 name[ctx.cfg.max_msg_len - 3] = '.'; 326 name[ctx.cfg.max_msg_len - 3] = '.';
327 } 327 }
328 reporevlink("commit", name, title, class, head, rev, NULL); 328
329 char *delim;
330
331 delim = repolink(title, class, "commit", head, NULL);
332 if (rev && strcmp(rev, ctx.qry.head)) {
333 html(delim);
334 html("id=");
335 html_url_arg(rev);
336 delim = "&amp;";
337 }
338 if ((ctx.qry.ssdiff && !toggle_ssdiff) || (!ctx.qry.ssdiff && toggle_ssdiff)) {
339 html(delim);
340 html("ss=1");
341 }
342 html("'>");
343 html_txt(name);
344 html("</a>");
329} 345}
330 346
331void cgit_refs_link(char *name, char *title, char *class, char *head, 347void cgit_refs_link(char *name, char *title, char *class, char *head,
332 char *rev, char *path) 348 char *rev, char *path)
333{ 349{
334 reporevlink("refs", name, title, class, head, rev, path); 350 reporevlink("refs", name, title, class, head, rev, path);
335} 351}
336 352
337void cgit_snapshot_link(char *name, char *title, char *class, char *head, 353void cgit_snapshot_link(char *name, char *title, char *class, char *head,
338 char *rev, char *archivename) 354 char *rev, char *archivename)
339{ 355{
340 reporevlink("snapshot", name, title, class, head, rev, archivename); 356 reporevlink("snapshot", name, title, class, head, rev, archivename);
341} 357}
342 358
343void cgit_diff_link(char *name, char *title, char *class, char *head, 359void cgit_diff_link(char *name, char *title, char *class, char *head,
344 char *new_rev, char *old_rev, char *path) 360 char *new_rev, char *old_rev, char *path,
361 int toggle_ssdiff)
345{ 362{
346 char *delim; 363 char *delim;
347 364
348 delim = repolink(title, class, "diff", head, path); 365 delim = repolink(title, class, "diff", head, path);
349 if (new_rev && strcmp(new_rev, ctx.qry.head)) { 366 if (new_rev && strcmp(new_rev, ctx.qry.head)) {
350 html(delim); 367 html(delim);
351 html("id="); 368 html("id=");
352 html_url_arg(new_rev); 369 html_url_arg(new_rev);
353 delim = "&amp;"; 370 delim = "&amp;";
354 } 371 }
355 if (old_rev) { 372 if (old_rev) {
356 html(delim); 373 html(delim);
357 html("id2="); 374 html("id2=");
358 html_url_arg(old_rev); 375 html_url_arg(old_rev);
376 delim = "&amp;";
377 }
378 if ((ctx.qry.ssdiff && !toggle_ssdiff) || (!ctx.qry.ssdiff && toggle_ssdiff)) {
379 html(delim);
380 html("ss=1");
359 } 381 }
360 html("'>"); 382 html("'>");
361 html_txt(name); 383 html_txt(name);
362 html("</a>"); 384 html("</a>");
363} 385}
364 386
365void cgit_patch_link(char *name, char *title, char *class, char *head, 387void cgit_patch_link(char *name, char *title, char *class, char *head,
366 char *rev) 388 char *rev)
367{ 389{
368 reporevlink("patch", name, title, class, head, rev, NULL); 390 reporevlink("patch", name, title, class, head, rev, NULL);
369} 391}
370 392
371void cgit_stats_link(char *name, char *title, char *class, char *head, 393void cgit_stats_link(char *name, char *title, char *class, char *head,
372 char *path) 394 char *path)
373{ 395{
374 reporevlink("stats", name, title, class, head, NULL, path); 396 reporevlink("stats", name, title, class, head, NULL, path);
375} 397}
376 398
377void cgit_object_link(struct object *obj) 399void cgit_object_link(struct object *obj)
378{ 400{
379 char *page, *shortrev, *fullrev, *name; 401 char *page, *shortrev, *fullrev, *name;
380 402
381 fullrev = sha1_to_hex(obj->sha1); 403 fullrev = sha1_to_hex(obj->sha1);
382 shortrev = xstrdup(fullrev); 404 shortrev = xstrdup(fullrev);
383 shortrev[10] = '\0'; 405 shortrev[10] = '\0';
384 if (obj->type == OBJ_COMMIT) { 406 if (obj->type == OBJ_COMMIT) {
385 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL, 407 cgit_commit_link(fmt("commit %s...", shortrev), NULL, NULL,
386 ctx.qry.head, fullrev); 408 ctx.qry.head, fullrev, 0);
387 return; 409 return;
388 } else if (obj->type == OBJ_TREE) 410 } else if (obj->type == OBJ_TREE)
389 page = "tree"; 411 page = "tree";
390 else if (obj->type == OBJ_TAG) 412 else if (obj->type == OBJ_TAG)
391 page = "tag"; 413 page = "tag";
392 else 414 else
393 page = "blob"; 415 page = "blob";
394 name = fmt("%s %s...", typename(obj->type), shortrev); 416 name = fmt("%s %s...", typename(obj->type), shortrev);
395 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL); 417 reporevlink(page, name, NULL, NULL, ctx.qry.head, fullrev, NULL);
396} 418}
397 419
398void cgit_print_date(time_t secs, char *format, int local_time) 420void cgit_print_date(time_t secs, char *format, int local_time)
399{ 421{
400 char buf[64]; 422 char buf[64];
401 struct tm *time; 423 struct tm *time;
402 424
403 if (!secs) 425 if (!secs)
404 return; 426 return;
405 if(local_time) 427 if(local_time)
406 time = localtime(&secs); 428 time = localtime(&secs);
407 else 429 else
408 time = gmtime(&secs); 430 time = gmtime(&secs);
409 strftime(buf, sizeof(buf)-1, format, time); 431 strftime(buf, sizeof(buf)-1, format, time);
410 html_txt(buf); 432 html_txt(buf);
411} 433}
412 434
413void cgit_print_age(time_t t, time_t max_relative, char *format) 435void cgit_print_age(time_t t, time_t max_relative, char *format)
414{ 436{
415 time_t now, secs; 437 time_t now, secs;
416 438
417 if (!t) 439 if (!t)
418 return; 440 return;
419 time(&now); 441 time(&now);
420 secs = now - t; 442 secs = now - t;
421 443
422 if (secs > max_relative && max_relative >= 0) { 444 if (secs > max_relative && max_relative >= 0) {
423 cgit_print_date(t, format, ctx.cfg.local_time); 445 cgit_print_date(t, format, ctx.cfg.local_time);
424 return; 446 return;
425 } 447 }
426 448
427 if (secs < TM_HOUR * 2) { 449 if (secs < TM_HOUR * 2) {
428 htmlf("<span class='age-mins'>%.0f min.</span>", 450 htmlf("<span class='age-mins'>%.0f min.</span>",
429 secs * 1.0 / TM_MIN); 451 secs * 1.0 / TM_MIN);
430 return; 452 return;
431 } 453 }
432 if (secs < TM_DAY * 2) { 454 if (secs < TM_DAY * 2) {
433 htmlf("<span class='age-hours'>%.0f hours</span>", 455 htmlf("<span class='age-hours'>%.0f hours</span>",
434 secs * 1.0 / TM_HOUR); 456 secs * 1.0 / TM_HOUR);
435 return; 457 return;
436 } 458 }
437 if (secs < TM_WEEK * 2) { 459 if (secs < TM_WEEK * 2) {
438 htmlf("<span class='age-days'>%.0f days</span>", 460 htmlf("<span class='age-days'>%.0f days</span>",
439 secs * 1.0 / TM_DAY); 461 secs * 1.0 / TM_DAY);
440 return; 462 return;
441 } 463 }
442 if (secs < TM_MONTH * 2) { 464 if (secs < TM_MONTH * 2) {
443 htmlf("<span class='age-weeks'>%.0f weeks</span>", 465 htmlf("<span class='age-weeks'>%.0f weeks</span>",
444 secs * 1.0 / TM_WEEK); 466 secs * 1.0 / TM_WEEK);
445 return; 467 return;
446 } 468 }
447 if (secs < TM_YEAR * 2) { 469 if (secs < TM_YEAR * 2) {
448 htmlf("<span class='age-months'>%.0f months</span>", 470 htmlf("<span class='age-months'>%.0f months</span>",
449 secs * 1.0 / TM_MONTH); 471 secs * 1.0 / TM_MONTH);
450 return; 472 return;
451 } 473 }
452 htmlf("<span class='age-years'>%.0f years</span>", 474 htmlf("<span class='age-years'>%.0f years</span>",
453 secs * 1.0 / TM_YEAR); 475 secs * 1.0 / TM_YEAR);
454} 476}
455 477
456void cgit_print_http_headers(struct cgit_context *ctx) 478void cgit_print_http_headers(struct cgit_context *ctx)
457{ 479{
458 if (ctx->env.no_http && !strcmp(ctx->env.no_http, "1")) 480 if (ctx->env.no_http && !strcmp(ctx->env.no_http, "1"))
459 return; 481 return;
460 482
461 if (ctx->page.status) 483 if (ctx->page.status)
462 htmlf("Status: %d %s\n", ctx->page.status, ctx->page.statusmsg); 484 htmlf("Status: %d %s\n", ctx->page.status, ctx->page.statusmsg);
463 if (ctx->page.mimetype && ctx->page.charset) 485 if (ctx->page.mimetype && ctx->page.charset)
464 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype, 486 htmlf("Content-Type: %s; charset=%s\n", ctx->page.mimetype,
465 ctx->page.charset); 487 ctx->page.charset);
466 else if (ctx->page.mimetype) 488 else if (ctx->page.mimetype)
467 htmlf("Content-Type: %s\n", ctx->page.mimetype); 489 htmlf("Content-Type: %s\n", ctx->page.mimetype);
468 if (ctx->page.size) 490 if (ctx->page.size)
469 htmlf("Content-Length: %ld\n", ctx->page.size); 491 htmlf("Content-Length: %ld\n", ctx->page.size);
470 if (ctx->page.filename) 492 if (ctx->page.filename)
471 htmlf("Content-Disposition: inline; filename=\"%s\"\n", 493 htmlf("Content-Disposition: inline; filename=\"%s\"\n",
472 ctx->page.filename); 494 ctx->page.filename);
473 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified)); 495 htmlf("Last-Modified: %s\n", http_date(ctx->page.modified));
474 htmlf("Expires: %s\n", http_date(ctx->page.expires)); 496 htmlf("Expires: %s\n", http_date(ctx->page.expires));
475 if (ctx->page.etag) 497 if (ctx->page.etag)
476 htmlf("ETag: \"%s\"\n", ctx->page.etag); 498 htmlf("ETag: \"%s\"\n", ctx->page.etag);
477 html("\n"); 499 html("\n");
478 if (ctx->env.request_method && !strcmp(ctx->env.request_method, "HEAD")) 500 if (ctx->env.request_method && !strcmp(ctx->env.request_method, "HEAD"))
479 exit(0); 501 exit(0);
480} 502}
481 503
482void cgit_print_docstart(struct cgit_context *ctx) 504void cgit_print_docstart(struct cgit_context *ctx)
483{ 505{
484 if (ctx->cfg.embedded) { 506 if (ctx->cfg.embedded) {
485 if (ctx->cfg.header) 507 if (ctx->cfg.header)
486 html_include(ctx->cfg.header); 508 html_include(ctx->cfg.header);
487 return; 509 return;
488 } 510 }
489 511
490 char *host = cgit_hosturl(); 512 char *host = cgit_hosturl();
491 html(cgit_doctype); 513 html(cgit_doctype);
492 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n"); 514 html("<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>\n");
493 html("<head>\n"); 515 html("<head>\n");
494 html("<title>"); 516 html("<title>");
495 html_txt(ctx->page.title); 517 html_txt(ctx->page.title);
496 html("</title>\n"); 518 html("</title>\n");
497 htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version); 519 htmlf("<meta name='generator' content='cgit %s'/>\n", cgit_version);
498 if (ctx->cfg.robots && *ctx->cfg.robots) 520 if (ctx->cfg.robots && *ctx->cfg.robots)
499 htmlf("<meta name='robots' content='%s'/>\n", ctx->cfg.robots); 521 htmlf("<meta name='robots' content='%s'/>\n", ctx->cfg.robots);
500 html("<link rel='stylesheet' type='text/css' href='"); 522 html("<link rel='stylesheet' type='text/css' href='");
501 html_attr(ctx->cfg.css); 523 html_attr(ctx->cfg.css);
502 html("'/>\n"); 524 html("'/>\n");
503 if (ctx->cfg.favicon) { 525 if (ctx->cfg.favicon) {
504 html("<link rel='shortcut icon' href='"); 526 html("<link rel='shortcut icon' href='");
505 html_attr(ctx->cfg.favicon); 527 html_attr(ctx->cfg.favicon);
506 html("'/>\n"); 528 html("'/>\n");
507 } 529 }
508 if (host && ctx->repo) { 530 if (host && ctx->repo) {
509 html("<link rel='alternate' title='Atom feed' href='"); 531 html("<link rel='alternate' title='Atom feed' href='");
510 html(cgit_httpscheme()); 532 html(cgit_httpscheme());
511 html_attr(cgit_hosturl()); 533 html_attr(cgit_hosturl());
512 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path, 534 html_attr(cgit_fileurl(ctx->repo->url, "atom", ctx->qry.path,
513 fmt("h=%s", ctx->qry.head))); 535 fmt("h=%s", ctx->qry.head)));
514 html("' type='application/atom+xml'/>\n"); 536 html("' type='application/atom+xml'/>\n");
515 } 537 }
516 if (ctx->cfg.head_include) 538 if (ctx->cfg.head_include)
517 html_include(ctx->cfg.head_include); 539 html_include(ctx->cfg.head_include);
518 html("</head>\n"); 540 html("</head>\n");
519 html("<body>\n"); 541 html("<body>\n");
520 if (ctx->cfg.header) 542 if (ctx->cfg.header)
521 html_include(ctx->cfg.header); 543 html_include(ctx->cfg.header);
522} 544}
523 545
524void cgit_print_docend() 546void cgit_print_docend()
525{ 547{
526 html("</div> <!-- class=content -->\n"); 548 html("</div> <!-- class=content -->\n");
527 if (ctx.cfg.embedded) { 549 if (ctx.cfg.embedded) {
528 html("</div> <!-- id=cgit -->\n"); 550 html("</div> <!-- id=cgit -->\n");
529 if (ctx.cfg.footer) 551 if (ctx.cfg.footer)
530 html_include(ctx.cfg.footer); 552 html_include(ctx.cfg.footer);
531 return; 553 return;
532 } 554 }
533 if (ctx.cfg.footer) 555 if (ctx.cfg.footer)
534 html_include(ctx.cfg.footer); 556 html_include(ctx.cfg.footer);
535 else { 557 else {
536 htmlf("<div class='footer'>generated by cgit %s at ", 558 htmlf("<div class='footer'>generated by cgit %s at ",
537 cgit_version); 559 cgit_version);
538 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time); 560 cgit_print_date(time(NULL), FMT_LONGDATE, ctx.cfg.local_time);
539 html("</div>\n"); 561 html("</div>\n");
540 } 562 }
541 html("</div> <!-- id=cgit -->\n"); 563 html("</div> <!-- id=cgit -->\n");
542 html("</body>\n</html>\n"); 564 html("</body>\n</html>\n");
543} 565}
544 566
545int print_branch_option(const char *refname, const unsigned char *sha1, 567int print_branch_option(const char *refname, const unsigned char *sha1,
546 int flags, void *cb_data) 568 int flags, void *cb_data)
547{ 569{
548 char *name = (char *)refname; 570 char *name = (char *)refname;
549 html_option(name, name, ctx.qry.head); 571 html_option(name, name, ctx.qry.head);
550 return 0; 572 return 0;
551} 573}
552 574
553int print_archive_ref(const char *refname, const unsigned char *sha1, 575int print_archive_ref(const char *refname, const unsigned char *sha1,
554 int flags, void *cb_data) 576 int flags, void *cb_data)
555{ 577{
556 struct tag *tag; 578 struct tag *tag;
557 struct taginfo *info; 579 struct taginfo *info;
558 struct object *obj; 580 struct object *obj;
559 char buf[256], *url; 581 char buf[256], *url;
560 unsigned char fileid[20]; 582 unsigned char fileid[20];
561 int *header = (int *)cb_data; 583 int *header = (int *)cb_data;
562 584
563 if (prefixcmp(refname, "refs/archives")) 585 if (prefixcmp(refname, "refs/archives"))
564 return 0; 586 return 0;
565 strncpy(buf, refname+14, sizeof(buf)); 587 strncpy(buf, refname+14, sizeof(buf));
566 obj = parse_object(sha1); 588 obj = parse_object(sha1);
567 if (!obj) 589 if (!obj)
568 return 1; 590 return 1;
569 if (obj->type == OBJ_TAG) { 591 if (obj->type == OBJ_TAG) {
570 tag = lookup_tag(sha1); 592 tag = lookup_tag(sha1);
571 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag))) 593 if (!tag || parse_tag(tag) || !(info = cgit_parse_tag(tag)))
572 return 0; 594 return 0;
573 hashcpy(fileid, tag->tagged->sha1); 595 hashcpy(fileid, tag->tagged->sha1);
574 } else if (obj->type != OBJ_BLOB) { 596 } else if (obj->type != OBJ_BLOB) {
575 return 0; 597 return 0;
576 } else { 598 } else {
577 hashcpy(fileid, sha1); 599 hashcpy(fileid, sha1);
578 } 600 }
579 if (!*header) { 601 if (!*header) {
580 html("<h1>download</h1>\n"); 602 html("<h1>download</h1>\n");
581 *header = 1; 603 *header = 1;
582 } 604 }
583 url = cgit_pageurl(ctx.qry.repo, "blob", 605 url = cgit_pageurl(ctx.qry.repo, "blob",
584 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid), 606 fmt("id=%s&amp;path=%s", sha1_to_hex(fileid),
585 buf)); 607 buf));
586 html_link_open(url, NULL, "menu"); 608 html_link_open(url, NULL, "menu");
587 html_txt(strlpart(buf, 20)); 609 html_txt(strlpart(buf, 20));
588 html_link_close(); 610 html_link_close();
589 return 0; 611 return 0;
590} 612}
591 613
592void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page) 614void cgit_add_hidden_formfields(int incl_head, int incl_search, char *page)
593{ 615{
594 char *url; 616 char *url;
595 617
596 if (!ctx.cfg.virtual_root) { 618 if (!ctx.cfg.virtual_root) {
597 url = fmt("%s/%s", ctx.qry.repo, page); 619 url = fmt("%s/%s", ctx.qry.repo, page);
598 if (ctx.qry.path) 620 if (ctx.qry.path)
599 url = fmt("%s/%s", url, ctx.qry.path); 621 url = fmt("%s/%s", url, ctx.qry.path);
600 html_hidden("url", url); 622 html_hidden("url", url);
601 } 623 }
602 624
603 if (incl_head && ctx.qry.head && ctx.repo->defbranch && 625 if (incl_head && ctx.qry.head && ctx.repo->defbranch &&
604 strcmp(ctx.qry.head, ctx.repo->defbranch)) 626 strcmp(ctx.qry.head, ctx.repo->defbranch))
605 html_hidden("h", ctx.qry.head); 627 html_hidden("h", ctx.qry.head);
606 628
607 if (ctx.qry.sha1) 629 if (ctx.qry.sha1)
608 html_hidden("id", ctx.qry.sha1); 630 html_hidden("id", ctx.qry.sha1);
609 if (ctx.qry.sha2) 631 if (ctx.qry.sha2)
610 html_hidden("id2", ctx.qry.sha2); 632 html_hidden("id2", ctx.qry.sha2);
611 if (ctx.qry.showmsg) 633 if (ctx.qry.showmsg)
612 html_hidden("showmsg", "1"); 634 html_hidden("showmsg", "1");
613 635
614 if (incl_search) { 636 if (incl_search) {
615 if (ctx.qry.grep) 637 if (ctx.qry.grep)
616 html_hidden("qt", ctx.qry.grep); 638 html_hidden("qt", ctx.qry.grep);
617 if (ctx.qry.search) 639 if (ctx.qry.search)
618 html_hidden("q", ctx.qry.search); 640 html_hidden("q", ctx.qry.search);
619 } 641 }
620} 642}
621 643
622const char *fallback_cmd = "repolist"; 644const char *fallback_cmd = "repolist";
623 645
624char *hc(struct cgit_cmd *cmd, const char *page) 646char *hc(struct cgit_cmd *cmd, const char *page)
625{ 647{
626 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active"); 648 return (strcmp(cmd ? cmd->name : fallback_cmd, page) ? NULL : "active");
627} 649}
628 650
629static void print_header(struct cgit_context *ctx) 651static void print_header(struct cgit_context *ctx)
630{ 652{
631 html("<table id='header'>\n"); 653 html("<table id='header'>\n");
632 html("<tr>\n"); 654 html("<tr>\n");
633 655
634 if (ctx->cfg.logo && ctx->cfg.logo[0] != 0) { 656 if (ctx->cfg.logo && ctx->cfg.logo[0] != 0) {
635 html("<td class='logo' rowspan='2'><a href='"); 657 html("<td class='logo' rowspan='2'><a href='");
636 if (ctx->cfg.logo_link) 658 if (ctx->cfg.logo_link)
637 html_attr(ctx->cfg.logo_link); 659 html_attr(ctx->cfg.logo_link);
638 else 660 else
639 html_attr(cgit_rooturl()); 661 html_attr(cgit_rooturl());
640 html("'><img src='"); 662 html("'><img src='");
641 html_attr(ctx->cfg.logo); 663 html_attr(ctx->cfg.logo);
642 html("' alt='cgit logo'/></a></td>\n"); 664 html("' alt='cgit logo'/></a></td>\n");
643 } 665 }
644 666
645 html("<td class='main'>"); 667 html("<td class='main'>");
646 if (ctx->repo) { 668 if (ctx->repo) {
647 cgit_index_link("index", NULL, NULL, NULL, 0); 669 cgit_index_link("index", NULL, NULL, NULL, 0);
648 html(" : "); 670 html(" : ");
649 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL); 671 cgit_summary_link(ctx->repo->name, ctx->repo->name, NULL, NULL);
650 html("</td><td class='form'>"); 672 html("</td><td class='form'>");
651 html("<form method='get' action=''>\n"); 673 html("<form method='get' action=''>\n");
652 cgit_add_hidden_formfields(0, 1, ctx->qry.page); 674 cgit_add_hidden_formfields(0, 1, ctx->qry.page);
653 html("<select name='h' onchange='this.form.submit();'>\n"); 675 html("<select name='h' onchange='this.form.submit();'>\n");
654 for_each_branch_ref(print_branch_option, ctx->qry.head); 676 for_each_branch_ref(print_branch_option, ctx->qry.head);
655 html("</select> "); 677 html("</select> ");
656 html("<input type='submit' name='' value='switch'/>"); 678 html("<input type='submit' name='' value='switch'/>");
657 html("</form>"); 679 html("</form>");
658 } else 680 } else
659 html_txt(ctx->cfg.root_title); 681 html_txt(ctx->cfg.root_title);
660 html("</td></tr>\n"); 682 html("</td></tr>\n");
661 683
662 html("<tr><td class='sub'>"); 684 html("<tr><td class='sub'>");
663 if (ctx->repo) { 685 if (ctx->repo) {
664 html_txt(ctx->repo->desc); 686 html_txt(ctx->repo->desc);
665 html("</td><td class='sub right'>"); 687 html("</td><td class='sub right'>");
666 html_txt(ctx->repo->owner); 688 html_txt(ctx->repo->owner);
667 } else { 689 } else {
668 if (ctx->cfg.root_desc) 690 if (ctx->cfg.root_desc)
669 html_txt(ctx->cfg.root_desc); 691 html_txt(ctx->cfg.root_desc);
670 else if (ctx->cfg.index_info) 692 else if (ctx->cfg.index_info)
671 html_include(ctx->cfg.index_info); 693 html_include(ctx->cfg.index_info);
672 } 694 }
673 html("</td></tr></table>\n"); 695 html("</td></tr></table>\n");
674} 696}
675 697
676void cgit_print_pageheader(struct cgit_context *ctx) 698void cgit_print_pageheader(struct cgit_context *ctx)
677{ 699{
678 struct cgit_cmd *cmd = cgit_get_cmd(ctx); 700 struct cgit_cmd *cmd = cgit_get_cmd(ctx);
679 701
680 if (!cmd && ctx->repo) 702 if (!cmd && ctx->repo)
681 fallback_cmd = "summary"; 703 fallback_cmd = "summary";
682 704
683 html("<div id='cgit'>"); 705 html("<div id='cgit'>");
684 if (!ctx->cfg.noheader) 706 if (!ctx->cfg.noheader)
685 print_header(ctx); 707 print_header(ctx);
686 708
687 html("<table class='tabs'><tr><td>\n"); 709 html("<table class='tabs'><tr><td>\n");
688 if (ctx->repo) { 710 if (ctx->repo) {
689 cgit_summary_link("summary", NULL, hc(cmd, "summary"), 711 cgit_summary_link("summary", NULL, hc(cmd, "summary"),
690 ctx->qry.head); 712 ctx->qry.head);
691 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head, 713 cgit_refs_link("refs", NULL, hc(cmd, "refs"), ctx->qry.head,
692 ctx->qry.sha1, NULL); 714 ctx->qry.sha1, NULL);
693 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head, 715 cgit_log_link("log", NULL, hc(cmd, "log"), ctx->qry.head,
694 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg); 716 NULL, NULL, 0, NULL, NULL, ctx->qry.showmsg);
695 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head, 717 cgit_tree_link("tree", NULL, hc(cmd, "tree"), ctx->qry.head,
696 ctx->qry.sha1, NULL); 718 ctx->qry.sha1, NULL);
697 cgit_commit_link("commit", NULL, hc(cmd, "commit"), 719 cgit_commit_link("commit", NULL, hc(cmd, "commit"),
698 ctx->qry.head, ctx->qry.sha1); 720 ctx->qry.head, ctx->qry.sha1, 0);
699 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head, 721 cgit_diff_link("diff", NULL, hc(cmd, "diff"), ctx->qry.head,
700 ctx->qry.sha1, ctx->qry.sha2, NULL); 722 ctx->qry.sha1, ctx->qry.sha2, NULL, 0);
701 if (ctx->repo->max_stats) 723 if (ctx->repo->max_stats)
702 cgit_stats_link("stats", NULL, hc(cmd, "stats"), 724 cgit_stats_link("stats", NULL, hc(cmd, "stats"),
703 ctx->qry.head, NULL); 725 ctx->qry.head, NULL);
704 if (ctx->repo->readme) 726 if (ctx->repo->readme)
705 reporevlink("about", "about", NULL, 727 reporevlink("about", "about", NULL,
706 hc(cmd, "about"), ctx->qry.head, NULL, 728 hc(cmd, "about"), ctx->qry.head, NULL,
707 NULL); 729 NULL);
708 html("</td><td class='form'>"); 730 html("</td><td class='form'>");
709 html("<form class='right' method='get' action='"); 731 html("<form class='right' method='get' action='");
710 if (ctx->cfg.virtual_root) 732 if (ctx->cfg.virtual_root)
711 html_url_path(cgit_fileurl(ctx->qry.repo, "log", 733 html_url_path(cgit_fileurl(ctx->qry.repo, "log",
712 ctx->qry.path, NULL)); 734 ctx->qry.path, NULL));
713 html("'>\n"); 735 html("'>\n");
714 cgit_add_hidden_formfields(1, 0, "log"); 736 cgit_add_hidden_formfields(1, 0, "log");
715 html("<select name='qt'>\n"); 737 html("<select name='qt'>\n");
716 html_option("grep", "log msg", ctx->qry.grep); 738 html_option("grep", "log msg", ctx->qry.grep);
717 html_option("author", "author", ctx->qry.grep); 739 html_option("author", "author", ctx->qry.grep);
718 html_option("committer", "committer", ctx->qry.grep); 740 html_option("committer", "committer", ctx->qry.grep);
719 html("</select>\n"); 741 html("</select>\n");
720 html("<input class='txt' type='text' size='10' name='q' value='"); 742 html("<input class='txt' type='text' size='10' name='q' value='");
721 html_attr(ctx->qry.search); 743 html_attr(ctx->qry.search);
722 html("'/>\n"); 744 html("'/>\n");
723 html("<input type='submit' value='search'/>\n"); 745 html("<input type='submit' value='search'/>\n");
724 html("</form>\n"); 746 html("</form>\n");
725 } else { 747 } else {
726 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0); 748 site_link(NULL, "index", NULL, hc(cmd, "repolist"), NULL, 0);
727 if (ctx->cfg.root_readme) 749 if (ctx->cfg.root_readme)
728 site_link("about", "about", NULL, hc(cmd, "about"), 750 site_link("about", "about", NULL, hc(cmd, "about"),
729 NULL, 0); 751 NULL, 0);
730 html("</td><td class='form'>"); 752 html("</td><td class='form'>");
731 html("<form method='get' action='"); 753 html("<form method='get' action='");
732 html_attr(cgit_rooturl()); 754 html_attr(cgit_rooturl());
733 html("'>\n"); 755 html("'>\n");
734 html("<input type='text' name='q' size='10' value='"); 756 html("<input type='text' name='q' size='10' value='");
735 html_attr(ctx->qry.search); 757 html_attr(ctx->qry.search);
736 html("'/>\n"); 758 html("'/>\n");
737 html("<input type='submit' value='search'/>\n"); 759 html("<input type='submit' value='search'/>\n");
738 html("</form>"); 760 html("</form>");
739 } 761 }
740 html("</td></tr></table>\n"); 762 html("</td></tr></table>\n");
741 html("<div class='content'>"); 763 html("<div class='content'>");
742} 764}
743 765
744void cgit_print_filemode(unsigned short mode) 766void cgit_print_filemode(unsigned short mode)
745{ 767{
746 if (S_ISDIR(mode)) 768 if (S_ISDIR(mode))
747 html("d"); 769 html("d");
748 else if (S_ISLNK(mode)) 770 else if (S_ISLNK(mode))
749 html("l"); 771 html("l");
750 else if (S_ISGITLINK(mode)) 772 else if (S_ISGITLINK(mode))
751 html("m"); 773 html("m");
752 else 774 else
753 html("-"); 775 html("-");
754 html_fileperm(mode >> 6); 776 html_fileperm(mode >> 6);
755 html_fileperm(mode >> 3); 777 html_fileperm(mode >> 3);
756 html_fileperm(mode); 778 html_fileperm(mode);
757} 779}
758 780
759void cgit_print_snapshot_links(const char *repo, const char *head, 781void cgit_print_snapshot_links(const char *repo, const char *head,
760 const char *hex, int snapshots) 782 const char *hex, int snapshots)
761{ 783{
762 const struct cgit_snapshot_format* f; 784 const struct cgit_snapshot_format* f;
763 char *prefix; 785 char *prefix;
764 char *filename; 786 char *filename;
765 unsigned char sha1[20]; 787 unsigned char sha1[20];
766 788
767 if (get_sha1(fmt("refs/tags/%s", hex), sha1) == 0 && 789 if (get_sha1(fmt("refs/tags/%s", hex), sha1) == 0 &&
768 (hex[0] == 'v' || hex[0] == 'V') && isdigit(hex[1])) 790 (hex[0] == 'v' || hex[0] == 'V') && isdigit(hex[1]))
769 hex++; 791 hex++;
770 prefix = xstrdup(fmt("%s-%s", cgit_repobasename(repo), hex)); 792 prefix = xstrdup(fmt("%s-%s", cgit_repobasename(repo), hex));
771 for (f = cgit_snapshot_formats; f->suffix; f++) { 793 for (f = cgit_snapshot_formats; f->suffix; f++) {
772 if (!(snapshots & f->bit)) 794 if (!(snapshots & f->bit))
773 continue; 795 continue;
774 filename = fmt("%s%s", prefix, f->suffix); 796 filename = fmt("%s%s", prefix, f->suffix);
775 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename); 797 cgit_snapshot_link(filename, NULL, NULL, NULL, NULL, filename);
776 html("<br/>"); 798 html("<br/>");
777 } 799 }
778} 800}
diff --git a/ui-shared.h b/ui-shared.h
index b12aa89..9ebc1f9 100644
--- a/ui-shared.h
+++ b/ui-shared.h
@@ -1,51 +1,52 @@
1#ifndef UI_SHARED_H 1#ifndef UI_SHARED_H
2#define UI_SHARED_H 2#define UI_SHARED_H
3 3
4extern char *cgit_httpscheme(); 4extern char *cgit_httpscheme();
5extern char *cgit_hosturl(); 5extern char *cgit_hosturl();
6extern char *cgit_rooturl(); 6extern char *cgit_rooturl();
7extern char *cgit_repourl(const char *reponame); 7extern char *cgit_repourl(const char *reponame);
8extern char *cgit_fileurl(const char *reponame, const char *pagename, 8extern char *cgit_fileurl(const char *reponame, const char *pagename,
9 const char *filename, const char *query); 9 const char *filename, const char *query);
10extern char *cgit_pageurl(const char *reponame, const char *pagename, 10extern char *cgit_pageurl(const char *reponame, const char *pagename,
11 const char *query); 11 const char *query);
12 12
13extern void cgit_index_link(char *name, char *title, char *class, 13extern void cgit_index_link(char *name, char *title, char *class,
14 char *pattern, int ofs); 14 char *pattern, int ofs);
15extern void cgit_summary_link(char *name, char *title, char *class, char *head); 15extern void cgit_summary_link(char *name, char *title, char *class, char *head);
16extern void cgit_tag_link(char *name, char *title, char *class, char *head, 16extern void cgit_tag_link(char *name, char *title, char *class, char *head,
17 char *rev); 17 char *rev);
18extern void cgit_tree_link(char *name, char *title, char *class, char *head, 18extern void cgit_tree_link(char *name, char *title, char *class, char *head,
19 char *rev, char *path); 19 char *rev, char *path);
20extern void cgit_plain_link(char *name, char *title, char *class, char *head, 20extern void cgit_plain_link(char *name, char *title, char *class, char *head,
21 char *rev, char *path); 21 char *rev, char *path);
22extern void cgit_log_link(char *name, char *title, char *class, char *head, 22extern void cgit_log_link(char *name, char *title, char *class, char *head,
23 char *rev, char *path, int ofs, char *grep, 23 char *rev, char *path, int ofs, char *grep,
24 char *pattern, int showmsg); 24 char *pattern, int showmsg);
25extern void cgit_commit_link(char *name, char *title, char *class, char *head, 25extern void cgit_commit_link(char *name, char *title, char *class, char *head,
26 char *rev); 26 char *rev, int toggle_ssdiff);
27extern void cgit_patch_link(char *name, char *title, char *class, char *head, 27extern void cgit_patch_link(char *name, char *title, char *class, char *head,
28 char *rev); 28 char *rev);
29extern void cgit_refs_link(char *name, char *title, char *class, char *head, 29extern void cgit_refs_link(char *name, char *title, char *class, char *head,
30 char *rev, char *path); 30 char *rev, char *path);
31extern void cgit_snapshot_link(char *name, char *title, char *class, 31extern void cgit_snapshot_link(char *name, char *title, char *class,
32 char *head, char *rev, char *archivename); 32 char *head, char *rev, char *archivename);
33extern void cgit_diff_link(char *name, char *title, char *class, char *head, 33extern void cgit_diff_link(char *name, char *title, char *class, char *head,
34 char *new_rev, char *old_rev, char *path); 34 char *new_rev, char *old_rev, char *path,
35 int toggle_ssdiff);
35extern void cgit_stats_link(char *name, char *title, char *class, char *head, 36extern void cgit_stats_link(char *name, char *title, char *class, char *head,
36 char *path); 37 char *path);
37extern void cgit_object_link(struct object *obj); 38extern void cgit_object_link(struct object *obj);
38 39
39extern void cgit_print_error(char *msg); 40extern void cgit_print_error(char *msg);
40extern void cgit_print_date(time_t secs, char *format, int local_time); 41extern void cgit_print_date(time_t secs, char *format, int local_time);
41extern void cgit_print_age(time_t t, time_t max_relative, char *format); 42extern void cgit_print_age(time_t t, time_t max_relative, char *format);
42extern void cgit_print_http_headers(struct cgit_context *ctx); 43extern void cgit_print_http_headers(struct cgit_context *ctx);
43extern void cgit_print_docstart(struct cgit_context *ctx); 44extern void cgit_print_docstart(struct cgit_context *ctx);
44extern void cgit_print_docend(); 45extern void cgit_print_docend();
45extern void cgit_print_pageheader(struct cgit_context *ctx); 46extern void cgit_print_pageheader(struct cgit_context *ctx);
46extern void cgit_print_filemode(unsigned short mode); 47extern void cgit_print_filemode(unsigned short mode);
47extern void cgit_print_snapshot_links(const char *repo, const char *head, 48extern void cgit_print_snapshot_links(const char *repo, const char *head,
48 const char *hex, int snapshots); 49 const char *hex, int snapshots);
49extern void cgit_add_hidden_formfields(int incl_head, int incl_search, 50extern void cgit_add_hidden_formfields(int incl_head, int incl_search,
50 char *page); 51 char *page);
51#endif /* UI_SHARED_H */ 52#endif /* UI_SHARED_H */
diff --git a/ui-ssdiff.c b/ui-ssdiff.c
new file mode 100644
index 0000000..408e620
--- a/dev/null
+++ b/ui-ssdiff.c
@@ -0,0 +1,369 @@
1#include "cgit.h"
2#include "html.h"
3#include "ui-shared.h"
4
5extern int use_ssdiff;
6
7static int current_old_line, current_new_line;
8
9struct deferred_lines {
10 int line_no;
11 char *line;
12 struct deferred_lines *next;
13};
14
15static struct deferred_lines *deferred_old, *deferred_old_last;
16static struct deferred_lines *deferred_new, *deferred_new_last;
17
18static char *longest_common_subsequence(char *A, char *B)
19{
20 int i, j, ri;
21 int m = strlen(A);
22 int n = strlen(B);
23 int L[m + 1][n + 1];
24 int tmp1, tmp2;
25 int lcs_length;
26 char *result;
27
28 for (i = m; i >= 0; i--) {
29 for (j = n; j >= 0; j--) {
30 if (A[i] == '\0' || B[j] == '\0') {
31 L[i][j] = 0;
32 } else if (A[i] == B[j]) {
33 L[i][j] = 1 + L[i + 1][j + 1];
34 } else {
35 tmp1 = L[i + 1][j];
36 tmp2 = L[i][j + 1];
37 L[i][j] = (tmp1 > tmp2 ? tmp1 : tmp2);
38 }
39 }
40 }
41
42 lcs_length = L[0][0];
43 result = xmalloc(lcs_length + 2);
44 memset(result, 0, sizeof(*result) * (lcs_length + 2));
45
46 ri = 0;
47 i = 0;
48 j = 0;
49 while (i < m && j < n) {
50 if (A[i] == B[j]) {
51 result[ri] = A[i];
52 ri += 1;
53 i += 1;
54 j += 1;
55 } else if (L[i + 1][j] >= L[i][j + 1]) {
56 i += 1;
57 } else {
58 j += 1;
59 }
60 }
61 return result;
62}
63
64static int line_from_hunk(char *line, char type)
65{
66 char *buf1, *buf2;
67 int len;
68
69 buf1 = strchr(line, type);
70 if (buf1 == NULL)
71 return 0;
72 buf1 += 1;
73 buf2 = strchr(buf1, ',');
74 if (buf2 == NULL)
75 return 0;
76 len = buf2 - buf1;
77 buf2 = xmalloc(len + 1);
78 strncpy(buf2, buf1, len);
79 buf2[len] = '\0';
80 int res = atoi(buf2);
81 free(buf2);
82 return res;
83}
84
85static char *replace_tabs(char *line)
86{
87 char *prev_buf = line;
88 char *cur_buf;
89 int linelen = strlen(line);
90 int n_tabs = 0;
91 int i;
92 char *result;
93 char *spaces = " ";
94
95 if (linelen == 0) {
96 result = xmalloc(1);
97 result[0] = '\0';
98 return result;
99 }
100
101 for (i = 0; i < linelen; i++)
102 if (line[i] == '\t')
103 n_tabs += 1;
104 result = xmalloc(linelen + n_tabs * 8 + 1);
105 result[0] = '\0';
106
107 while (1) {
108 cur_buf = strchr(prev_buf, '\t');
109 if (!cur_buf) {
110 strcat(result, prev_buf);
111 break;
112 } else {
113 strcat(result, " ");
114 strncat(result, spaces, 8 - (strlen(result) % 8));
115 strncat(result, prev_buf, cur_buf - prev_buf);
116 }
117 prev_buf = cur_buf + 1;
118 }
119 return result;
120}
121
122static int calc_deferred_lines(struct deferred_lines *start)
123{
124 struct deferred_lines *item = start;
125 int result = 0;
126 while (item) {
127 result += 1;
128 item = item->next;
129 }
130 return result;
131}
132
133static void deferred_old_add(char *line, int line_no)
134{
135 struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
136 item->line = xstrdup(line);
137 item->line_no = line_no;
138 item->next = NULL;
139 if (deferred_old) {
140 deferred_old_last->next = item;
141 deferred_old_last = item;
142 } else {
143 deferred_old = deferred_old_last = item;
144 }
145}
146
147static void deferred_new_add(char *line, int line_no)
148{
149 struct deferred_lines *item = xmalloc(sizeof(struct deferred_lines));
150 item->line = xstrdup(line);
151 item->line_no = line_no;
152 item->next = NULL;
153 if (deferred_new) {
154 deferred_new_last->next = item;
155 deferred_new_last = item;
156 } else {
157 deferred_new = deferred_new_last = item;
158 }
159}
160
161static void print_part_with_lcs(char *class, char *line, char *lcs)
162{
163 int line_len = strlen(line);
164 int i, j;
165 char c[2] = " ";
166 int same = 1;
167
168 j = 0;
169 for (i = 0; i < line_len; i++) {
170 c[0] = line[i];
171 if (same) {
172 if (line[i] == lcs[j])
173 j += 1;
174 else {
175 same = 0;
176 htmlf("<span class='%s'>", class);
177 }
178 } else if (line[i] == lcs[j]) {
179 same = 1;
180 htmlf("</span>");
181 j += 1;
182 }
183 html_txt(c);
184 }
185}
186
187static void print_ssdiff_line(char *class,
188 int old_line_no,
189 char *old_line,
190 int new_line_no,
191 char *new_line, int individual_chars)
192{
193 char *lcs = NULL;
194 if (old_line)
195 old_line = replace_tabs(old_line + 1);
196 if (new_line)
197 new_line = replace_tabs(new_line + 1);
198 if (individual_chars && old_line && new_line)
199 lcs = longest_common_subsequence(old_line, new_line);
200 html("<tr>");
201 if (old_line_no > 0)
202 htmlf("<td class='lineno'>%d</td><td class='%s'>",
203 old_line_no, class);
204 else if (old_line)
205 htmlf("<td class='lineno'></td><td class='%s'>", class);
206 else
207 htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
208 if (old_line) {
209 if (lcs)
210 print_part_with_lcs("del", old_line, lcs);
211 else
212 html_txt(old_line);
213 }
214
215 html("</td>");
216 if (new_line_no > 0)
217 htmlf("<td class='lineno'>%d</td><td class='%s'>",
218 new_line_no, class);
219 else if (new_line)
220 htmlf("<td class='lineno'></td><td class='%s'>", class);
221 else
222 htmlf("<td class='lineno'></td><td class='%s_dark'>", class);
223 if (new_line) {
224 if (lcs)
225 print_part_with_lcs("add", new_line, lcs);
226 else
227 html_txt(new_line);
228 }
229
230 html("</td></tr>");
231 if (lcs)
232 free(lcs);
233 if (new_line)
234 free(new_line);
235 if (old_line)
236 free(old_line);
237}
238
239static void print_deferred_old_lines()
240{
241 struct deferred_lines *iter_old, *tmp;
242 iter_old = deferred_old;
243 while (iter_old) {
244 print_ssdiff_line("del", iter_old->line_no,
245 iter_old->line, -1, NULL, 0);
246 tmp = iter_old->next;
247 free(iter_old);
248 iter_old = tmp;
249 }
250}
251
252static void print_deferred_new_lines()
253{
254 struct deferred_lines *iter_new, *tmp;
255 iter_new = deferred_new;
256 while (iter_new) {
257 print_ssdiff_line("add", -1, NULL,
258 iter_new->line_no, iter_new->line, 0);
259 tmp = iter_new->next;
260 free(iter_new);
261 iter_new = tmp;
262 }
263}
264
265static void print_deferred_changed_lines()
266{
267 struct deferred_lines *iter_old, *iter_new, *tmp;
268 int n_old_lines = calc_deferred_lines(deferred_old);
269 int n_new_lines = calc_deferred_lines(deferred_new);
270 int individual_chars = (n_old_lines == n_new_lines ? 1 : 0);
271
272 iter_old = deferred_old;
273 iter_new = deferred_new;
274 while (iter_old || iter_new) {
275 if (iter_old && iter_new)
276 print_ssdiff_line("changed", iter_old->line_no,
277 iter_old->line,
278 iter_new->line_no, iter_new->line,
279 individual_chars);
280 else if (iter_old)
281 print_ssdiff_line("changed", iter_old->line_no,
282 iter_old->line, -1, NULL, 0);
283 else if (iter_new)
284 print_ssdiff_line("changed", -1, NULL,
285 iter_new->line_no, iter_new->line, 0);
286 if (iter_old) {
287 tmp = iter_old->next;
288 free(iter_old);
289 iter_old = tmp;
290 }
291
292 if (iter_new) {
293 tmp = iter_new->next;
294 free(iter_new);
295 iter_new = tmp;
296 }
297 }
298}
299
300void cgit_ssdiff_print_deferred_lines()
301{
302 if (!deferred_old && !deferred_new)
303 return;
304 if (deferred_old && !deferred_new)
305 print_deferred_old_lines();
306 else if (!deferred_old && deferred_new)
307 print_deferred_new_lines();
308 else
309 print_deferred_changed_lines();
310 deferred_old = deferred_old_last = NULL;
311 deferred_new = deferred_new_last = NULL;
312}
313
314/*
315 * print a single line returned from xdiff
316 */
317void cgit_ssdiff_line_cb(char *line, int len)
318{
319 char c = line[len - 1];
320 line[len - 1] = '\0';
321 if (line[0] == '@') {
322 current_old_line = line_from_hunk(line, '-');
323 current_new_line = line_from_hunk(line, '+');
324 }
325
326 if (line[0] == ' ') {
327 if (deferred_old || deferred_new)
328 cgit_ssdiff_print_deferred_lines();
329 print_ssdiff_line("ctx", current_old_line, line,
330 current_new_line, line, 0);
331 current_old_line += 1;
332 current_new_line += 1;
333 } else if (line[0] == '+') {
334 deferred_new_add(line, current_new_line);
335 current_new_line += 1;
336 } else if (line[0] == '-') {
337 deferred_old_add(line, current_old_line);
338 current_old_line += 1;
339 } else if (line[0] == '@') {
340 html("<tr><td colspan='4' class='hunk'>");
341 html_txt(line);
342 html("</td></tr>");
343 } else {
344 html("<tr><td colspan='4' class='ctx'>");
345 html_txt(line);
346 html("</td></tr>");
347 }
348 line[len - 1] = c;
349}
350
351void cgit_ssdiff_header_begin()
352{
353 current_old_line = -1;
354 current_new_line = -1;
355 html("<tr><td class='space' colspan='4'><div></div></td></tr>");
356 html("<tr><td class='head' colspan='4'>");
357}
358
359void cgit_ssdiff_header_end()
360{
361 html("</td><tr>");
362}
363
364void cgit_ssdiff_footer()
365{
366 if (deferred_old || deferred_new)
367 cgit_ssdiff_print_deferred_lines();
368 html("<tr><td class='foot' colspan='4'></td></tr>");
369}
diff --git a/ui-ssdiff.h b/ui-ssdiff.h
new file mode 100644
index 0000000..64b4b12
--- a/dev/null
+++ b/ui-ssdiff.h
@@ -0,0 +1,13 @@
1#ifndef UI_SSDIFF_H
2#define UI_SSDIFF_H
3
4extern void cgit_ssdiff_print_deferred_lines();
5
6extern void cgit_ssdiff_line_cb(char *line, int len);
7
8extern void cgit_ssdiff_header_begin();
9extern void cgit_ssdiff_header_end();
10
11extern void cgit_ssdiff_footer();
12
13#endif /* UI_SSDIFF_H */