##// END OF EJS Templates
hgweb: move archivelist() of hgwebdir to webutil
Yuya Nishihara -
r37531:40a7c1dd default
parent child Browse files
Show More
@@ -1,542 +1,526 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import gc
11 import gc
12 import os
12 import os
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_SERVER_ERROR,
19 HTTP_SERVER_ERROR,
20 cspvalues,
20 cspvalues,
21 get_contact,
21 get_contact,
22 get_mtime,
22 get_mtime,
23 ismember,
23 ismember,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 statusmessage,
26 statusmessage,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 configitems,
30 configitems,
31 encoding,
31 encoding,
32 error,
32 error,
33 hg,
33 hg,
34 profiling,
34 profiling,
35 pycompat,
35 pycompat,
36 scmutil,
36 scmutil,
37 templater,
37 templater,
38 templateutil,
38 templateutil,
39 ui as uimod,
39 ui as uimod,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 hgweb_mod,
44 hgweb_mod,
45 request as requestmod,
45 request as requestmod,
46 webutil,
46 webutil,
47 wsgicgi,
47 wsgicgi,
48 )
48 )
49 from ..utils import dateutil
49 from ..utils import dateutil
50
50
51 def cleannames(items):
51 def cleannames(items):
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53
53
54 def findrepos(paths):
54 def findrepos(paths):
55 repos = []
55 repos = []
56 for prefix, root in cleannames(paths):
56 for prefix, root in cleannames(paths):
57 roothead, roottail = os.path.split(root)
57 roothead, roottail = os.path.split(root)
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 # /bar/ be served as as foo/N .
59 # /bar/ be served as as foo/N .
60 # '*' will not search inside dirs with .hg (except .hg/patches),
60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 try:
62 try:
63 recurse = {'*': False, '**': True}[roottail]
63 recurse = {'*': False, '**': True}[roottail]
64 except KeyError:
64 except KeyError:
65 repos.append((prefix, root))
65 repos.append((prefix, root))
66 continue
66 continue
67 roothead = os.path.normpath(os.path.abspath(roothead))
67 roothead = os.path.normpath(os.path.abspath(roothead))
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 repos.extend(urlrepos(prefix, roothead, paths))
69 repos.extend(urlrepos(prefix, roothead, paths))
70 return repos
70 return repos
71
71
72 def urlrepos(prefix, roothead, paths):
72 def urlrepos(prefix, roothead, paths):
73 """yield url paths and filesystem paths from a list of repo paths
73 """yield url paths and filesystem paths from a list of repo paths
74
74
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 """
80 """
81 for path in paths:
81 for path in paths:
82 path = os.path.normpath(path)
82 path = os.path.normpath(path)
83 yield (prefix + '/' +
83 yield (prefix + '/' +
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85
85
86 def readallowed(ui, req):
86 def readallowed(ui, req):
87 """Check allow_read and deny_read config options of a repo's ui object
87 """Check allow_read and deny_read config options of a repo's ui object
88 to determine user permissions. By default, with neither option set (or
88 to determine user permissions. By default, with neither option set (or
89 both empty), allow all users to read the repo. There are two ways a
89 both empty), allow all users to read the repo. There are two ways a
90 user can be denied read access: (1) deny_read is not empty, and the
90 user can be denied read access: (1) deny_read is not empty, and the
91 user is unauthenticated or deny_read contains user (or *), and (2)
91 user is unauthenticated or deny_read contains user (or *), and (2)
92 allow_read is not empty and the user is not in allow_read. Return True
92 allow_read is not empty and the user is not in allow_read. Return True
93 if user is allowed to read the repo, else return False."""
93 if user is allowed to read the repo, else return False."""
94
94
95 user = req.remoteuser
95 user = req.remoteuser
96
96
97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 if deny_read and (not user or ismember(ui, user, deny_read)):
98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 return False
99 return False
100
100
101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 # by default, allow reading if no allow_read option has been set
102 # by default, allow reading if no allow_read option has been set
103 if not allow_read or ismember(ui, user, allow_read):
103 if not allow_read or ismember(ui, user, allow_read):
104 return True
104 return True
105
105
106 return False
106 return False
107
107
108 def archivelist(ui, nodeid, url):
109 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
110 archives = []
111
112 for typ, spec in webutil.archivespecs.iteritems():
113 if typ in allowed or ui.configbool('web', 'allow' + typ,
114 untrusted=True):
115 archives.append({
116 'type': typ,
117 'extension': spec[2],
118 'node': nodeid,
119 'url': url,
120 })
121
122 return archives
123
124 def rawindexentries(ui, repos, req, subdir=''):
108 def rawindexentries(ui, repos, req, subdir=''):
125 descend = ui.configbool('web', 'descend')
109 descend = ui.configbool('web', 'descend')
126 collapse = ui.configbool('web', 'collapse')
110 collapse = ui.configbool('web', 'collapse')
127 seenrepos = set()
111 seenrepos = set()
128 seendirs = set()
112 seendirs = set()
129 for name, path in repos:
113 for name, path in repos:
130
114
131 if not name.startswith(subdir):
115 if not name.startswith(subdir):
132 continue
116 continue
133 name = name[len(subdir):]
117 name = name[len(subdir):]
134 directory = False
118 directory = False
135
119
136 if '/' in name:
120 if '/' in name:
137 if not descend:
121 if not descend:
138 continue
122 continue
139
123
140 nameparts = name.split('/')
124 nameparts = name.split('/')
141 rootname = nameparts[0]
125 rootname = nameparts[0]
142
126
143 if not collapse:
127 if not collapse:
144 pass
128 pass
145 elif rootname in seendirs:
129 elif rootname in seendirs:
146 continue
130 continue
147 elif rootname in seenrepos:
131 elif rootname in seenrepos:
148 pass
132 pass
149 else:
133 else:
150 directory = True
134 directory = True
151 name = rootname
135 name = rootname
152
136
153 # redefine the path to refer to the directory
137 # redefine the path to refer to the directory
154 discarded = '/'.join(nameparts[1:])
138 discarded = '/'.join(nameparts[1:])
155
139
156 # remove name parts plus accompanying slash
140 # remove name parts plus accompanying slash
157 path = path[:-len(discarded) - 1]
141 path = path[:-len(discarded) - 1]
158
142
159 try:
143 try:
160 r = hg.repository(ui, path)
144 r = hg.repository(ui, path)
161 directory = False
145 directory = False
162 except (IOError, error.RepoError):
146 except (IOError, error.RepoError):
163 pass
147 pass
164
148
165 parts = [
149 parts = [
166 req.apppath.strip('/'),
150 req.apppath.strip('/'),
167 subdir.strip('/'),
151 subdir.strip('/'),
168 name.strip('/'),
152 name.strip('/'),
169 ]
153 ]
170 url = '/' + '/'.join(p for p in parts if p) + '/'
154 url = '/' + '/'.join(p for p in parts if p) + '/'
171
155
172 # show either a directory entry or a repository
156 # show either a directory entry or a repository
173 if directory:
157 if directory:
174 # get the directory's time information
158 # get the directory's time information
175 try:
159 try:
176 d = (get_mtime(path), dateutil.makedate()[1])
160 d = (get_mtime(path), dateutil.makedate()[1])
177 except OSError:
161 except OSError:
178 continue
162 continue
179
163
180 # add '/' to the name to make it obvious that
164 # add '/' to the name to make it obvious that
181 # the entry is a directory, not a regular repository
165 # the entry is a directory, not a regular repository
182 row = {'contact': "",
166 row = {'contact': "",
183 'contact_sort': "",
167 'contact_sort': "",
184 'name': name + '/',
168 'name': name + '/',
185 'name_sort': name,
169 'name_sort': name,
186 'url': url,
170 'url': url,
187 'description': "",
171 'description': "",
188 'description_sort': "",
172 'description_sort': "",
189 'lastchange': d,
173 'lastchange': d,
190 'lastchange_sort': d[1] - d[0],
174 'lastchange_sort': d[1] - d[0],
191 'archives': [],
175 'archives': [],
192 'isdirectory': True,
176 'isdirectory': True,
193 'labels': templateutil.hybridlist([], name='label'),
177 'labels': templateutil.hybridlist([], name='label'),
194 }
178 }
195
179
196 seendirs.add(name)
180 seendirs.add(name)
197 yield row
181 yield row
198 continue
182 continue
199
183
200 u = ui.copy()
184 u = ui.copy()
201 try:
185 try:
202 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
203 except Exception as e:
187 except Exception as e:
204 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
205 continue
189 continue
206
190
207 def get(section, name, default=uimod._unset):
191 def get(section, name, default=uimod._unset):
208 return u.config(section, name, default, untrusted=True)
192 return u.config(section, name, default, untrusted=True)
209
193
210 if u.configbool("web", "hidden", untrusted=True):
194 if u.configbool("web", "hidden", untrusted=True):
211 continue
195 continue
212
196
213 if not readallowed(u, req):
197 if not readallowed(u, req):
214 continue
198 continue
215
199
216 # update time with local timezone
200 # update time with local timezone
217 try:
201 try:
218 r = hg.repository(ui, path)
202 r = hg.repository(ui, path)
219 except IOError:
203 except IOError:
220 u.warn(_('error accessing repository at %s\n') % path)
204 u.warn(_('error accessing repository at %s\n') % path)
221 continue
205 continue
222 except error.RepoError:
206 except error.RepoError:
223 u.warn(_('error accessing repository at %s\n') % path)
207 u.warn(_('error accessing repository at %s\n') % path)
224 continue
208 continue
225 try:
209 try:
226 d = (get_mtime(r.spath), dateutil.makedate()[1])
210 d = (get_mtime(r.spath), dateutil.makedate()[1])
227 except OSError:
211 except OSError:
228 continue
212 continue
229
213
230 contact = get_contact(get)
214 contact = get_contact(get)
231 description = get("web", "description")
215 description = get("web", "description")
232 seenrepos.add(name)
216 seenrepos.add(name)
233 name = get("web", "name", name)
217 name = get("web", "name", name)
234 labels = u.configlist('web', 'labels', untrusted=True)
218 labels = u.configlist('web', 'labels', untrusted=True)
235 row = {'contact': contact or "unknown",
219 row = {'contact': contact or "unknown",
236 'contact_sort': contact.upper() or "unknown",
220 'contact_sort': contact.upper() or "unknown",
237 'name': name,
221 'name': name,
238 'name_sort': name,
222 'name_sort': name,
239 'url': url,
223 'url': url,
240 'description': description or "unknown",
224 'description': description or "unknown",
241 'description_sort': description.upper() or "unknown",
225 'description_sort': description.upper() or "unknown",
242 'lastchange': d,
226 'lastchange': d,
243 'lastchange_sort': d[1] - d[0],
227 'lastchange_sort': d[1] - d[0],
244 'archives': archivelist(u, "tip", url),
228 'archives': webutil.archivelist(u, "tip", url),
245 'isdirectory': None,
229 'isdirectory': None,
246 'labels': templateutil.hybridlist(labels, name='label'),
230 'labels': templateutil.hybridlist(labels, name='label'),
247 }
231 }
248
232
249 yield row
233 yield row
250
234
251 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
252 descending, subdir):
236 descending, subdir):
253 rows = rawindexentries(ui, repos, req, subdir=subdir)
237 rows = rawindexentries(ui, repos, req, subdir=subdir)
254
238
255 sortdefault = None, False
239 sortdefault = None, False
256
240
257 if sortcolumn and sortdefault != (sortcolumn, descending):
241 if sortcolumn and sortdefault != (sortcolumn, descending):
258 sortkey = '%s_sort' % sortcolumn
242 sortkey = '%s_sort' % sortcolumn
259 rows = sorted(rows, key=lambda x: x[sortkey],
243 rows = sorted(rows, key=lambda x: x[sortkey],
260 reverse=descending)
244 reverse=descending)
261
245
262 for row, parity in zip(rows, paritygen(stripecount)):
246 for row, parity in zip(rows, paritygen(stripecount)):
263 row['parity'] = parity
247 row['parity'] = parity
264 yield row
248 yield row
265
249
266 def indexentries(ui, repos, req, stripecount, sortcolumn='',
250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
267 descending=False, subdir=''):
251 descending=False, subdir=''):
268 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
269 return templateutil.mappinggenerator(_indexentriesgen, args=args)
253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
270
254
271 class hgwebdir(object):
255 class hgwebdir(object):
272 """HTTP server for multiple repositories.
256 """HTTP server for multiple repositories.
273
257
274 Given a configuration, different repositories will be served depending
258 Given a configuration, different repositories will be served depending
275 on the request path.
259 on the request path.
276
260
277 Instances are typically used as WSGI applications.
261 Instances are typically used as WSGI applications.
278 """
262 """
279 def __init__(self, conf, baseui=None):
263 def __init__(self, conf, baseui=None):
280 self.conf = conf
264 self.conf = conf
281 self.baseui = baseui
265 self.baseui = baseui
282 self.ui = None
266 self.ui = None
283 self.lastrefresh = 0
267 self.lastrefresh = 0
284 self.motd = None
268 self.motd = None
285 self.refresh()
269 self.refresh()
286
270
287 def refresh(self):
271 def refresh(self):
288 if self.ui:
272 if self.ui:
289 refreshinterval = self.ui.configint('web', 'refreshinterval')
273 refreshinterval = self.ui.configint('web', 'refreshinterval')
290 else:
274 else:
291 item = configitems.coreitems['web']['refreshinterval']
275 item = configitems.coreitems['web']['refreshinterval']
292 refreshinterval = item.default
276 refreshinterval = item.default
293
277
294 # refreshinterval <= 0 means to always refresh.
278 # refreshinterval <= 0 means to always refresh.
295 if (refreshinterval > 0 and
279 if (refreshinterval > 0 and
296 self.lastrefresh + refreshinterval > time.time()):
280 self.lastrefresh + refreshinterval > time.time()):
297 return
281 return
298
282
299 if self.baseui:
283 if self.baseui:
300 u = self.baseui.copy()
284 u = self.baseui.copy()
301 else:
285 else:
302 u = uimod.ui.load()
286 u = uimod.ui.load()
303 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
304 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
305 # displaying bundling progress bar while serving feels wrong and may
289 # displaying bundling progress bar while serving feels wrong and may
306 # break some wsgi implementations.
290 # break some wsgi implementations.
307 u.setconfig('progress', 'disable', 'true', 'hgweb')
291 u.setconfig('progress', 'disable', 'true', 'hgweb')
308
292
309 if not isinstance(self.conf, (dict, list, tuple)):
293 if not isinstance(self.conf, (dict, list, tuple)):
310 map = {'paths': 'hgweb-paths'}
294 map = {'paths': 'hgweb-paths'}
311 if not os.path.exists(self.conf):
295 if not os.path.exists(self.conf):
312 raise error.Abort(_('config file %s not found!') % self.conf)
296 raise error.Abort(_('config file %s not found!') % self.conf)
313 u.readconfig(self.conf, remap=map, trust=True)
297 u.readconfig(self.conf, remap=map, trust=True)
314 paths = []
298 paths = []
315 for name, ignored in u.configitems('hgweb-paths'):
299 for name, ignored in u.configitems('hgweb-paths'):
316 for path in u.configlist('hgweb-paths', name):
300 for path in u.configlist('hgweb-paths', name):
317 paths.append((name, path))
301 paths.append((name, path))
318 elif isinstance(self.conf, (list, tuple)):
302 elif isinstance(self.conf, (list, tuple)):
319 paths = self.conf
303 paths = self.conf
320 elif isinstance(self.conf, dict):
304 elif isinstance(self.conf, dict):
321 paths = self.conf.items()
305 paths = self.conf.items()
322
306
323 repos = findrepos(paths)
307 repos = findrepos(paths)
324 for prefix, root in u.configitems('collections'):
308 for prefix, root in u.configitems('collections'):
325 prefix = util.pconvert(prefix)
309 prefix = util.pconvert(prefix)
326 for path in scmutil.walkrepos(root, followsym=True):
310 for path in scmutil.walkrepos(root, followsym=True):
327 repo = os.path.normpath(path)
311 repo = os.path.normpath(path)
328 name = util.pconvert(repo)
312 name = util.pconvert(repo)
329 if name.startswith(prefix):
313 if name.startswith(prefix):
330 name = name[len(prefix):]
314 name = name[len(prefix):]
331 repos.append((name.lstrip('/'), repo))
315 repos.append((name.lstrip('/'), repo))
332
316
333 self.repos = repos
317 self.repos = repos
334 self.ui = u
318 self.ui = u
335 encoding.encoding = self.ui.config('web', 'encoding')
319 encoding.encoding = self.ui.config('web', 'encoding')
336 self.style = self.ui.config('web', 'style')
320 self.style = self.ui.config('web', 'style')
337 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
338 self.stripecount = self.ui.config('web', 'stripes')
322 self.stripecount = self.ui.config('web', 'stripes')
339 if self.stripecount:
323 if self.stripecount:
340 self.stripecount = int(self.stripecount)
324 self.stripecount = int(self.stripecount)
341 prefix = self.ui.config('web', 'prefix')
325 prefix = self.ui.config('web', 'prefix')
342 if prefix.startswith('/'):
326 if prefix.startswith('/'):
343 prefix = prefix[1:]
327 prefix = prefix[1:]
344 if prefix.endswith('/'):
328 if prefix.endswith('/'):
345 prefix = prefix[:-1]
329 prefix = prefix[:-1]
346 self.prefix = prefix
330 self.prefix = prefix
347 self.lastrefresh = time.time()
331 self.lastrefresh = time.time()
348
332
349 def run(self):
333 def run(self):
350 if not encoding.environ.get('GATEWAY_INTERFACE',
334 if not encoding.environ.get('GATEWAY_INTERFACE',
351 '').startswith("CGI/1."):
335 '').startswith("CGI/1."):
352 raise RuntimeError("This function is only intended to be "
336 raise RuntimeError("This function is only intended to be "
353 "called while running as a CGI script.")
337 "called while running as a CGI script.")
354 wsgicgi.launch(self)
338 wsgicgi.launch(self)
355
339
356 def __call__(self, env, respond):
340 def __call__(self, env, respond):
357 baseurl = self.ui.config('web', 'baseurl')
341 baseurl = self.ui.config('web', 'baseurl')
358 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
359 res = requestmod.wsgiresponse(req, respond)
343 res = requestmod.wsgiresponse(req, respond)
360
344
361 return self.run_wsgi(req, res)
345 return self.run_wsgi(req, res)
362
346
363 def run_wsgi(self, req, res):
347 def run_wsgi(self, req, res):
364 profile = self.ui.configbool('profiling', 'enabled')
348 profile = self.ui.configbool('profiling', 'enabled')
365 with profiling.profile(self.ui, enabled=profile):
349 with profiling.profile(self.ui, enabled=profile):
366 try:
350 try:
367 for r in self._runwsgi(req, res):
351 for r in self._runwsgi(req, res):
368 yield r
352 yield r
369 finally:
353 finally:
370 # There are known cycles in localrepository that prevent
354 # There are known cycles in localrepository that prevent
371 # those objects (and tons of held references) from being
355 # those objects (and tons of held references) from being
372 # collected through normal refcounting. We mitigate those
356 # collected through normal refcounting. We mitigate those
373 # leaks by performing an explicit GC on every request.
357 # leaks by performing an explicit GC on every request.
374 # TODO remove this once leaks are fixed.
358 # TODO remove this once leaks are fixed.
375 # TODO only run this on requests that create localrepository
359 # TODO only run this on requests that create localrepository
376 # instances instead of every request.
360 # instances instead of every request.
377 gc.collect()
361 gc.collect()
378
362
379 def _runwsgi(self, req, res):
363 def _runwsgi(self, req, res):
380 try:
364 try:
381 self.refresh()
365 self.refresh()
382
366
383 csp, nonce = cspvalues(self.ui)
367 csp, nonce = cspvalues(self.ui)
384 if csp:
368 if csp:
385 res.headers['Content-Security-Policy'] = csp
369 res.headers['Content-Security-Policy'] = csp
386
370
387 virtual = req.dispatchpath.strip('/')
371 virtual = req.dispatchpath.strip('/')
388 tmpl = self.templater(req, nonce)
372 tmpl = self.templater(req, nonce)
389 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
390
374
391 # Global defaults. These can be overridden by any handler.
375 # Global defaults. These can be overridden by any handler.
392 res.status = '200 Script output follows'
376 res.status = '200 Script output follows'
393 res.headers['Content-Type'] = ctype
377 res.headers['Content-Type'] = ctype
394
378
395 # a static file
379 # a static file
396 if virtual.startswith('static/') or 'static' in req.qsparams:
380 if virtual.startswith('static/') or 'static' in req.qsparams:
397 if virtual.startswith('static/'):
381 if virtual.startswith('static/'):
398 fname = virtual[7:]
382 fname = virtual[7:]
399 else:
383 else:
400 fname = req.qsparams['static']
384 fname = req.qsparams['static']
401 static = self.ui.config("web", "static", None,
385 static = self.ui.config("web", "static", None,
402 untrusted=False)
386 untrusted=False)
403 if not static:
387 if not static:
404 tp = self.templatepath or templater.templatepaths()
388 tp = self.templatepath or templater.templatepaths()
405 if isinstance(tp, str):
389 if isinstance(tp, str):
406 tp = [tp]
390 tp = [tp]
407 static = [os.path.join(p, 'static') for p in tp]
391 static = [os.path.join(p, 'static') for p in tp]
408
392
409 staticfile(static, fname, res)
393 staticfile(static, fname, res)
410 return res.sendresponse()
394 return res.sendresponse()
411
395
412 # top-level index
396 # top-level index
413
397
414 repos = dict(self.repos)
398 repos = dict(self.repos)
415
399
416 if (not virtual or virtual == 'index') and virtual not in repos:
400 if (not virtual or virtual == 'index') and virtual not in repos:
417 return self.makeindex(req, res, tmpl)
401 return self.makeindex(req, res, tmpl)
418
402
419 # nested indexes and hgwebs
403 # nested indexes and hgwebs
420
404
421 if virtual.endswith('/index') and virtual not in repos:
405 if virtual.endswith('/index') and virtual not in repos:
422 subdir = virtual[:-len('index')]
406 subdir = virtual[:-len('index')]
423 if any(r.startswith(subdir) for r in repos):
407 if any(r.startswith(subdir) for r in repos):
424 return self.makeindex(req, res, tmpl, subdir)
408 return self.makeindex(req, res, tmpl, subdir)
425
409
426 def _virtualdirs():
410 def _virtualdirs():
427 # Check the full virtual path, each parent, and the root ('')
411 # Check the full virtual path, each parent, and the root ('')
428 if virtual != '':
412 if virtual != '':
429 yield virtual
413 yield virtual
430
414
431 for p in util.finddirs(virtual):
415 for p in util.finddirs(virtual):
432 yield p
416 yield p
433
417
434 yield ''
418 yield ''
435
419
436 for virtualrepo in _virtualdirs():
420 for virtualrepo in _virtualdirs():
437 real = repos.get(virtualrepo)
421 real = repos.get(virtualrepo)
438 if real:
422 if real:
439 # Re-parse the WSGI environment to take into account our
423 # Re-parse the WSGI environment to take into account our
440 # repository path component.
424 # repository path component.
441 req = requestmod.parserequestfromenv(
425 req = requestmod.parserequestfromenv(
442 req.rawenv, reponame=virtualrepo,
426 req.rawenv, reponame=virtualrepo,
443 altbaseurl=self.ui.config('web', 'baseurl'))
427 altbaseurl=self.ui.config('web', 'baseurl'))
444 try:
428 try:
445 # ensure caller gets private copy of ui
429 # ensure caller gets private copy of ui
446 repo = hg.repository(self.ui.copy(), real)
430 repo = hg.repository(self.ui.copy(), real)
447 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
431 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
448 except IOError as inst:
432 except IOError as inst:
449 msg = encoding.strtolocal(inst.strerror)
433 msg = encoding.strtolocal(inst.strerror)
450 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
434 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
451 except error.RepoError as inst:
435 except error.RepoError as inst:
452 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
436 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
453
437
454 # browse subdirectories
438 # browse subdirectories
455 subdir = virtual + '/'
439 subdir = virtual + '/'
456 if [r for r in repos if r.startswith(subdir)]:
440 if [r for r in repos if r.startswith(subdir)]:
457 return self.makeindex(req, res, tmpl, subdir)
441 return self.makeindex(req, res, tmpl, subdir)
458
442
459 # prefixes not found
443 # prefixes not found
460 res.status = '404 Not Found'
444 res.status = '404 Not Found'
461 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
445 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
462 return res.sendresponse()
446 return res.sendresponse()
463
447
464 except ErrorResponse as e:
448 except ErrorResponse as e:
465 res.status = statusmessage(e.code, pycompat.bytestr(e))
449 res.status = statusmessage(e.code, pycompat.bytestr(e))
466 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
450 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
467 return res.sendresponse()
451 return res.sendresponse()
468 finally:
452 finally:
469 tmpl = None
453 tmpl = None
470
454
471 def makeindex(self, req, res, tmpl, subdir=""):
455 def makeindex(self, req, res, tmpl, subdir=""):
472 self.refresh()
456 self.refresh()
473 sortable = ["name", "description", "contact", "lastchange"]
457 sortable = ["name", "description", "contact", "lastchange"]
474 sortcolumn, descending = None, False
458 sortcolumn, descending = None, False
475 if 'sort' in req.qsparams:
459 if 'sort' in req.qsparams:
476 sortcolumn = req.qsparams['sort']
460 sortcolumn = req.qsparams['sort']
477 descending = sortcolumn.startswith('-')
461 descending = sortcolumn.startswith('-')
478 if descending:
462 if descending:
479 sortcolumn = sortcolumn[1:]
463 sortcolumn = sortcolumn[1:]
480 if sortcolumn not in sortable:
464 if sortcolumn not in sortable:
481 sortcolumn = ""
465 sortcolumn = ""
482
466
483 sort = [("sort_%s" % column,
467 sort = [("sort_%s" % column,
484 "%s%s" % ((not descending and column == sortcolumn)
468 "%s%s" % ((not descending and column == sortcolumn)
485 and "-" or "", column))
469 and "-" or "", column))
486 for column in sortable]
470 for column in sortable]
487
471
488 self.refresh()
472 self.refresh()
489
473
490 entries = indexentries(self.ui, self.repos, req,
474 entries = indexentries(self.ui, self.repos, req,
491 self.stripecount, sortcolumn=sortcolumn,
475 self.stripecount, sortcolumn=sortcolumn,
492 descending=descending, subdir=subdir)
476 descending=descending, subdir=subdir)
493
477
494 mapping = {
478 mapping = {
495 'entries': entries,
479 'entries': entries,
496 'subdir': subdir,
480 'subdir': subdir,
497 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
481 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
498 'sortcolumn': sortcolumn,
482 'sortcolumn': sortcolumn,
499 'descending': descending,
483 'descending': descending,
500 }
484 }
501 mapping.update(sort)
485 mapping.update(sort)
502 res.setbodygen(tmpl.generate('index', mapping))
486 res.setbodygen(tmpl.generate('index', mapping))
503 return res.sendresponse()
487 return res.sendresponse()
504
488
505 def templater(self, req, nonce):
489 def templater(self, req, nonce):
506
490
507 def motd(**map):
491 def motd(**map):
508 if self.motd is not None:
492 if self.motd is not None:
509 yield self.motd
493 yield self.motd
510 else:
494 else:
511 yield config('web', 'motd')
495 yield config('web', 'motd')
512
496
513 def config(section, name, default=uimod._unset, untrusted=True):
497 def config(section, name, default=uimod._unset, untrusted=True):
514 return self.ui.config(section, name, default, untrusted)
498 return self.ui.config(section, name, default, untrusted)
515
499
516 vars = {}
500 vars = {}
517 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
501 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
518 self.templatepath)
502 self.templatepath)
519 if style == styles[0]:
503 if style == styles[0]:
520 vars['style'] = style
504 vars['style'] = style
521
505
522 sessionvars = webutil.sessionvars(vars, r'?')
506 sessionvars = webutil.sessionvars(vars, r'?')
523 logourl = config('web', 'logourl')
507 logourl = config('web', 'logourl')
524 logoimg = config('web', 'logoimg')
508 logoimg = config('web', 'logoimg')
525 staticurl = (config('web', 'staticurl')
509 staticurl = (config('web', 'staticurl')
526 or req.apppath + '/static/')
510 or req.apppath + '/static/')
527 if not staticurl.endswith('/'):
511 if not staticurl.endswith('/'):
528 staticurl += '/'
512 staticurl += '/'
529
513
530 defaults = {
514 defaults = {
531 "encoding": encoding.encoding,
515 "encoding": encoding.encoding,
532 "motd": motd,
516 "motd": motd,
533 "url": req.apppath + '/',
517 "url": req.apppath + '/',
534 "logourl": logourl,
518 "logourl": logourl,
535 "logoimg": logoimg,
519 "logoimg": logoimg,
536 "staticurl": staticurl,
520 "staticurl": staticurl,
537 "sessionvars": sessionvars,
521 "sessionvars": sessionvars,
538 "style": style,
522 "style": style,
539 "nonce": nonce,
523 "nonce": nonce,
540 }
524 }
541 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
525 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
542 return tmpl
526 return tmpl
@@ -1,700 +1,716 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 obsutil,
31 obsutil,
32 patch,
32 patch,
33 pathutil,
33 pathutil,
34 pycompat,
34 pycompat,
35 scmutil,
35 scmutil,
36 templatefilters,
36 templatefilters,
37 templatekw,
37 templatekw,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 )
40 )
41
41
42 from ..utils import (
42 from ..utils import (
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46 archivespecs = util.sortdict((
46 archivespecs = util.sortdict((
47 ('zip', ('application/zip', 'zip', '.zip', None)),
47 ('zip', ('application/zip', 'zip', '.zip', None)),
48 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
48 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
49 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 ))
50 ))
51
51
52 def archivelist(ui, nodeid, url):
53 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
54 archives = []
55
56 for typ, spec in archivespecs.iteritems():
57 if typ in allowed or ui.configbool('web', 'allow' + typ,
58 untrusted=True):
59 archives.append({
60 'type': typ,
61 'extension': spec[2],
62 'node': nodeid,
63 'url': url,
64 })
65
66 return archives
67
52 def up(p):
68 def up(p):
53 if p[0:1] != "/":
69 if p[0:1] != "/":
54 p = "/" + p
70 p = "/" + p
55 if p[-1:] == "/":
71 if p[-1:] == "/":
56 p = p[:-1]
72 p = p[:-1]
57 up = os.path.dirname(p)
73 up = os.path.dirname(p)
58 if up == "/":
74 if up == "/":
59 return "/"
75 return "/"
60 return up + "/"
76 return up + "/"
61
77
62 def _navseq(step, firststep=None):
78 def _navseq(step, firststep=None):
63 if firststep:
79 if firststep:
64 yield firststep
80 yield firststep
65 if firststep >= 20 and firststep <= 40:
81 if firststep >= 20 and firststep <= 40:
66 firststep = 50
82 firststep = 50
67 yield firststep
83 yield firststep
68 assert step > 0
84 assert step > 0
69 assert firststep > 0
85 assert firststep > 0
70 while step <= firststep:
86 while step <= firststep:
71 step *= 10
87 step *= 10
72 while True:
88 while True:
73 yield 1 * step
89 yield 1 * step
74 yield 3 * step
90 yield 3 * step
75 step *= 10
91 step *= 10
76
92
77 class revnav(object):
93 class revnav(object):
78
94
79 def __init__(self, repo):
95 def __init__(self, repo):
80 """Navigation generation object
96 """Navigation generation object
81
97
82 :repo: repo object we generate nav for
98 :repo: repo object we generate nav for
83 """
99 """
84 # used for hex generation
100 # used for hex generation
85 self._revlog = repo.changelog
101 self._revlog = repo.changelog
86
102
87 def __nonzero__(self):
103 def __nonzero__(self):
88 """return True if any revision to navigate over"""
104 """return True if any revision to navigate over"""
89 return self._first() is not None
105 return self._first() is not None
90
106
91 __bool__ = __nonzero__
107 __bool__ = __nonzero__
92
108
93 def _first(self):
109 def _first(self):
94 """return the minimum non-filtered changeset or None"""
110 """return the minimum non-filtered changeset or None"""
95 try:
111 try:
96 return next(iter(self._revlog))
112 return next(iter(self._revlog))
97 except StopIteration:
113 except StopIteration:
98 return None
114 return None
99
115
100 def hex(self, rev):
116 def hex(self, rev):
101 return hex(self._revlog.node(rev))
117 return hex(self._revlog.node(rev))
102
118
103 def gen(self, pos, pagelen, limit):
119 def gen(self, pos, pagelen, limit):
104 """computes label and revision id for navigation link
120 """computes label and revision id for navigation link
105
121
106 :pos: is the revision relative to which we generate navigation.
122 :pos: is the revision relative to which we generate navigation.
107 :pagelen: the size of each navigation page
123 :pagelen: the size of each navigation page
108 :limit: how far shall we link
124 :limit: how far shall we link
109
125
110 The return is:
126 The return is:
111 - a single element tuple
127 - a single element tuple
112 - containing a dictionary with a `before` and `after` key
128 - containing a dictionary with a `before` and `after` key
113 - values are generator functions taking arbitrary number of kwargs
129 - values are generator functions taking arbitrary number of kwargs
114 - yield items are dictionaries with `label` and `node` keys
130 - yield items are dictionaries with `label` and `node` keys
115 """
131 """
116 if not self:
132 if not self:
117 # empty repo
133 # empty repo
118 return ({'before': (), 'after': ()},)
134 return ({'before': (), 'after': ()},)
119
135
120 targets = []
136 targets = []
121 for f in _navseq(1, pagelen):
137 for f in _navseq(1, pagelen):
122 if f > limit:
138 if f > limit:
123 break
139 break
124 targets.append(pos + f)
140 targets.append(pos + f)
125 targets.append(pos - f)
141 targets.append(pos - f)
126 targets.sort()
142 targets.sort()
127
143
128 first = self._first()
144 first = self._first()
129 navbefore = [("(%i)" % first, self.hex(first))]
145 navbefore = [("(%i)" % first, self.hex(first))]
130 navafter = []
146 navafter = []
131 for rev in targets:
147 for rev in targets:
132 if rev not in self._revlog:
148 if rev not in self._revlog:
133 continue
149 continue
134 if pos < rev < limit:
150 if pos < rev < limit:
135 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
151 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
136 if 0 < rev < pos:
152 if 0 < rev < pos:
137 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
153 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
138
154
139
155
140 navafter.append(("tip", "tip"))
156 navafter.append(("tip", "tip"))
141
157
142 data = lambda i: {"label": i[0], "node": i[1]}
158 data = lambda i: {"label": i[0], "node": i[1]}
143 return ({'before': lambda **map: (data(i) for i in navbefore),
159 return ({'before': lambda **map: (data(i) for i in navbefore),
144 'after': lambda **map: (data(i) for i in navafter)},)
160 'after': lambda **map: (data(i) for i in navafter)},)
145
161
146 class filerevnav(revnav):
162 class filerevnav(revnav):
147
163
148 def __init__(self, repo, path):
164 def __init__(self, repo, path):
149 """Navigation generation object
165 """Navigation generation object
150
166
151 :repo: repo object we generate nav for
167 :repo: repo object we generate nav for
152 :path: path of the file we generate nav for
168 :path: path of the file we generate nav for
153 """
169 """
154 # used for iteration
170 # used for iteration
155 self._changelog = repo.unfiltered().changelog
171 self._changelog = repo.unfiltered().changelog
156 # used for hex generation
172 # used for hex generation
157 self._revlog = repo.file(path)
173 self._revlog = repo.file(path)
158
174
159 def hex(self, rev):
175 def hex(self, rev):
160 return hex(self._changelog.node(self._revlog.linkrev(rev)))
176 return hex(self._changelog.node(self._revlog.linkrev(rev)))
161
177
162 class _siblings(object):
178 class _siblings(object):
163 def __init__(self, siblings=None, hiderev=None):
179 def __init__(self, siblings=None, hiderev=None):
164 if siblings is None:
180 if siblings is None:
165 siblings = []
181 siblings = []
166 self.siblings = [s for s in siblings if s.node() != nullid]
182 self.siblings = [s for s in siblings if s.node() != nullid]
167 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
183 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
168 self.siblings = []
184 self.siblings = []
169
185
170 def __iter__(self):
186 def __iter__(self):
171 for s in self.siblings:
187 for s in self.siblings:
172 d = {
188 d = {
173 'node': s.hex(),
189 'node': s.hex(),
174 'rev': s.rev(),
190 'rev': s.rev(),
175 'user': s.user(),
191 'user': s.user(),
176 'date': s.date(),
192 'date': s.date(),
177 'description': s.description(),
193 'description': s.description(),
178 'branch': s.branch(),
194 'branch': s.branch(),
179 }
195 }
180 if util.safehasattr(s, 'path'):
196 if util.safehasattr(s, 'path'):
181 d['file'] = s.path()
197 d['file'] = s.path()
182 yield d
198 yield d
183
199
184 def __len__(self):
200 def __len__(self):
185 return len(self.siblings)
201 return len(self.siblings)
186
202
187 def difffeatureopts(req, ui, section):
203 def difffeatureopts(req, ui, section):
188 diffopts = patch.difffeatureopts(ui, untrusted=True,
204 diffopts = patch.difffeatureopts(ui, untrusted=True,
189 section=section, whitespace=True)
205 section=section, whitespace=True)
190
206
191 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
207 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
192 v = req.qsparams.get(k)
208 v = req.qsparams.get(k)
193 if v is not None:
209 if v is not None:
194 v = stringutil.parsebool(v)
210 v = stringutil.parsebool(v)
195 setattr(diffopts, k, v if v is not None else True)
211 setattr(diffopts, k, v if v is not None else True)
196
212
197 return diffopts
213 return diffopts
198
214
199 def annotate(req, fctx, ui):
215 def annotate(req, fctx, ui):
200 diffopts = difffeatureopts(req, ui, 'annotate')
216 diffopts = difffeatureopts(req, ui, 'annotate')
201 return fctx.annotate(follow=True, diffopts=diffopts)
217 return fctx.annotate(follow=True, diffopts=diffopts)
202
218
203 def parents(ctx, hide=None):
219 def parents(ctx, hide=None):
204 if isinstance(ctx, context.basefilectx):
220 if isinstance(ctx, context.basefilectx):
205 introrev = ctx.introrev()
221 introrev = ctx.introrev()
206 if ctx.changectx().rev() != introrev:
222 if ctx.changectx().rev() != introrev:
207 return _siblings([ctx.repo()[introrev]], hide)
223 return _siblings([ctx.repo()[introrev]], hide)
208 return _siblings(ctx.parents(), hide)
224 return _siblings(ctx.parents(), hide)
209
225
210 def children(ctx, hide=None):
226 def children(ctx, hide=None):
211 return _siblings(ctx.children(), hide)
227 return _siblings(ctx.children(), hide)
212
228
213 def renamelink(fctx):
229 def renamelink(fctx):
214 r = fctx.renamed()
230 r = fctx.renamed()
215 if r:
231 if r:
216 return [{'file': r[0], 'node': hex(r[1])}]
232 return [{'file': r[0], 'node': hex(r[1])}]
217 return []
233 return []
218
234
219 def nodetagsdict(repo, node):
235 def nodetagsdict(repo, node):
220 return [{"name": i} for i in repo.nodetags(node)]
236 return [{"name": i} for i in repo.nodetags(node)]
221
237
222 def nodebookmarksdict(repo, node):
238 def nodebookmarksdict(repo, node):
223 return [{"name": i} for i in repo.nodebookmarks(node)]
239 return [{"name": i} for i in repo.nodebookmarks(node)]
224
240
225 def nodebranchdict(repo, ctx):
241 def nodebranchdict(repo, ctx):
226 branches = []
242 branches = []
227 branch = ctx.branch()
243 branch = ctx.branch()
228 # If this is an empty repo, ctx.node() == nullid,
244 # If this is an empty repo, ctx.node() == nullid,
229 # ctx.branch() == 'default'.
245 # ctx.branch() == 'default'.
230 try:
246 try:
231 branchnode = repo.branchtip(branch)
247 branchnode = repo.branchtip(branch)
232 except error.RepoLookupError:
248 except error.RepoLookupError:
233 branchnode = None
249 branchnode = None
234 if branchnode == ctx.node():
250 if branchnode == ctx.node():
235 branches.append({"name": branch})
251 branches.append({"name": branch})
236 return branches
252 return branches
237
253
238 def nodeinbranch(repo, ctx):
254 def nodeinbranch(repo, ctx):
239 branches = []
255 branches = []
240 branch = ctx.branch()
256 branch = ctx.branch()
241 try:
257 try:
242 branchnode = repo.branchtip(branch)
258 branchnode = repo.branchtip(branch)
243 except error.RepoLookupError:
259 except error.RepoLookupError:
244 branchnode = None
260 branchnode = None
245 if branch != 'default' and branchnode != ctx.node():
261 if branch != 'default' and branchnode != ctx.node():
246 branches.append({"name": branch})
262 branches.append({"name": branch})
247 return branches
263 return branches
248
264
249 def nodebranchnodefault(ctx):
265 def nodebranchnodefault(ctx):
250 branches = []
266 branches = []
251 branch = ctx.branch()
267 branch = ctx.branch()
252 if branch != 'default':
268 if branch != 'default':
253 branches.append({"name": branch})
269 branches.append({"name": branch})
254 return branches
270 return branches
255
271
256 def showtag(repo, tmpl, t1, node=nullid, **args):
272 def showtag(repo, tmpl, t1, node=nullid, **args):
257 args = pycompat.byteskwargs(args)
273 args = pycompat.byteskwargs(args)
258 for t in repo.nodetags(node):
274 for t in repo.nodetags(node):
259 lm = args.copy()
275 lm = args.copy()
260 lm['tag'] = t
276 lm['tag'] = t
261 yield tmpl.generate(t1, lm)
277 yield tmpl.generate(t1, lm)
262
278
263 def showbookmark(repo, tmpl, t1, node=nullid, **args):
279 def showbookmark(repo, tmpl, t1, node=nullid, **args):
264 args = pycompat.byteskwargs(args)
280 args = pycompat.byteskwargs(args)
265 for t in repo.nodebookmarks(node):
281 for t in repo.nodebookmarks(node):
266 lm = args.copy()
282 lm = args.copy()
267 lm['bookmark'] = t
283 lm['bookmark'] = t
268 yield tmpl.generate(t1, lm)
284 yield tmpl.generate(t1, lm)
269
285
270 def branchentries(repo, stripecount, limit=0):
286 def branchentries(repo, stripecount, limit=0):
271 tips = []
287 tips = []
272 heads = repo.heads()
288 heads = repo.heads()
273 parity = paritygen(stripecount)
289 parity = paritygen(stripecount)
274 sortkey = lambda item: (not item[1], item[0].rev())
290 sortkey = lambda item: (not item[1], item[0].rev())
275
291
276 def entries(**map):
292 def entries(**map):
277 count = 0
293 count = 0
278 if not tips:
294 if not tips:
279 for tag, hs, tip, closed in repo.branchmap().iterbranches():
295 for tag, hs, tip, closed in repo.branchmap().iterbranches():
280 tips.append((repo[tip], closed))
296 tips.append((repo[tip], closed))
281 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
297 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
282 if limit > 0 and count >= limit:
298 if limit > 0 and count >= limit:
283 return
299 return
284 count += 1
300 count += 1
285 if closed:
301 if closed:
286 status = 'closed'
302 status = 'closed'
287 elif ctx.node() not in heads:
303 elif ctx.node() not in heads:
288 status = 'inactive'
304 status = 'inactive'
289 else:
305 else:
290 status = 'open'
306 status = 'open'
291 yield {
307 yield {
292 'parity': next(parity),
308 'parity': next(parity),
293 'branch': ctx.branch(),
309 'branch': ctx.branch(),
294 'status': status,
310 'status': status,
295 'node': ctx.hex(),
311 'node': ctx.hex(),
296 'date': ctx.date()
312 'date': ctx.date()
297 }
313 }
298
314
299 return entries
315 return entries
300
316
301 def cleanpath(repo, path):
317 def cleanpath(repo, path):
302 path = path.lstrip('/')
318 path = path.lstrip('/')
303 return pathutil.canonpath(repo.root, '', path)
319 return pathutil.canonpath(repo.root, '', path)
304
320
305 def changectx(repo, req):
321 def changectx(repo, req):
306 changeid = "tip"
322 changeid = "tip"
307 if 'node' in req.qsparams:
323 if 'node' in req.qsparams:
308 changeid = req.qsparams['node']
324 changeid = req.qsparams['node']
309 ipos = changeid.find(':')
325 ipos = changeid.find(':')
310 if ipos != -1:
326 if ipos != -1:
311 changeid = changeid[(ipos + 1):]
327 changeid = changeid[(ipos + 1):]
312
328
313 return scmutil.revsymbol(repo, changeid)
329 return scmutil.revsymbol(repo, changeid)
314
330
315 def basechangectx(repo, req):
331 def basechangectx(repo, req):
316 if 'node' in req.qsparams:
332 if 'node' in req.qsparams:
317 changeid = req.qsparams['node']
333 changeid = req.qsparams['node']
318 ipos = changeid.find(':')
334 ipos = changeid.find(':')
319 if ipos != -1:
335 if ipos != -1:
320 changeid = changeid[:ipos]
336 changeid = changeid[:ipos]
321 return scmutil.revsymbol(repo, changeid)
337 return scmutil.revsymbol(repo, changeid)
322
338
323 return None
339 return None
324
340
325 def filectx(repo, req):
341 def filectx(repo, req):
326 if 'file' not in req.qsparams:
342 if 'file' not in req.qsparams:
327 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
343 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
328 path = cleanpath(repo, req.qsparams['file'])
344 path = cleanpath(repo, req.qsparams['file'])
329 if 'node' in req.qsparams:
345 if 'node' in req.qsparams:
330 changeid = req.qsparams['node']
346 changeid = req.qsparams['node']
331 elif 'filenode' in req.qsparams:
347 elif 'filenode' in req.qsparams:
332 changeid = req.qsparams['filenode']
348 changeid = req.qsparams['filenode']
333 else:
349 else:
334 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
350 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
335 try:
351 try:
336 fctx = scmutil.revsymbol(repo, changeid)[path]
352 fctx = scmutil.revsymbol(repo, changeid)[path]
337 except error.RepoError:
353 except error.RepoError:
338 fctx = repo.filectx(path, fileid=changeid)
354 fctx = repo.filectx(path, fileid=changeid)
339
355
340 return fctx
356 return fctx
341
357
342 def linerange(req):
358 def linerange(req):
343 linerange = req.qsparams.getall('linerange')
359 linerange = req.qsparams.getall('linerange')
344 if not linerange:
360 if not linerange:
345 return None
361 return None
346 if len(linerange) > 1:
362 if len(linerange) > 1:
347 raise ErrorResponse(HTTP_BAD_REQUEST,
363 raise ErrorResponse(HTTP_BAD_REQUEST,
348 'redundant linerange parameter')
364 'redundant linerange parameter')
349 try:
365 try:
350 fromline, toline = map(int, linerange[0].split(':', 1))
366 fromline, toline = map(int, linerange[0].split(':', 1))
351 except ValueError:
367 except ValueError:
352 raise ErrorResponse(HTTP_BAD_REQUEST,
368 raise ErrorResponse(HTTP_BAD_REQUEST,
353 'invalid linerange parameter')
369 'invalid linerange parameter')
354 try:
370 try:
355 return util.processlinerange(fromline, toline)
371 return util.processlinerange(fromline, toline)
356 except error.ParseError as exc:
372 except error.ParseError as exc:
357 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
373 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
358
374
359 def formatlinerange(fromline, toline):
375 def formatlinerange(fromline, toline):
360 return '%d:%d' % (fromline + 1, toline)
376 return '%d:%d' % (fromline + 1, toline)
361
377
362 def succsandmarkers(context, mapping):
378 def succsandmarkers(context, mapping):
363 repo = context.resource(mapping, 'repo')
379 repo = context.resource(mapping, 'repo')
364 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
380 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
365 for item in itemmappings.tovalue(context, mapping):
381 for item in itemmappings.tovalue(context, mapping):
366 item['successors'] = _siblings(repo[successor]
382 item['successors'] = _siblings(repo[successor]
367 for successor in item['successors'])
383 for successor in item['successors'])
368 yield item
384 yield item
369
385
370 # teach templater succsandmarkers is switched to (context, mapping) API
386 # teach templater succsandmarkers is switched to (context, mapping) API
371 succsandmarkers._requires = {'repo', 'ctx'}
387 succsandmarkers._requires = {'repo', 'ctx'}
372
388
373 def whyunstable(context, mapping):
389 def whyunstable(context, mapping):
374 repo = context.resource(mapping, 'repo')
390 repo = context.resource(mapping, 'repo')
375 ctx = context.resource(mapping, 'ctx')
391 ctx = context.resource(mapping, 'ctx')
376
392
377 entries = obsutil.whyunstable(repo, ctx)
393 entries = obsutil.whyunstable(repo, ctx)
378 for entry in entries:
394 for entry in entries:
379 if entry.get('divergentnodes'):
395 if entry.get('divergentnodes'):
380 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
396 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
381 yield entry
397 yield entry
382
398
383 whyunstable._requires = {'repo', 'ctx'}
399 whyunstable._requires = {'repo', 'ctx'}
384
400
385 def commonentry(repo, ctx):
401 def commonentry(repo, ctx):
386 node = ctx.node()
402 node = ctx.node()
387 return {
403 return {
388 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
404 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
389 # filectx, but I'm not pretty sure if that would always work because
405 # filectx, but I'm not pretty sure if that would always work because
390 # fctx.parents() != fctx.changectx.parents() for example.
406 # fctx.parents() != fctx.changectx.parents() for example.
391 'ctx': ctx,
407 'ctx': ctx,
392 'rev': ctx.rev(),
408 'rev': ctx.rev(),
393 'node': hex(node),
409 'node': hex(node),
394 'author': ctx.user(),
410 'author': ctx.user(),
395 'desc': ctx.description(),
411 'desc': ctx.description(),
396 'date': ctx.date(),
412 'date': ctx.date(),
397 'extra': ctx.extra(),
413 'extra': ctx.extra(),
398 'phase': ctx.phasestr(),
414 'phase': ctx.phasestr(),
399 'obsolete': ctx.obsolete(),
415 'obsolete': ctx.obsolete(),
400 'succsandmarkers': succsandmarkers,
416 'succsandmarkers': succsandmarkers,
401 'instabilities': [{"instability": i} for i in ctx.instabilities()],
417 'instabilities': [{"instability": i} for i in ctx.instabilities()],
402 'whyunstable': whyunstable,
418 'whyunstable': whyunstable,
403 'branch': nodebranchnodefault(ctx),
419 'branch': nodebranchnodefault(ctx),
404 'inbranch': nodeinbranch(repo, ctx),
420 'inbranch': nodeinbranch(repo, ctx),
405 'branches': nodebranchdict(repo, ctx),
421 'branches': nodebranchdict(repo, ctx),
406 'tags': nodetagsdict(repo, node),
422 'tags': nodetagsdict(repo, node),
407 'bookmarks': nodebookmarksdict(repo, node),
423 'bookmarks': nodebookmarksdict(repo, node),
408 'parent': lambda **x: parents(ctx),
424 'parent': lambda **x: parents(ctx),
409 'child': lambda **x: children(ctx),
425 'child': lambda **x: children(ctx),
410 }
426 }
411
427
412 def changelistentry(web, ctx):
428 def changelistentry(web, ctx):
413 '''Obtain a dictionary to be used for entries in a changelist.
429 '''Obtain a dictionary to be used for entries in a changelist.
414
430
415 This function is called when producing items for the "entries" list passed
431 This function is called when producing items for the "entries" list passed
416 to the "shortlog" and "changelog" templates.
432 to the "shortlog" and "changelog" templates.
417 '''
433 '''
418 repo = web.repo
434 repo = web.repo
419 rev = ctx.rev()
435 rev = ctx.rev()
420 n = ctx.node()
436 n = ctx.node()
421 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
437 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
422 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
438 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
423
439
424 entry = commonentry(repo, ctx)
440 entry = commonentry(repo, ctx)
425 entry.update(
441 entry.update(
426 allparents=lambda **x: parents(ctx),
442 allparents=lambda **x: parents(ctx),
427 parent=lambda **x: parents(ctx, rev - 1),
443 parent=lambda **x: parents(ctx, rev - 1),
428 child=lambda **x: children(ctx, rev + 1),
444 child=lambda **x: children(ctx, rev + 1),
429 changelogtag=showtags,
445 changelogtag=showtags,
430 files=files,
446 files=files,
431 )
447 )
432 return entry
448 return entry
433
449
434 def symrevorshortnode(req, ctx):
450 def symrevorshortnode(req, ctx):
435 if 'node' in req.qsparams:
451 if 'node' in req.qsparams:
436 return templatefilters.revescape(req.qsparams['node'])
452 return templatefilters.revescape(req.qsparams['node'])
437 else:
453 else:
438 return short(ctx.node())
454 return short(ctx.node())
439
455
440 def changesetentry(web, ctx):
456 def changesetentry(web, ctx):
441 '''Obtain a dictionary to be used to render the "changeset" template.'''
457 '''Obtain a dictionary to be used to render the "changeset" template.'''
442
458
443 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
459 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
444 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
460 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
445 ctx.node())
461 ctx.node())
446 showbranch = nodebranchnodefault(ctx)
462 showbranch = nodebranchnodefault(ctx)
447
463
448 files = []
464 files = []
449 parity = paritygen(web.stripecount)
465 parity = paritygen(web.stripecount)
450 for blockno, f in enumerate(ctx.files()):
466 for blockno, f in enumerate(ctx.files()):
451 template = 'filenodelink' if f in ctx else 'filenolink'
467 template = 'filenodelink' if f in ctx else 'filenolink'
452 files.append(web.tmpl.generate(template, {
468 files.append(web.tmpl.generate(template, {
453 'node': ctx.hex(),
469 'node': ctx.hex(),
454 'file': f,
470 'file': f,
455 'blockno': blockno + 1,
471 'blockno': blockno + 1,
456 'parity': next(parity),
472 'parity': next(parity),
457 }))
473 }))
458
474
459 basectx = basechangectx(web.repo, web.req)
475 basectx = basechangectx(web.repo, web.req)
460 if basectx is None:
476 if basectx is None:
461 basectx = ctx.p1()
477 basectx = ctx.p1()
462
478
463 style = web.config('web', 'style')
479 style = web.config('web', 'style')
464 if 'style' in web.req.qsparams:
480 if 'style' in web.req.qsparams:
465 style = web.req.qsparams['style']
481 style = web.req.qsparams['style']
466
482
467 diff = diffs(web, ctx, basectx, None, style)
483 diff = diffs(web, ctx, basectx, None, style)
468
484
469 parity = paritygen(web.stripecount)
485 parity = paritygen(web.stripecount)
470 diffstatsgen = diffstatgen(ctx, basectx)
486 diffstatsgen = diffstatgen(ctx, basectx)
471 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
487 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
472
488
473 return dict(
489 return dict(
474 diff=diff,
490 diff=diff,
475 symrev=symrevorshortnode(web.req, ctx),
491 symrev=symrevorshortnode(web.req, ctx),
476 basenode=basectx.hex(),
492 basenode=basectx.hex(),
477 changesettag=showtags,
493 changesettag=showtags,
478 changesetbookmark=showbookmarks,
494 changesetbookmark=showbookmarks,
479 changesetbranch=showbranch,
495 changesetbranch=showbranch,
480 files=files,
496 files=files,
481 diffsummary=lambda **x: diffsummary(diffstatsgen),
497 diffsummary=lambda **x: diffsummary(diffstatsgen),
482 diffstat=diffstats,
498 diffstat=diffstats,
483 archives=web.archivelist(ctx.hex()),
499 archives=web.archivelist(ctx.hex()),
484 **pycompat.strkwargs(commonentry(web.repo, ctx)))
500 **pycompat.strkwargs(commonentry(web.repo, ctx)))
485
501
486 def listfilediffs(tmpl, files, node, max):
502 def listfilediffs(tmpl, files, node, max):
487 for f in files[:max]:
503 for f in files[:max]:
488 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
504 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
489 if len(files) > max:
505 if len(files) > max:
490 yield tmpl.generate('fileellipses', {})
506 yield tmpl.generate('fileellipses', {})
491
507
492 def diffs(web, ctx, basectx, files, style, linerange=None,
508 def diffs(web, ctx, basectx, files, style, linerange=None,
493 lineidprefix=''):
509 lineidprefix=''):
494
510
495 def prettyprintlines(lines, blockno):
511 def prettyprintlines(lines, blockno):
496 for lineno, l in enumerate(lines, 1):
512 for lineno, l in enumerate(lines, 1):
497 difflineno = "%d.%d" % (blockno, lineno)
513 difflineno = "%d.%d" % (blockno, lineno)
498 if l.startswith('+'):
514 if l.startswith('+'):
499 ltype = "difflineplus"
515 ltype = "difflineplus"
500 elif l.startswith('-'):
516 elif l.startswith('-'):
501 ltype = "difflineminus"
517 ltype = "difflineminus"
502 elif l.startswith('@'):
518 elif l.startswith('@'):
503 ltype = "difflineat"
519 ltype = "difflineat"
504 else:
520 else:
505 ltype = "diffline"
521 ltype = "diffline"
506 yield web.tmpl.generate(ltype, {
522 yield web.tmpl.generate(ltype, {
507 'line': l,
523 'line': l,
508 'lineno': lineno,
524 'lineno': lineno,
509 'lineid': lineidprefix + "l%s" % difflineno,
525 'lineid': lineidprefix + "l%s" % difflineno,
510 'linenumber': "% 8s" % difflineno,
526 'linenumber': "% 8s" % difflineno,
511 })
527 })
512
528
513 repo = web.repo
529 repo = web.repo
514 if files:
530 if files:
515 m = match.exact(repo.root, repo.getcwd(), files)
531 m = match.exact(repo.root, repo.getcwd(), files)
516 else:
532 else:
517 m = match.always(repo.root, repo.getcwd())
533 m = match.always(repo.root, repo.getcwd())
518
534
519 diffopts = patch.diffopts(repo.ui, untrusted=True)
535 diffopts = patch.diffopts(repo.ui, untrusted=True)
520 node1 = basectx.node()
536 node1 = basectx.node()
521 node2 = ctx.node()
537 node2 = ctx.node()
522 parity = paritygen(web.stripecount)
538 parity = paritygen(web.stripecount)
523
539
524 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
540 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
525 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
541 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
526 if style != 'raw':
542 if style != 'raw':
527 header = header[1:]
543 header = header[1:]
528 lines = [h + '\n' for h in header]
544 lines = [h + '\n' for h in header]
529 for hunkrange, hunklines in hunks:
545 for hunkrange, hunklines in hunks:
530 if linerange is not None and hunkrange is not None:
546 if linerange is not None and hunkrange is not None:
531 s1, l1, s2, l2 = hunkrange
547 s1, l1, s2, l2 = hunkrange
532 if not mdiff.hunkinrange((s2, l2), linerange):
548 if not mdiff.hunkinrange((s2, l2), linerange):
533 continue
549 continue
534 lines.extend(hunklines)
550 lines.extend(hunklines)
535 if lines:
551 if lines:
536 yield web.tmpl.generate('diffblock', {
552 yield web.tmpl.generate('diffblock', {
537 'parity': next(parity),
553 'parity': next(parity),
538 'blockno': blockno,
554 'blockno': blockno,
539 'lines': prettyprintlines(lines, blockno),
555 'lines': prettyprintlines(lines, blockno),
540 })
556 })
541
557
542 def compare(tmpl, context, leftlines, rightlines):
558 def compare(tmpl, context, leftlines, rightlines):
543 '''Generator function that provides side-by-side comparison data.'''
559 '''Generator function that provides side-by-side comparison data.'''
544
560
545 def compline(type, leftlineno, leftline, rightlineno, rightline):
561 def compline(type, leftlineno, leftline, rightlineno, rightline):
546 lineid = leftlineno and ("l%d" % leftlineno) or ''
562 lineid = leftlineno and ("l%d" % leftlineno) or ''
547 lineid += rightlineno and ("r%d" % rightlineno) or ''
563 lineid += rightlineno and ("r%d" % rightlineno) or ''
548 llno = '%d' % leftlineno if leftlineno else ''
564 llno = '%d' % leftlineno if leftlineno else ''
549 rlno = '%d' % rightlineno if rightlineno else ''
565 rlno = '%d' % rightlineno if rightlineno else ''
550 return tmpl.generate('comparisonline', {
566 return tmpl.generate('comparisonline', {
551 'type': type,
567 'type': type,
552 'lineid': lineid,
568 'lineid': lineid,
553 'leftlineno': leftlineno,
569 'leftlineno': leftlineno,
554 'leftlinenumber': "% 6s" % llno,
570 'leftlinenumber': "% 6s" % llno,
555 'leftline': leftline or '',
571 'leftline': leftline or '',
556 'rightlineno': rightlineno,
572 'rightlineno': rightlineno,
557 'rightlinenumber': "% 6s" % rlno,
573 'rightlinenumber': "% 6s" % rlno,
558 'rightline': rightline or '',
574 'rightline': rightline or '',
559 })
575 })
560
576
561 def getblock(opcodes):
577 def getblock(opcodes):
562 for type, llo, lhi, rlo, rhi in opcodes:
578 for type, llo, lhi, rlo, rhi in opcodes:
563 len1 = lhi - llo
579 len1 = lhi - llo
564 len2 = rhi - rlo
580 len2 = rhi - rlo
565 count = min(len1, len2)
581 count = min(len1, len2)
566 for i in xrange(count):
582 for i in xrange(count):
567 yield compline(type=type,
583 yield compline(type=type,
568 leftlineno=llo + i + 1,
584 leftlineno=llo + i + 1,
569 leftline=leftlines[llo + i],
585 leftline=leftlines[llo + i],
570 rightlineno=rlo + i + 1,
586 rightlineno=rlo + i + 1,
571 rightline=rightlines[rlo + i])
587 rightline=rightlines[rlo + i])
572 if len1 > len2:
588 if len1 > len2:
573 for i in xrange(llo + count, lhi):
589 for i in xrange(llo + count, lhi):
574 yield compline(type=type,
590 yield compline(type=type,
575 leftlineno=i + 1,
591 leftlineno=i + 1,
576 leftline=leftlines[i],
592 leftline=leftlines[i],
577 rightlineno=None,
593 rightlineno=None,
578 rightline=None)
594 rightline=None)
579 elif len2 > len1:
595 elif len2 > len1:
580 for i in xrange(rlo + count, rhi):
596 for i in xrange(rlo + count, rhi):
581 yield compline(type=type,
597 yield compline(type=type,
582 leftlineno=None,
598 leftlineno=None,
583 leftline=None,
599 leftline=None,
584 rightlineno=i + 1,
600 rightlineno=i + 1,
585 rightline=rightlines[i])
601 rightline=rightlines[i])
586
602
587 s = difflib.SequenceMatcher(None, leftlines, rightlines)
603 s = difflib.SequenceMatcher(None, leftlines, rightlines)
588 if context < 0:
604 if context < 0:
589 yield tmpl.generate('comparisonblock',
605 yield tmpl.generate('comparisonblock',
590 {'lines': getblock(s.get_opcodes())})
606 {'lines': getblock(s.get_opcodes())})
591 else:
607 else:
592 for oc in s.get_grouped_opcodes(n=context):
608 for oc in s.get_grouped_opcodes(n=context):
593 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
609 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
594
610
595 def diffstatgen(ctx, basectx):
611 def diffstatgen(ctx, basectx):
596 '''Generator function that provides the diffstat data.'''
612 '''Generator function that provides the diffstat data.'''
597
613
598 stats = patch.diffstatdata(
614 stats = patch.diffstatdata(
599 util.iterlines(ctx.diff(basectx, noprefix=False)))
615 util.iterlines(ctx.diff(basectx, noprefix=False)))
600 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
616 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
601 while True:
617 while True:
602 yield stats, maxname, maxtotal, addtotal, removetotal, binary
618 yield stats, maxname, maxtotal, addtotal, removetotal, binary
603
619
604 def diffsummary(statgen):
620 def diffsummary(statgen):
605 '''Return a short summary of the diff.'''
621 '''Return a short summary of the diff.'''
606
622
607 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
623 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
608 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
624 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
609 len(stats), addtotal, removetotal)
625 len(stats), addtotal, removetotal)
610
626
611 def diffstat(tmpl, ctx, statgen, parity):
627 def diffstat(tmpl, ctx, statgen, parity):
612 '''Return a diffstat template for each file in the diff.'''
628 '''Return a diffstat template for each file in the diff.'''
613
629
614 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
630 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
615 files = ctx.files()
631 files = ctx.files()
616
632
617 def pct(i):
633 def pct(i):
618 if maxtotal == 0:
634 if maxtotal == 0:
619 return 0
635 return 0
620 return (float(i) / maxtotal) * 100
636 return (float(i) / maxtotal) * 100
621
637
622 fileno = 0
638 fileno = 0
623 for filename, adds, removes, isbinary in stats:
639 for filename, adds, removes, isbinary in stats:
624 template = 'diffstatlink' if filename in files else 'diffstatnolink'
640 template = 'diffstatlink' if filename in files else 'diffstatnolink'
625 total = adds + removes
641 total = adds + removes
626 fileno += 1
642 fileno += 1
627 yield tmpl.generate(template, {
643 yield tmpl.generate(template, {
628 'node': ctx.hex(),
644 'node': ctx.hex(),
629 'file': filename,
645 'file': filename,
630 'fileno': fileno,
646 'fileno': fileno,
631 'total': total,
647 'total': total,
632 'addpct': pct(adds),
648 'addpct': pct(adds),
633 'removepct': pct(removes),
649 'removepct': pct(removes),
634 'parity': next(parity),
650 'parity': next(parity),
635 })
651 })
636
652
637 class sessionvars(object):
653 class sessionvars(object):
638 def __init__(self, vars, start='?'):
654 def __init__(self, vars, start='?'):
639 self.start = start
655 self.start = start
640 self.vars = vars
656 self.vars = vars
641 def __getitem__(self, key):
657 def __getitem__(self, key):
642 return self.vars[key]
658 return self.vars[key]
643 def __setitem__(self, key, value):
659 def __setitem__(self, key, value):
644 self.vars[key] = value
660 self.vars[key] = value
645 def __copy__(self):
661 def __copy__(self):
646 return sessionvars(copy.copy(self.vars), self.start)
662 return sessionvars(copy.copy(self.vars), self.start)
647 def __iter__(self):
663 def __iter__(self):
648 separator = self.start
664 separator = self.start
649 for key, value in sorted(self.vars.iteritems()):
665 for key, value in sorted(self.vars.iteritems()):
650 yield {'name': key,
666 yield {'name': key,
651 'value': pycompat.bytestr(value),
667 'value': pycompat.bytestr(value),
652 'separator': separator,
668 'separator': separator,
653 }
669 }
654 separator = '&'
670 separator = '&'
655
671
656 class wsgiui(uimod.ui):
672 class wsgiui(uimod.ui):
657 # default termwidth breaks under mod_wsgi
673 # default termwidth breaks under mod_wsgi
658 def termwidth(self):
674 def termwidth(self):
659 return 80
675 return 80
660
676
661 def getwebsubs(repo):
677 def getwebsubs(repo):
662 websubtable = []
678 websubtable = []
663 websubdefs = repo.ui.configitems('websub')
679 websubdefs = repo.ui.configitems('websub')
664 # we must maintain interhg backwards compatibility
680 # we must maintain interhg backwards compatibility
665 websubdefs += repo.ui.configitems('interhg')
681 websubdefs += repo.ui.configitems('interhg')
666 for key, pattern in websubdefs:
682 for key, pattern in websubdefs:
667 # grab the delimiter from the character after the "s"
683 # grab the delimiter from the character after the "s"
668 unesc = pattern[1:2]
684 unesc = pattern[1:2]
669 delim = re.escape(unesc)
685 delim = re.escape(unesc)
670
686
671 # identify portions of the pattern, taking care to avoid escaped
687 # identify portions of the pattern, taking care to avoid escaped
672 # delimiters. the replace format and flags are optional, but
688 # delimiters. the replace format and flags are optional, but
673 # delimiters are required.
689 # delimiters are required.
674 match = re.match(
690 match = re.match(
675 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
691 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
676 % (delim, delim, delim), pattern)
692 % (delim, delim, delim), pattern)
677 if not match:
693 if not match:
678 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
694 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
679 % (key, pattern))
695 % (key, pattern))
680 continue
696 continue
681
697
682 # we need to unescape the delimiter for regexp and format
698 # we need to unescape the delimiter for regexp and format
683 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
699 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
684 regexp = delim_re.sub(unesc, match.group(1))
700 regexp = delim_re.sub(unesc, match.group(1))
685 format = delim_re.sub(unesc, match.group(2))
701 format = delim_re.sub(unesc, match.group(2))
686
702
687 # the pattern allows for 6 regexp flags, so set them if necessary
703 # the pattern allows for 6 regexp flags, so set them if necessary
688 flagin = match.group(3)
704 flagin = match.group(3)
689 flags = 0
705 flags = 0
690 if flagin:
706 if flagin:
691 for flag in flagin.upper():
707 for flag in flagin.upper():
692 flags |= re.__dict__[flag]
708 flags |= re.__dict__[flag]
693
709
694 try:
710 try:
695 regexp = re.compile(regexp, flags)
711 regexp = re.compile(regexp, flags)
696 websubtable.append((regexp, format))
712 websubtable.append((regexp, format))
697 except re.error:
713 except re.error:
698 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
714 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
699 % (key, regexp))
715 % (key, regexp))
700 return websubtable
716 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now