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