##// END OF EJS Templates
hgweb: make fctx.annotate a separated function so it could be wrapped...
Jun Wu -
r30081:dd0ff715 default
parent child Browse files
Show More
@@ -1,1317 +1,1314 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import copy
11 import copy
12 import mimetypes
12 import mimetypes
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, short
17 from ..node import hex, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_FORBIDDEN,
21 HTTP_FORBIDDEN,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 HTTP_OK,
23 HTTP_OK,
24 get_contact,
24 get_contact,
25 paritygen,
25 paritygen,
26 staticfile,
26 staticfile,
27 )
27 )
28
28
29 from .. import (
29 from .. import (
30 archival,
30 archival,
31 encoding,
31 encoding,
32 error,
32 error,
33 graphmod,
33 graphmod,
34 patch,
35 revset,
34 revset,
36 scmutil,
35 scmutil,
37 templatefilters,
36 templatefilters,
38 templater,
37 templater,
39 util,
38 util,
40 )
39 )
41
40
42 from . import (
41 from . import (
43 webutil,
42 webutil,
44 )
43 )
45
44
46 __all__ = []
45 __all__ = []
47 commands = {}
46 commands = {}
48
47
49 class webcommand(object):
48 class webcommand(object):
50 """Decorator used to register a web command handler.
49 """Decorator used to register a web command handler.
51
50
52 The decorator takes as its positional arguments the name/path the
51 The decorator takes as its positional arguments the name/path the
53 command should be accessible under.
52 command should be accessible under.
54
53
55 Usage:
54 Usage:
56
55
57 @webcommand('mycommand')
56 @webcommand('mycommand')
58 def mycommand(web, req, tmpl):
57 def mycommand(web, req, tmpl):
59 pass
58 pass
60 """
59 """
61
60
62 def __init__(self, name):
61 def __init__(self, name):
63 self.name = name
62 self.name = name
64
63
65 def __call__(self, func):
64 def __call__(self, func):
66 __all__.append(self.name)
65 __all__.append(self.name)
67 commands[self.name] = func
66 commands[self.name] = func
68 return func
67 return func
69
68
70 @webcommand('log')
69 @webcommand('log')
71 def log(web, req, tmpl):
70 def log(web, req, tmpl):
72 """
71 """
73 /log[/{revision}[/{path}]]
72 /log[/{revision}[/{path}]]
74 --------------------------
73 --------------------------
75
74
76 Show repository or file history.
75 Show repository or file history.
77
76
78 For URLs of the form ``/log/{revision}``, a list of changesets starting at
77 For URLs of the form ``/log/{revision}``, a list of changesets starting at
79 the specified changeset identifier is shown. If ``{revision}`` is not
78 the specified changeset identifier is shown. If ``{revision}`` is not
80 defined, the default is ``tip``. This form is equivalent to the
79 defined, the default is ``tip``. This form is equivalent to the
81 ``changelog`` handler.
80 ``changelog`` handler.
82
81
83 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
82 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
84 file will be shown. This form is equivalent to the ``filelog`` handler.
83 file will be shown. This form is equivalent to the ``filelog`` handler.
85 """
84 """
86
85
87 if 'file' in req.form and req.form['file'][0]:
86 if 'file' in req.form and req.form['file'][0]:
88 return filelog(web, req, tmpl)
87 return filelog(web, req, tmpl)
89 else:
88 else:
90 return changelog(web, req, tmpl)
89 return changelog(web, req, tmpl)
91
90
92 @webcommand('rawfile')
91 @webcommand('rawfile')
93 def rawfile(web, req, tmpl):
92 def rawfile(web, req, tmpl):
94 guessmime = web.configbool('web', 'guessmime', False)
93 guessmime = web.configbool('web', 'guessmime', False)
95
94
96 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
95 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
97 if not path:
96 if not path:
98 content = manifest(web, req, tmpl)
97 content = manifest(web, req, tmpl)
99 req.respond(HTTP_OK, web.ctype)
98 req.respond(HTTP_OK, web.ctype)
100 return content
99 return content
101
100
102 try:
101 try:
103 fctx = webutil.filectx(web.repo, req)
102 fctx = webutil.filectx(web.repo, req)
104 except error.LookupError as inst:
103 except error.LookupError as inst:
105 try:
104 try:
106 content = manifest(web, req, tmpl)
105 content = manifest(web, req, tmpl)
107 req.respond(HTTP_OK, web.ctype)
106 req.respond(HTTP_OK, web.ctype)
108 return content
107 return content
109 except ErrorResponse:
108 except ErrorResponse:
110 raise inst
109 raise inst
111
110
112 path = fctx.path()
111 path = fctx.path()
113 text = fctx.data()
112 text = fctx.data()
114 mt = 'application/binary'
113 mt = 'application/binary'
115 if guessmime:
114 if guessmime:
116 mt = mimetypes.guess_type(path)[0]
115 mt = mimetypes.guess_type(path)[0]
117 if mt is None:
116 if mt is None:
118 if util.binary(text):
117 if util.binary(text):
119 mt = 'application/binary'
118 mt = 'application/binary'
120 else:
119 else:
121 mt = 'text/plain'
120 mt = 'text/plain'
122 if mt.startswith('text/'):
121 if mt.startswith('text/'):
123 mt += '; charset="%s"' % encoding.encoding
122 mt += '; charset="%s"' % encoding.encoding
124
123
125 req.respond(HTTP_OK, mt, path, body=text)
124 req.respond(HTTP_OK, mt, path, body=text)
126 return []
125 return []
127
126
128 def _filerevision(web, req, tmpl, fctx):
127 def _filerevision(web, req, tmpl, fctx):
129 f = fctx.path()
128 f = fctx.path()
130 text = fctx.data()
129 text = fctx.data()
131 parity = paritygen(web.stripecount)
130 parity = paritygen(web.stripecount)
132
131
133 if util.binary(text):
132 if util.binary(text):
134 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
133 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
135 text = '(binary:%s)' % mt
134 text = '(binary:%s)' % mt
136
135
137 def lines():
136 def lines():
138 for lineno, t in enumerate(text.splitlines(True)):
137 for lineno, t in enumerate(text.splitlines(True)):
139 yield {"line": t,
138 yield {"line": t,
140 "lineid": "l%d" % (lineno + 1),
139 "lineid": "l%d" % (lineno + 1),
141 "linenumber": "% 6d" % (lineno + 1),
140 "linenumber": "% 6d" % (lineno + 1),
142 "parity": next(parity)}
141 "parity": next(parity)}
143
142
144 return tmpl("filerevision",
143 return tmpl("filerevision",
145 file=f,
144 file=f,
146 path=webutil.up(f),
145 path=webutil.up(f),
147 text=lines(),
146 text=lines(),
148 symrev=webutil.symrevorshortnode(req, fctx),
147 symrev=webutil.symrevorshortnode(req, fctx),
149 rename=webutil.renamelink(fctx),
148 rename=webutil.renamelink(fctx),
150 permissions=fctx.manifest().flags(f),
149 permissions=fctx.manifest().flags(f),
151 **webutil.commonentry(web.repo, fctx))
150 **webutil.commonentry(web.repo, fctx))
152
151
153 @webcommand('file')
152 @webcommand('file')
154 def file(web, req, tmpl):
153 def file(web, req, tmpl):
155 """
154 """
156 /file/{revision}[/{path}]
155 /file/{revision}[/{path}]
157 -------------------------
156 -------------------------
158
157
159 Show information about a directory or file in the repository.
158 Show information about a directory or file in the repository.
160
159
161 Info about the ``path`` given as a URL parameter will be rendered.
160 Info about the ``path`` given as a URL parameter will be rendered.
162
161
163 If ``path`` is a directory, information about the entries in that
162 If ``path`` is a directory, information about the entries in that
164 directory will be rendered. This form is equivalent to the ``manifest``
163 directory will be rendered. This form is equivalent to the ``manifest``
165 handler.
164 handler.
166
165
167 If ``path`` is a file, information about that file will be shown via
166 If ``path`` is a file, information about that file will be shown via
168 the ``filerevision`` template.
167 the ``filerevision`` template.
169
168
170 If ``path`` is not defined, information about the root directory will
169 If ``path`` is not defined, information about the root directory will
171 be rendered.
170 be rendered.
172 """
171 """
173 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
172 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
174 if not path:
173 if not path:
175 return manifest(web, req, tmpl)
174 return manifest(web, req, tmpl)
176 try:
175 try:
177 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
176 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
178 except error.LookupError as inst:
177 except error.LookupError as inst:
179 try:
178 try:
180 return manifest(web, req, tmpl)
179 return manifest(web, req, tmpl)
181 except ErrorResponse:
180 except ErrorResponse:
182 raise inst
181 raise inst
183
182
184 def _search(web, req, tmpl):
183 def _search(web, req, tmpl):
185 MODE_REVISION = 'rev'
184 MODE_REVISION = 'rev'
186 MODE_KEYWORD = 'keyword'
185 MODE_KEYWORD = 'keyword'
187 MODE_REVSET = 'revset'
186 MODE_REVSET = 'revset'
188
187
189 def revsearch(ctx):
188 def revsearch(ctx):
190 yield ctx
189 yield ctx
191
190
192 def keywordsearch(query):
191 def keywordsearch(query):
193 lower = encoding.lower
192 lower = encoding.lower
194 qw = lower(query).split()
193 qw = lower(query).split()
195
194
196 def revgen():
195 def revgen():
197 cl = web.repo.changelog
196 cl = web.repo.changelog
198 for i in xrange(len(web.repo) - 1, 0, -100):
197 for i in xrange(len(web.repo) - 1, 0, -100):
199 l = []
198 l = []
200 for j in cl.revs(max(0, i - 99), i):
199 for j in cl.revs(max(0, i - 99), i):
201 ctx = web.repo[j]
200 ctx = web.repo[j]
202 l.append(ctx)
201 l.append(ctx)
203 l.reverse()
202 l.reverse()
204 for e in l:
203 for e in l:
205 yield e
204 yield e
206
205
207 for ctx in revgen():
206 for ctx in revgen():
208 miss = 0
207 miss = 0
209 for q in qw:
208 for q in qw:
210 if not (q in lower(ctx.user()) or
209 if not (q in lower(ctx.user()) or
211 q in lower(ctx.description()) or
210 q in lower(ctx.description()) or
212 q in lower(" ".join(ctx.files()))):
211 q in lower(" ".join(ctx.files()))):
213 miss = 1
212 miss = 1
214 break
213 break
215 if miss:
214 if miss:
216 continue
215 continue
217
216
218 yield ctx
217 yield ctx
219
218
220 def revsetsearch(revs):
219 def revsetsearch(revs):
221 for r in revs:
220 for r in revs:
222 yield web.repo[r]
221 yield web.repo[r]
223
222
224 searchfuncs = {
223 searchfuncs = {
225 MODE_REVISION: (revsearch, 'exact revision search'),
224 MODE_REVISION: (revsearch, 'exact revision search'),
226 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
225 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
227 MODE_REVSET: (revsetsearch, 'revset expression search'),
226 MODE_REVSET: (revsetsearch, 'revset expression search'),
228 }
227 }
229
228
230 def getsearchmode(query):
229 def getsearchmode(query):
231 try:
230 try:
232 ctx = web.repo[query]
231 ctx = web.repo[query]
233 except (error.RepoError, error.LookupError):
232 except (error.RepoError, error.LookupError):
234 # query is not an exact revision pointer, need to
233 # query is not an exact revision pointer, need to
235 # decide if it's a revset expression or keywords
234 # decide if it's a revset expression or keywords
236 pass
235 pass
237 else:
236 else:
238 return MODE_REVISION, ctx
237 return MODE_REVISION, ctx
239
238
240 revdef = 'reverse(%s)' % query
239 revdef = 'reverse(%s)' % query
241 try:
240 try:
242 tree = revset.parse(revdef)
241 tree = revset.parse(revdef)
243 except error.ParseError:
242 except error.ParseError:
244 # can't parse to a revset tree
243 # can't parse to a revset tree
245 return MODE_KEYWORD, query
244 return MODE_KEYWORD, query
246
245
247 if revset.depth(tree) <= 2:
246 if revset.depth(tree) <= 2:
248 # no revset syntax used
247 # no revset syntax used
249 return MODE_KEYWORD, query
248 return MODE_KEYWORD, query
250
249
251 if any((token, (value or '')[:3]) == ('string', 're:')
250 if any((token, (value or '')[:3]) == ('string', 're:')
252 for token, value, pos in revset.tokenize(revdef)):
251 for token, value, pos in revset.tokenize(revdef)):
253 return MODE_KEYWORD, query
252 return MODE_KEYWORD, query
254
253
255 funcsused = revset.funcsused(tree)
254 funcsused = revset.funcsused(tree)
256 if not funcsused.issubset(revset.safesymbols):
255 if not funcsused.issubset(revset.safesymbols):
257 return MODE_KEYWORD, query
256 return MODE_KEYWORD, query
258
257
259 mfunc = revset.match(web.repo.ui, revdef)
258 mfunc = revset.match(web.repo.ui, revdef)
260 try:
259 try:
261 revs = mfunc(web.repo)
260 revs = mfunc(web.repo)
262 return MODE_REVSET, revs
261 return MODE_REVSET, revs
263 # ParseError: wrongly placed tokens, wrongs arguments, etc
262 # ParseError: wrongly placed tokens, wrongs arguments, etc
264 # RepoLookupError: no such revision, e.g. in 'revision:'
263 # RepoLookupError: no such revision, e.g. in 'revision:'
265 # Abort: bookmark/tag not exists
264 # Abort: bookmark/tag not exists
266 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
265 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
267 except (error.ParseError, error.RepoLookupError, error.Abort,
266 except (error.ParseError, error.RepoLookupError, error.Abort,
268 LookupError):
267 LookupError):
269 return MODE_KEYWORD, query
268 return MODE_KEYWORD, query
270
269
271 def changelist(**map):
270 def changelist(**map):
272 count = 0
271 count = 0
273
272
274 for ctx in searchfunc[0](funcarg):
273 for ctx in searchfunc[0](funcarg):
275 count += 1
274 count += 1
276 n = ctx.node()
275 n = ctx.node()
277 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
276 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
278 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
277 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
279
278
280 yield tmpl('searchentry',
279 yield tmpl('searchentry',
281 parity=next(parity),
280 parity=next(parity),
282 changelogtag=showtags,
281 changelogtag=showtags,
283 files=files,
282 files=files,
284 **webutil.commonentry(web.repo, ctx))
283 **webutil.commonentry(web.repo, ctx))
285
284
286 if count >= revcount:
285 if count >= revcount:
287 break
286 break
288
287
289 query = req.form['rev'][0]
288 query = req.form['rev'][0]
290 revcount = web.maxchanges
289 revcount = web.maxchanges
291 if 'revcount' in req.form:
290 if 'revcount' in req.form:
292 try:
291 try:
293 revcount = int(req.form.get('revcount', [revcount])[0])
292 revcount = int(req.form.get('revcount', [revcount])[0])
294 revcount = max(revcount, 1)
293 revcount = max(revcount, 1)
295 tmpl.defaults['sessionvars']['revcount'] = revcount
294 tmpl.defaults['sessionvars']['revcount'] = revcount
296 except ValueError:
295 except ValueError:
297 pass
296 pass
298
297
299 lessvars = copy.copy(tmpl.defaults['sessionvars'])
298 lessvars = copy.copy(tmpl.defaults['sessionvars'])
300 lessvars['revcount'] = max(revcount / 2, 1)
299 lessvars['revcount'] = max(revcount / 2, 1)
301 lessvars['rev'] = query
300 lessvars['rev'] = query
302 morevars = copy.copy(tmpl.defaults['sessionvars'])
301 morevars = copy.copy(tmpl.defaults['sessionvars'])
303 morevars['revcount'] = revcount * 2
302 morevars['revcount'] = revcount * 2
304 morevars['rev'] = query
303 morevars['rev'] = query
305
304
306 mode, funcarg = getsearchmode(query)
305 mode, funcarg = getsearchmode(query)
307
306
308 if 'forcekw' in req.form:
307 if 'forcekw' in req.form:
309 showforcekw = ''
308 showforcekw = ''
310 showunforcekw = searchfuncs[mode][1]
309 showunforcekw = searchfuncs[mode][1]
311 mode = MODE_KEYWORD
310 mode = MODE_KEYWORD
312 funcarg = query
311 funcarg = query
313 else:
312 else:
314 if mode != MODE_KEYWORD:
313 if mode != MODE_KEYWORD:
315 showforcekw = searchfuncs[MODE_KEYWORD][1]
314 showforcekw = searchfuncs[MODE_KEYWORD][1]
316 else:
315 else:
317 showforcekw = ''
316 showforcekw = ''
318 showunforcekw = ''
317 showunforcekw = ''
319
318
320 searchfunc = searchfuncs[mode]
319 searchfunc = searchfuncs[mode]
321
320
322 tip = web.repo['tip']
321 tip = web.repo['tip']
323 parity = paritygen(web.stripecount)
322 parity = paritygen(web.stripecount)
324
323
325 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
324 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
326 entries=changelist, archives=web.archivelist("tip"),
325 entries=changelist, archives=web.archivelist("tip"),
327 morevars=morevars, lessvars=lessvars,
326 morevars=morevars, lessvars=lessvars,
328 modedesc=searchfunc[1],
327 modedesc=searchfunc[1],
329 showforcekw=showforcekw, showunforcekw=showunforcekw)
328 showforcekw=showforcekw, showunforcekw=showunforcekw)
330
329
331 @webcommand('changelog')
330 @webcommand('changelog')
332 def changelog(web, req, tmpl, shortlog=False):
331 def changelog(web, req, tmpl, shortlog=False):
333 """
332 """
334 /changelog[/{revision}]
333 /changelog[/{revision}]
335 -----------------------
334 -----------------------
336
335
337 Show information about multiple changesets.
336 Show information about multiple changesets.
338
337
339 If the optional ``revision`` URL argument is absent, information about
338 If the optional ``revision`` URL argument is absent, information about
340 all changesets starting at ``tip`` will be rendered. If the ``revision``
339 all changesets starting at ``tip`` will be rendered. If the ``revision``
341 argument is present, changesets will be shown starting from the specified
340 argument is present, changesets will be shown starting from the specified
342 revision.
341 revision.
343
342
344 If ``revision`` is absent, the ``rev`` query string argument may be
343 If ``revision`` is absent, the ``rev`` query string argument may be
345 defined. This will perform a search for changesets.
344 defined. This will perform a search for changesets.
346
345
347 The argument for ``rev`` can be a single revision, a revision set,
346 The argument for ``rev`` can be a single revision, a revision set,
348 or a literal keyword to search for in changeset data (equivalent to
347 or a literal keyword to search for in changeset data (equivalent to
349 :hg:`log -k`).
348 :hg:`log -k`).
350
349
351 The ``revcount`` query string argument defines the maximum numbers of
350 The ``revcount`` query string argument defines the maximum numbers of
352 changesets to render.
351 changesets to render.
353
352
354 For non-searches, the ``changelog`` template will be rendered.
353 For non-searches, the ``changelog`` template will be rendered.
355 """
354 """
356
355
357 query = ''
356 query = ''
358 if 'node' in req.form:
357 if 'node' in req.form:
359 ctx = webutil.changectx(web.repo, req)
358 ctx = webutil.changectx(web.repo, req)
360 symrev = webutil.symrevorshortnode(req, ctx)
359 symrev = webutil.symrevorshortnode(req, ctx)
361 elif 'rev' in req.form:
360 elif 'rev' in req.form:
362 return _search(web, req, tmpl)
361 return _search(web, req, tmpl)
363 else:
362 else:
364 ctx = web.repo['tip']
363 ctx = web.repo['tip']
365 symrev = 'tip'
364 symrev = 'tip'
366
365
367 def changelist():
366 def changelist():
368 revs = []
367 revs = []
369 if pos != -1:
368 if pos != -1:
370 revs = web.repo.changelog.revs(pos, 0)
369 revs = web.repo.changelog.revs(pos, 0)
371 curcount = 0
370 curcount = 0
372 for rev in revs:
371 for rev in revs:
373 curcount += 1
372 curcount += 1
374 if curcount > revcount + 1:
373 if curcount > revcount + 1:
375 break
374 break
376
375
377 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
376 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
378 entry['parity'] = next(parity)
377 entry['parity'] = next(parity)
379 yield entry
378 yield entry
380
379
381 if shortlog:
380 if shortlog:
382 revcount = web.maxshortchanges
381 revcount = web.maxshortchanges
383 else:
382 else:
384 revcount = web.maxchanges
383 revcount = web.maxchanges
385
384
386 if 'revcount' in req.form:
385 if 'revcount' in req.form:
387 try:
386 try:
388 revcount = int(req.form.get('revcount', [revcount])[0])
387 revcount = int(req.form.get('revcount', [revcount])[0])
389 revcount = max(revcount, 1)
388 revcount = max(revcount, 1)
390 tmpl.defaults['sessionvars']['revcount'] = revcount
389 tmpl.defaults['sessionvars']['revcount'] = revcount
391 except ValueError:
390 except ValueError:
392 pass
391 pass
393
392
394 lessvars = copy.copy(tmpl.defaults['sessionvars'])
393 lessvars = copy.copy(tmpl.defaults['sessionvars'])
395 lessvars['revcount'] = max(revcount / 2, 1)
394 lessvars['revcount'] = max(revcount / 2, 1)
396 morevars = copy.copy(tmpl.defaults['sessionvars'])
395 morevars = copy.copy(tmpl.defaults['sessionvars'])
397 morevars['revcount'] = revcount * 2
396 morevars['revcount'] = revcount * 2
398
397
399 count = len(web.repo)
398 count = len(web.repo)
400 pos = ctx.rev()
399 pos = ctx.rev()
401 parity = paritygen(web.stripecount)
400 parity = paritygen(web.stripecount)
402
401
403 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
402 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
404
403
405 entries = list(changelist())
404 entries = list(changelist())
406 latestentry = entries[:1]
405 latestentry = entries[:1]
407 if len(entries) > revcount:
406 if len(entries) > revcount:
408 nextentry = entries[-1:]
407 nextentry = entries[-1:]
409 entries = entries[:-1]
408 entries = entries[:-1]
410 else:
409 else:
411 nextentry = []
410 nextentry = []
412
411
413 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
412 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
414 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
413 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
415 entries=entries,
414 entries=entries,
416 latestentry=latestentry, nextentry=nextentry,
415 latestentry=latestentry, nextentry=nextentry,
417 archives=web.archivelist("tip"), revcount=revcount,
416 archives=web.archivelist("tip"), revcount=revcount,
418 morevars=morevars, lessvars=lessvars, query=query)
417 morevars=morevars, lessvars=lessvars, query=query)
419
418
420 @webcommand('shortlog')
419 @webcommand('shortlog')
421 def shortlog(web, req, tmpl):
420 def shortlog(web, req, tmpl):
422 """
421 """
423 /shortlog
422 /shortlog
424 ---------
423 ---------
425
424
426 Show basic information about a set of changesets.
425 Show basic information about a set of changesets.
427
426
428 This accepts the same parameters as the ``changelog`` handler. The only
427 This accepts the same parameters as the ``changelog`` handler. The only
429 difference is the ``shortlog`` template will be rendered instead of the
428 difference is the ``shortlog`` template will be rendered instead of the
430 ``changelog`` template.
429 ``changelog`` template.
431 """
430 """
432 return changelog(web, req, tmpl, shortlog=True)
431 return changelog(web, req, tmpl, shortlog=True)
433
432
434 @webcommand('changeset')
433 @webcommand('changeset')
435 def changeset(web, req, tmpl):
434 def changeset(web, req, tmpl):
436 """
435 """
437 /changeset[/{revision}]
436 /changeset[/{revision}]
438 -----------------------
437 -----------------------
439
438
440 Show information about a single changeset.
439 Show information about a single changeset.
441
440
442 A URL path argument is the changeset identifier to show. See ``hg help
441 A URL path argument is the changeset identifier to show. See ``hg help
443 revisions`` for possible values. If not defined, the ``tip`` changeset
442 revisions`` for possible values. If not defined, the ``tip`` changeset
444 will be shown.
443 will be shown.
445
444
446 The ``changeset`` template is rendered. Contents of the ``changesettag``,
445 The ``changeset`` template is rendered. Contents of the ``changesettag``,
447 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
446 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
448 templates related to diffs may all be used to produce the output.
447 templates related to diffs may all be used to produce the output.
449 """
448 """
450 ctx = webutil.changectx(web.repo, req)
449 ctx = webutil.changectx(web.repo, req)
451
450
452 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
451 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
453
452
454 rev = webcommand('rev')(changeset)
453 rev = webcommand('rev')(changeset)
455
454
456 def decodepath(path):
455 def decodepath(path):
457 """Hook for mapping a path in the repository to a path in the
456 """Hook for mapping a path in the repository to a path in the
458 working copy.
457 working copy.
459
458
460 Extensions (e.g., largefiles) can override this to remap files in
459 Extensions (e.g., largefiles) can override this to remap files in
461 the virtual file system presented by the manifest command below."""
460 the virtual file system presented by the manifest command below."""
462 return path
461 return path
463
462
464 @webcommand('manifest')
463 @webcommand('manifest')
465 def manifest(web, req, tmpl):
464 def manifest(web, req, tmpl):
466 """
465 """
467 /manifest[/{revision}[/{path}]]
466 /manifest[/{revision}[/{path}]]
468 -------------------------------
467 -------------------------------
469
468
470 Show information about a directory.
469 Show information about a directory.
471
470
472 If the URL path arguments are omitted, information about the root
471 If the URL path arguments are omitted, information about the root
473 directory for the ``tip`` changeset will be shown.
472 directory for the ``tip`` changeset will be shown.
474
473
475 Because this handler can only show information for directories, it
474 Because this handler can only show information for directories, it
476 is recommended to use the ``file`` handler instead, as it can handle both
475 is recommended to use the ``file`` handler instead, as it can handle both
477 directories and files.
476 directories and files.
478
477
479 The ``manifest`` template will be rendered for this handler.
478 The ``manifest`` template will be rendered for this handler.
480 """
479 """
481 if 'node' in req.form:
480 if 'node' in req.form:
482 ctx = webutil.changectx(web.repo, req)
481 ctx = webutil.changectx(web.repo, req)
483 symrev = webutil.symrevorshortnode(req, ctx)
482 symrev = webutil.symrevorshortnode(req, ctx)
484 else:
483 else:
485 ctx = web.repo['tip']
484 ctx = web.repo['tip']
486 symrev = 'tip'
485 symrev = 'tip'
487 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
486 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
488 mf = ctx.manifest()
487 mf = ctx.manifest()
489 node = ctx.node()
488 node = ctx.node()
490
489
491 files = {}
490 files = {}
492 dirs = {}
491 dirs = {}
493 parity = paritygen(web.stripecount)
492 parity = paritygen(web.stripecount)
494
493
495 if path and path[-1] != "/":
494 if path and path[-1] != "/":
496 path += "/"
495 path += "/"
497 l = len(path)
496 l = len(path)
498 abspath = "/" + path
497 abspath = "/" + path
499
498
500 for full, n in mf.iteritems():
499 for full, n in mf.iteritems():
501 # the virtual path (working copy path) used for the full
500 # the virtual path (working copy path) used for the full
502 # (repository) path
501 # (repository) path
503 f = decodepath(full)
502 f = decodepath(full)
504
503
505 if f[:l] != path:
504 if f[:l] != path:
506 continue
505 continue
507 remain = f[l:]
506 remain = f[l:]
508 elements = remain.split('/')
507 elements = remain.split('/')
509 if len(elements) == 1:
508 if len(elements) == 1:
510 files[remain] = full
509 files[remain] = full
511 else:
510 else:
512 h = dirs # need to retain ref to dirs (root)
511 h = dirs # need to retain ref to dirs (root)
513 for elem in elements[0:-1]:
512 for elem in elements[0:-1]:
514 if elem not in h:
513 if elem not in h:
515 h[elem] = {}
514 h[elem] = {}
516 h = h[elem]
515 h = h[elem]
517 if len(h) > 1:
516 if len(h) > 1:
518 break
517 break
519 h[None] = None # denotes files present
518 h[None] = None # denotes files present
520
519
521 if mf and not files and not dirs:
520 if mf and not files and not dirs:
522 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
521 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
523
522
524 def filelist(**map):
523 def filelist(**map):
525 for f in sorted(files):
524 for f in sorted(files):
526 full = files[f]
525 full = files[f]
527
526
528 fctx = ctx.filectx(full)
527 fctx = ctx.filectx(full)
529 yield {"file": full,
528 yield {"file": full,
530 "parity": next(parity),
529 "parity": next(parity),
531 "basename": f,
530 "basename": f,
532 "date": fctx.date(),
531 "date": fctx.date(),
533 "size": fctx.size(),
532 "size": fctx.size(),
534 "permissions": mf.flags(full)}
533 "permissions": mf.flags(full)}
535
534
536 def dirlist(**map):
535 def dirlist(**map):
537 for d in sorted(dirs):
536 for d in sorted(dirs):
538
537
539 emptydirs = []
538 emptydirs = []
540 h = dirs[d]
539 h = dirs[d]
541 while isinstance(h, dict) and len(h) == 1:
540 while isinstance(h, dict) and len(h) == 1:
542 k, v = h.items()[0]
541 k, v = h.items()[0]
543 if v:
542 if v:
544 emptydirs.append(k)
543 emptydirs.append(k)
545 h = v
544 h = v
546
545
547 path = "%s%s" % (abspath, d)
546 path = "%s%s" % (abspath, d)
548 yield {"parity": next(parity),
547 yield {"parity": next(parity),
549 "path": path,
548 "path": path,
550 "emptydirs": "/".join(emptydirs),
549 "emptydirs": "/".join(emptydirs),
551 "basename": d}
550 "basename": d}
552
551
553 return tmpl("manifest",
552 return tmpl("manifest",
554 symrev=symrev,
553 symrev=symrev,
555 path=abspath,
554 path=abspath,
556 up=webutil.up(abspath),
555 up=webutil.up(abspath),
557 upparity=next(parity),
556 upparity=next(parity),
558 fentries=filelist,
557 fentries=filelist,
559 dentries=dirlist,
558 dentries=dirlist,
560 archives=web.archivelist(hex(node)),
559 archives=web.archivelist(hex(node)),
561 **webutil.commonentry(web.repo, ctx))
560 **webutil.commonentry(web.repo, ctx))
562
561
563 @webcommand('tags')
562 @webcommand('tags')
564 def tags(web, req, tmpl):
563 def tags(web, req, tmpl):
565 """
564 """
566 /tags
565 /tags
567 -----
566 -----
568
567
569 Show information about tags.
568 Show information about tags.
570
569
571 No arguments are accepted.
570 No arguments are accepted.
572
571
573 The ``tags`` template is rendered.
572 The ``tags`` template is rendered.
574 """
573 """
575 i = list(reversed(web.repo.tagslist()))
574 i = list(reversed(web.repo.tagslist()))
576 parity = paritygen(web.stripecount)
575 parity = paritygen(web.stripecount)
577
576
578 def entries(notip, latestonly, **map):
577 def entries(notip, latestonly, **map):
579 t = i
578 t = i
580 if notip:
579 if notip:
581 t = [(k, n) for k, n in i if k != "tip"]
580 t = [(k, n) for k, n in i if k != "tip"]
582 if latestonly:
581 if latestonly:
583 t = t[:1]
582 t = t[:1]
584 for k, n in t:
583 for k, n in t:
585 yield {"parity": next(parity),
584 yield {"parity": next(parity),
586 "tag": k,
585 "tag": k,
587 "date": web.repo[n].date(),
586 "date": web.repo[n].date(),
588 "node": hex(n)}
587 "node": hex(n)}
589
588
590 return tmpl("tags",
589 return tmpl("tags",
591 node=hex(web.repo.changelog.tip()),
590 node=hex(web.repo.changelog.tip()),
592 entries=lambda **x: entries(False, False, **x),
591 entries=lambda **x: entries(False, False, **x),
593 entriesnotip=lambda **x: entries(True, False, **x),
592 entriesnotip=lambda **x: entries(True, False, **x),
594 latestentry=lambda **x: entries(True, True, **x))
593 latestentry=lambda **x: entries(True, True, **x))
595
594
596 @webcommand('bookmarks')
595 @webcommand('bookmarks')
597 def bookmarks(web, req, tmpl):
596 def bookmarks(web, req, tmpl):
598 """
597 """
599 /bookmarks
598 /bookmarks
600 ----------
599 ----------
601
600
602 Show information about bookmarks.
601 Show information about bookmarks.
603
602
604 No arguments are accepted.
603 No arguments are accepted.
605
604
606 The ``bookmarks`` template is rendered.
605 The ``bookmarks`` template is rendered.
607 """
606 """
608 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
607 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
609 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
608 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
610 i = sorted(i, key=sortkey, reverse=True)
609 i = sorted(i, key=sortkey, reverse=True)
611 parity = paritygen(web.stripecount)
610 parity = paritygen(web.stripecount)
612
611
613 def entries(latestonly, **map):
612 def entries(latestonly, **map):
614 t = i
613 t = i
615 if latestonly:
614 if latestonly:
616 t = i[:1]
615 t = i[:1]
617 for k, n in t:
616 for k, n in t:
618 yield {"parity": next(parity),
617 yield {"parity": next(parity),
619 "bookmark": k,
618 "bookmark": k,
620 "date": web.repo[n].date(),
619 "date": web.repo[n].date(),
621 "node": hex(n)}
620 "node": hex(n)}
622
621
623 if i:
622 if i:
624 latestrev = i[0][1]
623 latestrev = i[0][1]
625 else:
624 else:
626 latestrev = -1
625 latestrev = -1
627
626
628 return tmpl("bookmarks",
627 return tmpl("bookmarks",
629 node=hex(web.repo.changelog.tip()),
628 node=hex(web.repo.changelog.tip()),
630 lastchange=[{"date": web.repo[latestrev].date()}],
629 lastchange=[{"date": web.repo[latestrev].date()}],
631 entries=lambda **x: entries(latestonly=False, **x),
630 entries=lambda **x: entries(latestonly=False, **x),
632 latestentry=lambda **x: entries(latestonly=True, **x))
631 latestentry=lambda **x: entries(latestonly=True, **x))
633
632
634 @webcommand('branches')
633 @webcommand('branches')
635 def branches(web, req, tmpl):
634 def branches(web, req, tmpl):
636 """
635 """
637 /branches
636 /branches
638 ---------
637 ---------
639
638
640 Show information about branches.
639 Show information about branches.
641
640
642 All known branches are contained in the output, even closed branches.
641 All known branches are contained in the output, even closed branches.
643
642
644 No arguments are accepted.
643 No arguments are accepted.
645
644
646 The ``branches`` template is rendered.
645 The ``branches`` template is rendered.
647 """
646 """
648 entries = webutil.branchentries(web.repo, web.stripecount)
647 entries = webutil.branchentries(web.repo, web.stripecount)
649 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
648 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
650 return tmpl('branches', node=hex(web.repo.changelog.tip()),
649 return tmpl('branches', node=hex(web.repo.changelog.tip()),
651 entries=entries, latestentry=latestentry)
650 entries=entries, latestentry=latestentry)
652
651
653 @webcommand('summary')
652 @webcommand('summary')
654 def summary(web, req, tmpl):
653 def summary(web, req, tmpl):
655 """
654 """
656 /summary
655 /summary
657 --------
656 --------
658
657
659 Show a summary of repository state.
658 Show a summary of repository state.
660
659
661 Information about the latest changesets, bookmarks, tags, and branches
660 Information about the latest changesets, bookmarks, tags, and branches
662 is captured by this handler.
661 is captured by this handler.
663
662
664 The ``summary`` template is rendered.
663 The ``summary`` template is rendered.
665 """
664 """
666 i = reversed(web.repo.tagslist())
665 i = reversed(web.repo.tagslist())
667
666
668 def tagentries(**map):
667 def tagentries(**map):
669 parity = paritygen(web.stripecount)
668 parity = paritygen(web.stripecount)
670 count = 0
669 count = 0
671 for k, n in i:
670 for k, n in i:
672 if k == "tip": # skip tip
671 if k == "tip": # skip tip
673 continue
672 continue
674
673
675 count += 1
674 count += 1
676 if count > 10: # limit to 10 tags
675 if count > 10: # limit to 10 tags
677 break
676 break
678
677
679 yield tmpl("tagentry",
678 yield tmpl("tagentry",
680 parity=next(parity),
679 parity=next(parity),
681 tag=k,
680 tag=k,
682 node=hex(n),
681 node=hex(n),
683 date=web.repo[n].date())
682 date=web.repo[n].date())
684
683
685 def bookmarks(**map):
684 def bookmarks(**map):
686 parity = paritygen(web.stripecount)
685 parity = paritygen(web.stripecount)
687 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
686 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
688 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
687 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
689 marks = sorted(marks, key=sortkey, reverse=True)
688 marks = sorted(marks, key=sortkey, reverse=True)
690 for k, n in marks[:10]: # limit to 10 bookmarks
689 for k, n in marks[:10]: # limit to 10 bookmarks
691 yield {'parity': next(parity),
690 yield {'parity': next(parity),
692 'bookmark': k,
691 'bookmark': k,
693 'date': web.repo[n].date(),
692 'date': web.repo[n].date(),
694 'node': hex(n)}
693 'node': hex(n)}
695
694
696 def changelist(**map):
695 def changelist(**map):
697 parity = paritygen(web.stripecount, offset=start - end)
696 parity = paritygen(web.stripecount, offset=start - end)
698 l = [] # build a list in forward order for efficiency
697 l = [] # build a list in forward order for efficiency
699 revs = []
698 revs = []
700 if start < end:
699 if start < end:
701 revs = web.repo.changelog.revs(start, end - 1)
700 revs = web.repo.changelog.revs(start, end - 1)
702 for i in revs:
701 for i in revs:
703 ctx = web.repo[i]
702 ctx = web.repo[i]
704
703
705 l.append(tmpl(
704 l.append(tmpl(
706 'shortlogentry',
705 'shortlogentry',
707 parity=next(parity),
706 parity=next(parity),
708 **webutil.commonentry(web.repo, ctx)))
707 **webutil.commonentry(web.repo, ctx)))
709
708
710 for entry in reversed(l):
709 for entry in reversed(l):
711 yield entry
710 yield entry
712
711
713 tip = web.repo['tip']
712 tip = web.repo['tip']
714 count = len(web.repo)
713 count = len(web.repo)
715 start = max(0, count - web.maxchanges)
714 start = max(0, count - web.maxchanges)
716 end = min(count, start + web.maxchanges)
715 end = min(count, start + web.maxchanges)
717
716
718 return tmpl("summary",
717 return tmpl("summary",
719 desc=web.config("web", "description", "unknown"),
718 desc=web.config("web", "description", "unknown"),
720 owner=get_contact(web.config) or "unknown",
719 owner=get_contact(web.config) or "unknown",
721 lastchange=tip.date(),
720 lastchange=tip.date(),
722 tags=tagentries,
721 tags=tagentries,
723 bookmarks=bookmarks,
722 bookmarks=bookmarks,
724 branches=webutil.branchentries(web.repo, web.stripecount, 10),
723 branches=webutil.branchentries(web.repo, web.stripecount, 10),
725 shortlog=changelist,
724 shortlog=changelist,
726 node=tip.hex(),
725 node=tip.hex(),
727 symrev='tip',
726 symrev='tip',
728 archives=web.archivelist("tip"),
727 archives=web.archivelist("tip"),
729 labels=web.configlist('web', 'labels'))
728 labels=web.configlist('web', 'labels'))
730
729
731 @webcommand('filediff')
730 @webcommand('filediff')
732 def filediff(web, req, tmpl):
731 def filediff(web, req, tmpl):
733 """
732 """
734 /diff/{revision}/{path}
733 /diff/{revision}/{path}
735 -----------------------
734 -----------------------
736
735
737 Show how a file changed in a particular commit.
736 Show how a file changed in a particular commit.
738
737
739 The ``filediff`` template is rendered.
738 The ``filediff`` template is rendered.
740
739
741 This handler is registered under both the ``/diff`` and ``/filediff``
740 This handler is registered under both the ``/diff`` and ``/filediff``
742 paths. ``/diff`` is used in modern code.
741 paths. ``/diff`` is used in modern code.
743 """
742 """
744 fctx, ctx = None, None
743 fctx, ctx = None, None
745 try:
744 try:
746 fctx = webutil.filectx(web.repo, req)
745 fctx = webutil.filectx(web.repo, req)
747 except LookupError:
746 except LookupError:
748 ctx = webutil.changectx(web.repo, req)
747 ctx = webutil.changectx(web.repo, req)
749 path = webutil.cleanpath(web.repo, req.form['file'][0])
748 path = webutil.cleanpath(web.repo, req.form['file'][0])
750 if path not in ctx.files():
749 if path not in ctx.files():
751 raise
750 raise
752
751
753 if fctx is not None:
752 if fctx is not None:
754 path = fctx.path()
753 path = fctx.path()
755 ctx = fctx.changectx()
754 ctx = fctx.changectx()
756
755
757 parity = paritygen(web.stripecount)
756 parity = paritygen(web.stripecount)
758 style = web.config('web', 'style', 'paper')
757 style = web.config('web', 'style', 'paper')
759 if 'style' in req.form:
758 if 'style' in req.form:
760 style = req.form['style'][0]
759 style = req.form['style'][0]
761
760
762 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
761 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
763 if fctx is not None:
762 if fctx is not None:
764 rename = webutil.renamelink(fctx)
763 rename = webutil.renamelink(fctx)
765 ctx = fctx
764 ctx = fctx
766 else:
765 else:
767 rename = []
766 rename = []
768 ctx = ctx
767 ctx = ctx
769 return tmpl("filediff",
768 return tmpl("filediff",
770 file=path,
769 file=path,
771 symrev=webutil.symrevorshortnode(req, ctx),
770 symrev=webutil.symrevorshortnode(req, ctx),
772 rename=rename,
771 rename=rename,
773 diff=diffs,
772 diff=diffs,
774 **webutil.commonentry(web.repo, ctx))
773 **webutil.commonentry(web.repo, ctx))
775
774
776 diff = webcommand('diff')(filediff)
775 diff = webcommand('diff')(filediff)
777
776
778 @webcommand('comparison')
777 @webcommand('comparison')
779 def comparison(web, req, tmpl):
778 def comparison(web, req, tmpl):
780 """
779 """
781 /comparison/{revision}/{path}
780 /comparison/{revision}/{path}
782 -----------------------------
781 -----------------------------
783
782
784 Show a comparison between the old and new versions of a file from changes
783 Show a comparison between the old and new versions of a file from changes
785 made on a particular revision.
784 made on a particular revision.
786
785
787 This is similar to the ``diff`` handler. However, this form features
786 This is similar to the ``diff`` handler. However, this form features
788 a split or side-by-side diff rather than a unified diff.
787 a split or side-by-side diff rather than a unified diff.
789
788
790 The ``context`` query string argument can be used to control the lines of
789 The ``context`` query string argument can be used to control the lines of
791 context in the diff.
790 context in the diff.
792
791
793 The ``filecomparison`` template is rendered.
792 The ``filecomparison`` template is rendered.
794 """
793 """
795 ctx = webutil.changectx(web.repo, req)
794 ctx = webutil.changectx(web.repo, req)
796 if 'file' not in req.form:
795 if 'file' not in req.form:
797 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
796 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
798 path = webutil.cleanpath(web.repo, req.form['file'][0])
797 path = webutil.cleanpath(web.repo, req.form['file'][0])
799
798
800 parsecontext = lambda v: v == 'full' and -1 or int(v)
799 parsecontext = lambda v: v == 'full' and -1 or int(v)
801 if 'context' in req.form:
800 if 'context' in req.form:
802 context = parsecontext(req.form['context'][0])
801 context = parsecontext(req.form['context'][0])
803 else:
802 else:
804 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
803 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
805
804
806 def filelines(f):
805 def filelines(f):
807 if util.binary(f.data()):
806 if util.binary(f.data()):
808 mt = mimetypes.guess_type(f.path())[0]
807 mt = mimetypes.guess_type(f.path())[0]
809 if not mt:
808 if not mt:
810 mt = 'application/octet-stream'
809 mt = 'application/octet-stream'
811 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
810 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
812 return f.data().splitlines()
811 return f.data().splitlines()
813
812
814 fctx = None
813 fctx = None
815 parent = ctx.p1()
814 parent = ctx.p1()
816 leftrev = parent.rev()
815 leftrev = parent.rev()
817 leftnode = parent.node()
816 leftnode = parent.node()
818 rightrev = ctx.rev()
817 rightrev = ctx.rev()
819 rightnode = ctx.node()
818 rightnode = ctx.node()
820 if path in ctx:
819 if path in ctx:
821 fctx = ctx[path]
820 fctx = ctx[path]
822 rightlines = filelines(fctx)
821 rightlines = filelines(fctx)
823 if path not in parent:
822 if path not in parent:
824 leftlines = ()
823 leftlines = ()
825 else:
824 else:
826 pfctx = parent[path]
825 pfctx = parent[path]
827 leftlines = filelines(pfctx)
826 leftlines = filelines(pfctx)
828 else:
827 else:
829 rightlines = ()
828 rightlines = ()
830 pfctx = ctx.parents()[0][path]
829 pfctx = ctx.parents()[0][path]
831 leftlines = filelines(pfctx)
830 leftlines = filelines(pfctx)
832
831
833 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
832 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
834 if fctx is not None:
833 if fctx is not None:
835 rename = webutil.renamelink(fctx)
834 rename = webutil.renamelink(fctx)
836 ctx = fctx
835 ctx = fctx
837 else:
836 else:
838 rename = []
837 rename = []
839 ctx = ctx
838 ctx = ctx
840 return tmpl('filecomparison',
839 return tmpl('filecomparison',
841 file=path,
840 file=path,
842 symrev=webutil.symrevorshortnode(req, ctx),
841 symrev=webutil.symrevorshortnode(req, ctx),
843 rename=rename,
842 rename=rename,
844 leftrev=leftrev,
843 leftrev=leftrev,
845 leftnode=hex(leftnode),
844 leftnode=hex(leftnode),
846 rightrev=rightrev,
845 rightrev=rightrev,
847 rightnode=hex(rightnode),
846 rightnode=hex(rightnode),
848 comparison=comparison,
847 comparison=comparison,
849 **webutil.commonentry(web.repo, ctx))
848 **webutil.commonentry(web.repo, ctx))
850
849
851 @webcommand('annotate')
850 @webcommand('annotate')
852 def annotate(web, req, tmpl):
851 def annotate(web, req, tmpl):
853 """
852 """
854 /annotate/{revision}/{path}
853 /annotate/{revision}/{path}
855 ---------------------------
854 ---------------------------
856
855
857 Show changeset information for each line in a file.
856 Show changeset information for each line in a file.
858
857
859 The ``fileannotate`` template is rendered.
858 The ``fileannotate`` template is rendered.
860 """
859 """
861 fctx = webutil.filectx(web.repo, req)
860 fctx = webutil.filectx(web.repo, req)
862 f = fctx.path()
861 f = fctx.path()
863 parity = paritygen(web.stripecount)
862 parity = paritygen(web.stripecount)
864 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
865 section='annotate', whitespace=True)
866
863
867 def parents(f):
864 def parents(f):
868 for p in f.parents():
865 for p in f.parents():
869 yield {
866 yield {
870 "node": p.hex(),
867 "node": p.hex(),
871 "rev": p.rev(),
868 "rev": p.rev(),
872 }
869 }
873
870
874 def annotate(**map):
871 def annotate(**map):
875 if util.binary(fctx.data()):
872 if util.binary(fctx.data()):
876 mt = (mimetypes.guess_type(fctx.path())[0]
873 mt = (mimetypes.guess_type(fctx.path())[0]
877 or 'application/octet-stream')
874 or 'application/octet-stream')
878 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
875 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
879 else:
876 else:
880 lines = fctx.annotate(follow=True, linenumber=True,
877 lines = webutil.annotate(fctx, web.repo.ui)
881 diffopts=diffopts)
878
882 previousrev = None
879 previousrev = None
883 blockparitygen = paritygen(1)
880 blockparitygen = paritygen(1)
884 for lineno, ((f, targetline), l) in enumerate(lines):
881 for lineno, ((f, targetline), l) in enumerate(lines):
885 rev = f.rev()
882 rev = f.rev()
886 if rev != previousrev:
883 if rev != previousrev:
887 blockhead = True
884 blockhead = True
888 blockparity = next(blockparitygen)
885 blockparity = next(blockparitygen)
889 else:
886 else:
890 blockhead = None
887 blockhead = None
891 previousrev = rev
888 previousrev = rev
892 yield {"parity": next(parity),
889 yield {"parity": next(parity),
893 "node": f.hex(),
890 "node": f.hex(),
894 "rev": rev,
891 "rev": rev,
895 "author": f.user(),
892 "author": f.user(),
896 "parents": parents(f),
893 "parents": parents(f),
897 "desc": f.description(),
894 "desc": f.description(),
898 "extra": f.extra(),
895 "extra": f.extra(),
899 "file": f.path(),
896 "file": f.path(),
900 "blockhead": blockhead,
897 "blockhead": blockhead,
901 "blockparity": blockparity,
898 "blockparity": blockparity,
902 "targetline": targetline,
899 "targetline": targetline,
903 "line": l,
900 "line": l,
904 "lineno": lineno + 1,
901 "lineno": lineno + 1,
905 "lineid": "l%d" % (lineno + 1),
902 "lineid": "l%d" % (lineno + 1),
906 "linenumber": "% 6d" % (lineno + 1),
903 "linenumber": "% 6d" % (lineno + 1),
907 "revdate": f.date()}
904 "revdate": f.date()}
908
905
909 return tmpl("fileannotate",
906 return tmpl("fileannotate",
910 file=f,
907 file=f,
911 annotate=annotate,
908 annotate=annotate,
912 path=webutil.up(f),
909 path=webutil.up(f),
913 symrev=webutil.symrevorshortnode(req, fctx),
910 symrev=webutil.symrevorshortnode(req, fctx),
914 rename=webutil.renamelink(fctx),
911 rename=webutil.renamelink(fctx),
915 permissions=fctx.manifest().flags(f),
912 permissions=fctx.manifest().flags(f),
916 **webutil.commonentry(web.repo, fctx))
913 **webutil.commonentry(web.repo, fctx))
917
914
918 @webcommand('filelog')
915 @webcommand('filelog')
919 def filelog(web, req, tmpl):
916 def filelog(web, req, tmpl):
920 """
917 """
921 /filelog/{revision}/{path}
918 /filelog/{revision}/{path}
922 --------------------------
919 --------------------------
923
920
924 Show information about the history of a file in the repository.
921 Show information about the history of a file in the repository.
925
922
926 The ``revcount`` query string argument can be defined to control the
923 The ``revcount`` query string argument can be defined to control the
927 maximum number of entries to show.
924 maximum number of entries to show.
928
925
929 The ``filelog`` template will be rendered.
926 The ``filelog`` template will be rendered.
930 """
927 """
931
928
932 try:
929 try:
933 fctx = webutil.filectx(web.repo, req)
930 fctx = webutil.filectx(web.repo, req)
934 f = fctx.path()
931 f = fctx.path()
935 fl = fctx.filelog()
932 fl = fctx.filelog()
936 except error.LookupError:
933 except error.LookupError:
937 f = webutil.cleanpath(web.repo, req.form['file'][0])
934 f = webutil.cleanpath(web.repo, req.form['file'][0])
938 fl = web.repo.file(f)
935 fl = web.repo.file(f)
939 numrevs = len(fl)
936 numrevs = len(fl)
940 if not numrevs: # file doesn't exist at all
937 if not numrevs: # file doesn't exist at all
941 raise
938 raise
942 rev = webutil.changectx(web.repo, req).rev()
939 rev = webutil.changectx(web.repo, req).rev()
943 first = fl.linkrev(0)
940 first = fl.linkrev(0)
944 if rev < first: # current rev is from before file existed
941 if rev < first: # current rev is from before file existed
945 raise
942 raise
946 frev = numrevs - 1
943 frev = numrevs - 1
947 while fl.linkrev(frev) > rev:
944 while fl.linkrev(frev) > rev:
948 frev -= 1
945 frev -= 1
949 fctx = web.repo.filectx(f, fl.linkrev(frev))
946 fctx = web.repo.filectx(f, fl.linkrev(frev))
950
947
951 revcount = web.maxshortchanges
948 revcount = web.maxshortchanges
952 if 'revcount' in req.form:
949 if 'revcount' in req.form:
953 try:
950 try:
954 revcount = int(req.form.get('revcount', [revcount])[0])
951 revcount = int(req.form.get('revcount', [revcount])[0])
955 revcount = max(revcount, 1)
952 revcount = max(revcount, 1)
956 tmpl.defaults['sessionvars']['revcount'] = revcount
953 tmpl.defaults['sessionvars']['revcount'] = revcount
957 except ValueError:
954 except ValueError:
958 pass
955 pass
959
956
960 lessvars = copy.copy(tmpl.defaults['sessionvars'])
957 lessvars = copy.copy(tmpl.defaults['sessionvars'])
961 lessvars['revcount'] = max(revcount / 2, 1)
958 lessvars['revcount'] = max(revcount / 2, 1)
962 morevars = copy.copy(tmpl.defaults['sessionvars'])
959 morevars = copy.copy(tmpl.defaults['sessionvars'])
963 morevars['revcount'] = revcount * 2
960 morevars['revcount'] = revcount * 2
964
961
965 count = fctx.filerev() + 1
962 count = fctx.filerev() + 1
966 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
963 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
967 end = min(count, start + revcount) # last rev on this page
964 end = min(count, start + revcount) # last rev on this page
968 parity = paritygen(web.stripecount, offset=start - end)
965 parity = paritygen(web.stripecount, offset=start - end)
969
966
970 def entries():
967 def entries():
971 l = []
968 l = []
972
969
973 repo = web.repo
970 repo = web.repo
974 revs = fctx.filelog().revs(start, end - 1)
971 revs = fctx.filelog().revs(start, end - 1)
975 for i in revs:
972 for i in revs:
976 iterfctx = fctx.filectx(i)
973 iterfctx = fctx.filectx(i)
977
974
978 l.append(dict(
975 l.append(dict(
979 parity=next(parity),
976 parity=next(parity),
980 filerev=i,
977 filerev=i,
981 file=f,
978 file=f,
982 rename=webutil.renamelink(iterfctx),
979 rename=webutil.renamelink(iterfctx),
983 **webutil.commonentry(repo, iterfctx)))
980 **webutil.commonentry(repo, iterfctx)))
984 for e in reversed(l):
981 for e in reversed(l):
985 yield e
982 yield e
986
983
987 entries = list(entries())
984 entries = list(entries())
988 latestentry = entries[:1]
985 latestentry = entries[:1]
989
986
990 revnav = webutil.filerevnav(web.repo, fctx.path())
987 revnav = webutil.filerevnav(web.repo, fctx.path())
991 nav = revnav.gen(end - 1, revcount, count)
988 nav = revnav.gen(end - 1, revcount, count)
992 return tmpl("filelog",
989 return tmpl("filelog",
993 file=f,
990 file=f,
994 nav=nav,
991 nav=nav,
995 symrev=webutil.symrevorshortnode(req, fctx),
992 symrev=webutil.symrevorshortnode(req, fctx),
996 entries=entries,
993 entries=entries,
997 latestentry=latestentry,
994 latestentry=latestentry,
998 revcount=revcount,
995 revcount=revcount,
999 morevars=morevars,
996 morevars=morevars,
1000 lessvars=lessvars,
997 lessvars=lessvars,
1001 **webutil.commonentry(web.repo, fctx))
998 **webutil.commonentry(web.repo, fctx))
1002
999
1003 @webcommand('archive')
1000 @webcommand('archive')
1004 def archive(web, req, tmpl):
1001 def archive(web, req, tmpl):
1005 """
1002 """
1006 /archive/{revision}.{format}[/{path}]
1003 /archive/{revision}.{format}[/{path}]
1007 -------------------------------------
1004 -------------------------------------
1008
1005
1009 Obtain an archive of repository content.
1006 Obtain an archive of repository content.
1010
1007
1011 The content and type of the archive is defined by a URL path parameter.
1008 The content and type of the archive is defined by a URL path parameter.
1012 ``format`` is the file extension of the archive type to be generated. e.g.
1009 ``format`` is the file extension of the archive type to be generated. e.g.
1013 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1010 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1014 server configuration.
1011 server configuration.
1015
1012
1016 The optional ``path`` URL parameter controls content to include in the
1013 The optional ``path`` URL parameter controls content to include in the
1017 archive. If omitted, every file in the specified revision is present in the
1014 archive. If omitted, every file in the specified revision is present in the
1018 archive. If included, only the specified file or contents of the specified
1015 archive. If included, only the specified file or contents of the specified
1019 directory will be included in the archive.
1016 directory will be included in the archive.
1020
1017
1021 No template is used for this handler. Raw, binary content is generated.
1018 No template is used for this handler. Raw, binary content is generated.
1022 """
1019 """
1023
1020
1024 type_ = req.form.get('type', [None])[0]
1021 type_ = req.form.get('type', [None])[0]
1025 allowed = web.configlist("web", "allow_archive")
1022 allowed = web.configlist("web", "allow_archive")
1026 key = req.form['node'][0]
1023 key = req.form['node'][0]
1027
1024
1028 if type_ not in web.archives:
1025 if type_ not in web.archives:
1029 msg = 'Unsupported archive type: %s' % type_
1026 msg = 'Unsupported archive type: %s' % type_
1030 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1027 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1031
1028
1032 if not ((type_ in allowed or
1029 if not ((type_ in allowed or
1033 web.configbool("web", "allow" + type_, False))):
1030 web.configbool("web", "allow" + type_, False))):
1034 msg = 'Archive type not allowed: %s' % type_
1031 msg = 'Archive type not allowed: %s' % type_
1035 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1032 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1036
1033
1037 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1034 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1038 cnode = web.repo.lookup(key)
1035 cnode = web.repo.lookup(key)
1039 arch_version = key
1036 arch_version = key
1040 if cnode == key or key == 'tip':
1037 if cnode == key or key == 'tip':
1041 arch_version = short(cnode)
1038 arch_version = short(cnode)
1042 name = "%s-%s" % (reponame, arch_version)
1039 name = "%s-%s" % (reponame, arch_version)
1043
1040
1044 ctx = webutil.changectx(web.repo, req)
1041 ctx = webutil.changectx(web.repo, req)
1045 pats = []
1042 pats = []
1046 matchfn = scmutil.match(ctx, [])
1043 matchfn = scmutil.match(ctx, [])
1047 file = req.form.get('file', None)
1044 file = req.form.get('file', None)
1048 if file:
1045 if file:
1049 pats = ['path:' + file[0]]
1046 pats = ['path:' + file[0]]
1050 matchfn = scmutil.match(ctx, pats, default='path')
1047 matchfn = scmutil.match(ctx, pats, default='path')
1051 if pats:
1048 if pats:
1052 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1049 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1053 if not files:
1050 if not files:
1054 raise ErrorResponse(HTTP_NOT_FOUND,
1051 raise ErrorResponse(HTTP_NOT_FOUND,
1055 'file(s) not found: %s' % file[0])
1052 'file(s) not found: %s' % file[0])
1056
1053
1057 mimetype, artype, extension, encoding = web.archivespecs[type_]
1054 mimetype, artype, extension, encoding = web.archivespecs[type_]
1058 headers = [
1055 headers = [
1059 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1056 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1060 ]
1057 ]
1061 if encoding:
1058 if encoding:
1062 headers.append(('Content-Encoding', encoding))
1059 headers.append(('Content-Encoding', encoding))
1063 req.headers.extend(headers)
1060 req.headers.extend(headers)
1064 req.respond(HTTP_OK, mimetype)
1061 req.respond(HTTP_OK, mimetype)
1065
1062
1066 archival.archive(web.repo, req, cnode, artype, prefix=name,
1063 archival.archive(web.repo, req, cnode, artype, prefix=name,
1067 matchfn=matchfn,
1064 matchfn=matchfn,
1068 subrepos=web.configbool("web", "archivesubrepos"))
1065 subrepos=web.configbool("web", "archivesubrepos"))
1069 return []
1066 return []
1070
1067
1071
1068
1072 @webcommand('static')
1069 @webcommand('static')
1073 def static(web, req, tmpl):
1070 def static(web, req, tmpl):
1074 fname = req.form['file'][0]
1071 fname = req.form['file'][0]
1075 # a repo owner may set web.static in .hg/hgrc to get any file
1072 # a repo owner may set web.static in .hg/hgrc to get any file
1076 # readable by the user running the CGI script
1073 # readable by the user running the CGI script
1077 static = web.config("web", "static", None, untrusted=False)
1074 static = web.config("web", "static", None, untrusted=False)
1078 if not static:
1075 if not static:
1079 tp = web.templatepath or templater.templatepaths()
1076 tp = web.templatepath or templater.templatepaths()
1080 if isinstance(tp, str):
1077 if isinstance(tp, str):
1081 tp = [tp]
1078 tp = [tp]
1082 static = [os.path.join(p, 'static') for p in tp]
1079 static = [os.path.join(p, 'static') for p in tp]
1083 staticfile(static, fname, req)
1080 staticfile(static, fname, req)
1084 return []
1081 return []
1085
1082
1086 @webcommand('graph')
1083 @webcommand('graph')
1087 def graph(web, req, tmpl):
1084 def graph(web, req, tmpl):
1088 """
1085 """
1089 /graph[/{revision}]
1086 /graph[/{revision}]
1090 -------------------
1087 -------------------
1091
1088
1092 Show information about the graphical topology of the repository.
1089 Show information about the graphical topology of the repository.
1093
1090
1094 Information rendered by this handler can be used to create visual
1091 Information rendered by this handler can be used to create visual
1095 representations of repository topology.
1092 representations of repository topology.
1096
1093
1097 The ``revision`` URL parameter controls the starting changeset.
1094 The ``revision`` URL parameter controls the starting changeset.
1098
1095
1099 The ``revcount`` query string argument can define the number of changesets
1096 The ``revcount`` query string argument can define the number of changesets
1100 to show information for.
1097 to show information for.
1101
1098
1102 This handler will render the ``graph`` template.
1099 This handler will render the ``graph`` template.
1103 """
1100 """
1104
1101
1105 if 'node' in req.form:
1102 if 'node' in req.form:
1106 ctx = webutil.changectx(web.repo, req)
1103 ctx = webutil.changectx(web.repo, req)
1107 symrev = webutil.symrevorshortnode(req, ctx)
1104 symrev = webutil.symrevorshortnode(req, ctx)
1108 else:
1105 else:
1109 ctx = web.repo['tip']
1106 ctx = web.repo['tip']
1110 symrev = 'tip'
1107 symrev = 'tip'
1111 rev = ctx.rev()
1108 rev = ctx.rev()
1112
1109
1113 bg_height = 39
1110 bg_height = 39
1114 revcount = web.maxshortchanges
1111 revcount = web.maxshortchanges
1115 if 'revcount' in req.form:
1112 if 'revcount' in req.form:
1116 try:
1113 try:
1117 revcount = int(req.form.get('revcount', [revcount])[0])
1114 revcount = int(req.form.get('revcount', [revcount])[0])
1118 revcount = max(revcount, 1)
1115 revcount = max(revcount, 1)
1119 tmpl.defaults['sessionvars']['revcount'] = revcount
1116 tmpl.defaults['sessionvars']['revcount'] = revcount
1120 except ValueError:
1117 except ValueError:
1121 pass
1118 pass
1122
1119
1123 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1120 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1124 lessvars['revcount'] = max(revcount / 2, 1)
1121 lessvars['revcount'] = max(revcount / 2, 1)
1125 morevars = copy.copy(tmpl.defaults['sessionvars'])
1122 morevars = copy.copy(tmpl.defaults['sessionvars'])
1126 morevars['revcount'] = revcount * 2
1123 morevars['revcount'] = revcount * 2
1127
1124
1128 count = len(web.repo)
1125 count = len(web.repo)
1129 pos = rev
1126 pos = rev
1130
1127
1131 uprev = min(max(0, count - 1), rev + revcount)
1128 uprev = min(max(0, count - 1), rev + revcount)
1132 downrev = max(0, rev - revcount)
1129 downrev = max(0, rev - revcount)
1133 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1130 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1134
1131
1135 tree = []
1132 tree = []
1136 if pos != -1:
1133 if pos != -1:
1137 allrevs = web.repo.changelog.revs(pos, 0)
1134 allrevs = web.repo.changelog.revs(pos, 0)
1138 revs = []
1135 revs = []
1139 for i in allrevs:
1136 for i in allrevs:
1140 revs.append(i)
1137 revs.append(i)
1141 if len(revs) >= revcount:
1138 if len(revs) >= revcount:
1142 break
1139 break
1143
1140
1144 # We have to feed a baseset to dagwalker as it is expecting smartset
1141 # We have to feed a baseset to dagwalker as it is expecting smartset
1145 # object. This does not have a big impact on hgweb performance itself
1142 # object. This does not have a big impact on hgweb performance itself
1146 # since hgweb graphing code is not itself lazy yet.
1143 # since hgweb graphing code is not itself lazy yet.
1147 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1144 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1148 # As we said one line above... not lazy.
1145 # As we said one line above... not lazy.
1149 tree = list(graphmod.colored(dag, web.repo))
1146 tree = list(graphmod.colored(dag, web.repo))
1150
1147
1151 def getcolumns(tree):
1148 def getcolumns(tree):
1152 cols = 0
1149 cols = 0
1153 for (id, type, ctx, vtx, edges) in tree:
1150 for (id, type, ctx, vtx, edges) in tree:
1154 if type != graphmod.CHANGESET:
1151 if type != graphmod.CHANGESET:
1155 continue
1152 continue
1156 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1153 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1157 max([edge[1] for edge in edges] or [0]))
1154 max([edge[1] for edge in edges] or [0]))
1158 return cols
1155 return cols
1159
1156
1160 def graphdata(usetuples, encodestr):
1157 def graphdata(usetuples, encodestr):
1161 data = []
1158 data = []
1162
1159
1163 row = 0
1160 row = 0
1164 for (id, type, ctx, vtx, edges) in tree:
1161 for (id, type, ctx, vtx, edges) in tree:
1165 if type != graphmod.CHANGESET:
1162 if type != graphmod.CHANGESET:
1166 continue
1163 continue
1167 node = str(ctx)
1164 node = str(ctx)
1168 age = encodestr(templatefilters.age(ctx.date()))
1165 age = encodestr(templatefilters.age(ctx.date()))
1169 desc = templatefilters.firstline(encodestr(ctx.description()))
1166 desc = templatefilters.firstline(encodestr(ctx.description()))
1170 desc = cgi.escape(templatefilters.nonempty(desc))
1167 desc = cgi.escape(templatefilters.nonempty(desc))
1171 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1168 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1172 branch = cgi.escape(encodestr(ctx.branch()))
1169 branch = cgi.escape(encodestr(ctx.branch()))
1173 try:
1170 try:
1174 branchnode = web.repo.branchtip(branch)
1171 branchnode = web.repo.branchtip(branch)
1175 except error.RepoLookupError:
1172 except error.RepoLookupError:
1176 branchnode = None
1173 branchnode = None
1177 branch = branch, branchnode == ctx.node()
1174 branch = branch, branchnode == ctx.node()
1178
1175
1179 if usetuples:
1176 if usetuples:
1180 data.append((node, vtx, edges, desc, user, age, branch,
1177 data.append((node, vtx, edges, desc, user, age, branch,
1181 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1178 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1182 [cgi.escape(encodestr(x))
1179 [cgi.escape(encodestr(x))
1183 for x in ctx.bookmarks()]))
1180 for x in ctx.bookmarks()]))
1184 else:
1181 else:
1185 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1182 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1186 'color': (edge[2] - 1) % 6 + 1,
1183 'color': (edge[2] - 1) % 6 + 1,
1187 'width': edge[3], 'bcolor': edge[4]}
1184 'width': edge[3], 'bcolor': edge[4]}
1188 for edge in edges]
1185 for edge in edges]
1189
1186
1190 data.append(
1187 data.append(
1191 {'node': node,
1188 {'node': node,
1192 'col': vtx[0],
1189 'col': vtx[0],
1193 'color': (vtx[1] - 1) % 6 + 1,
1190 'color': (vtx[1] - 1) % 6 + 1,
1194 'edges': edgedata,
1191 'edges': edgedata,
1195 'row': row,
1192 'row': row,
1196 'nextrow': row + 1,
1193 'nextrow': row + 1,
1197 'desc': desc,
1194 'desc': desc,
1198 'user': user,
1195 'user': user,
1199 'age': age,
1196 'age': age,
1200 'bookmarks': webutil.nodebookmarksdict(
1197 'bookmarks': webutil.nodebookmarksdict(
1201 web.repo, ctx.node()),
1198 web.repo, ctx.node()),
1202 'branches': webutil.nodebranchdict(web.repo, ctx),
1199 'branches': webutil.nodebranchdict(web.repo, ctx),
1203 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1200 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1204 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1201 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1205
1202
1206 row += 1
1203 row += 1
1207
1204
1208 return data
1205 return data
1209
1206
1210 cols = getcolumns(tree)
1207 cols = getcolumns(tree)
1211 rows = len(tree)
1208 rows = len(tree)
1212 canvasheight = (rows + 1) * bg_height - 27
1209 canvasheight = (rows + 1) * bg_height - 27
1213
1210
1214 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1211 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1215 uprev=uprev,
1212 uprev=uprev,
1216 lessvars=lessvars, morevars=morevars, downrev=downrev,
1213 lessvars=lessvars, morevars=morevars, downrev=downrev,
1217 cols=cols, rows=rows,
1214 cols=cols, rows=rows,
1218 canvaswidth=(cols + 1) * bg_height,
1215 canvaswidth=(cols + 1) * bg_height,
1219 truecanvasheight=rows * bg_height,
1216 truecanvasheight=rows * bg_height,
1220 canvasheight=canvasheight, bg_height=bg_height,
1217 canvasheight=canvasheight, bg_height=bg_height,
1221 # {jsdata} will be passed to |json, so it must be in utf-8
1218 # {jsdata} will be passed to |json, so it must be in utf-8
1222 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1219 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1223 nodes=lambda **x: graphdata(False, str),
1220 nodes=lambda **x: graphdata(False, str),
1224 node=ctx.hex(), changenav=changenav)
1221 node=ctx.hex(), changenav=changenav)
1225
1222
1226 def _getdoc(e):
1223 def _getdoc(e):
1227 doc = e[0].__doc__
1224 doc = e[0].__doc__
1228 if doc:
1225 if doc:
1229 doc = _(doc).partition('\n')[0]
1226 doc = _(doc).partition('\n')[0]
1230 else:
1227 else:
1231 doc = _('(no help text available)')
1228 doc = _('(no help text available)')
1232 return doc
1229 return doc
1233
1230
1234 @webcommand('help')
1231 @webcommand('help')
1235 def help(web, req, tmpl):
1232 def help(web, req, tmpl):
1236 """
1233 """
1237 /help[/{topic}]
1234 /help[/{topic}]
1238 ---------------
1235 ---------------
1239
1236
1240 Render help documentation.
1237 Render help documentation.
1241
1238
1242 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1239 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1243 is defined, that help topic will be rendered. If not, an index of
1240 is defined, that help topic will be rendered. If not, an index of
1244 available help topics will be rendered.
1241 available help topics will be rendered.
1245
1242
1246 The ``help`` template will be rendered when requesting help for a topic.
1243 The ``help`` template will be rendered when requesting help for a topic.
1247 ``helptopics`` will be rendered for the index of help topics.
1244 ``helptopics`` will be rendered for the index of help topics.
1248 """
1245 """
1249 from .. import commands, help as helpmod # avoid cycle
1246 from .. import commands, help as helpmod # avoid cycle
1250
1247
1251 topicname = req.form.get('node', [None])[0]
1248 topicname = req.form.get('node', [None])[0]
1252 if not topicname:
1249 if not topicname:
1253 def topics(**map):
1250 def topics(**map):
1254 for entries, summary, _doc in helpmod.helptable:
1251 for entries, summary, _doc in helpmod.helptable:
1255 yield {'topic': entries[0], 'summary': summary}
1252 yield {'topic': entries[0], 'summary': summary}
1256
1253
1257 early, other = [], []
1254 early, other = [], []
1258 primary = lambda s: s.partition('|')[0]
1255 primary = lambda s: s.partition('|')[0]
1259 for c, e in commands.table.iteritems():
1256 for c, e in commands.table.iteritems():
1260 doc = _getdoc(e)
1257 doc = _getdoc(e)
1261 if 'DEPRECATED' in doc or c.startswith('debug'):
1258 if 'DEPRECATED' in doc or c.startswith('debug'):
1262 continue
1259 continue
1263 cmd = primary(c)
1260 cmd = primary(c)
1264 if cmd.startswith('^'):
1261 if cmd.startswith('^'):
1265 early.append((cmd[1:], doc))
1262 early.append((cmd[1:], doc))
1266 else:
1263 else:
1267 other.append((cmd, doc))
1264 other.append((cmd, doc))
1268
1265
1269 early.sort()
1266 early.sort()
1270 other.sort()
1267 other.sort()
1271
1268
1272 def earlycommands(**map):
1269 def earlycommands(**map):
1273 for c, doc in early:
1270 for c, doc in early:
1274 yield {'topic': c, 'summary': doc}
1271 yield {'topic': c, 'summary': doc}
1275
1272
1276 def othercommands(**map):
1273 def othercommands(**map):
1277 for c, doc in other:
1274 for c, doc in other:
1278 yield {'topic': c, 'summary': doc}
1275 yield {'topic': c, 'summary': doc}
1279
1276
1280 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1277 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1281 othercommands=othercommands, title='Index')
1278 othercommands=othercommands, title='Index')
1282
1279
1283 # Render an index of sub-topics.
1280 # Render an index of sub-topics.
1284 if topicname in helpmod.subtopics:
1281 if topicname in helpmod.subtopics:
1285 topics = []
1282 topics = []
1286 for entries, summary, _doc in helpmod.subtopics[topicname]:
1283 for entries, summary, _doc in helpmod.subtopics[topicname]:
1287 topics.append({
1284 topics.append({
1288 'topic': '%s.%s' % (topicname, entries[0]),
1285 'topic': '%s.%s' % (topicname, entries[0]),
1289 'basename': entries[0],
1286 'basename': entries[0],
1290 'summary': summary,
1287 'summary': summary,
1291 })
1288 })
1292
1289
1293 return tmpl('helptopics', topics=topics, title=topicname,
1290 return tmpl('helptopics', topics=topics, title=topicname,
1294 subindex=True)
1291 subindex=True)
1295
1292
1296 u = webutil.wsgiui()
1293 u = webutil.wsgiui()
1297 u.verbose = True
1294 u.verbose = True
1298
1295
1299 # Render a page from a sub-topic.
1296 # Render a page from a sub-topic.
1300 if '.' in topicname:
1297 if '.' in topicname:
1301 # TODO implement support for rendering sections, like
1298 # TODO implement support for rendering sections, like
1302 # `hg help` works.
1299 # `hg help` works.
1303 topic, subtopic = topicname.split('.', 1)
1300 topic, subtopic = topicname.split('.', 1)
1304 if topic not in helpmod.subtopics:
1301 if topic not in helpmod.subtopics:
1305 raise ErrorResponse(HTTP_NOT_FOUND)
1302 raise ErrorResponse(HTTP_NOT_FOUND)
1306 else:
1303 else:
1307 topic = topicname
1304 topic = topicname
1308 subtopic = None
1305 subtopic = None
1309
1306
1310 try:
1307 try:
1311 doc = helpmod.help_(u, topic, subtopic=subtopic)
1308 doc = helpmod.help_(u, topic, subtopic=subtopic)
1312 except error.UnknownCommand:
1309 except error.UnknownCommand:
1313 raise ErrorResponse(HTTP_NOT_FOUND)
1310 raise ErrorResponse(HTTP_NOT_FOUND)
1314 return tmpl('help', topic=topicname, doc=doc)
1311 return tmpl('help', topic=topicname, doc=doc)
1315
1312
1316 # tell hggettext to extract docstrings from these functions:
1313 # tell hggettext to extract docstrings from these functions:
1317 i18nfunctions = commands.values()
1314 i18nfunctions = commands.values()
@@ -1,608 +1,613 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_NOT_FOUND,
21 HTTP_NOT_FOUND,
22 paritygen,
22 paritygen,
23 )
23 )
24
24
25 from .. import (
25 from .. import (
26 context,
26 context,
27 error,
27 error,
28 match,
28 match,
29 patch,
29 patch,
30 pathutil,
30 pathutil,
31 templatefilters,
31 templatefilters,
32 ui as uimod,
32 ui as uimod,
33 util,
33 util,
34 )
34 )
35
35
36 def up(p):
36 def up(p):
37 if p[0] != "/":
37 if p[0] != "/":
38 p = "/" + p
38 p = "/" + p
39 if p[-1] == "/":
39 if p[-1] == "/":
40 p = p[:-1]
40 p = p[:-1]
41 up = os.path.dirname(p)
41 up = os.path.dirname(p)
42 if up == "/":
42 if up == "/":
43 return "/"
43 return "/"
44 return up + "/"
44 return up + "/"
45
45
46 def _navseq(step, firststep=None):
46 def _navseq(step, firststep=None):
47 if firststep:
47 if firststep:
48 yield firststep
48 yield firststep
49 if firststep >= 20 and firststep <= 40:
49 if firststep >= 20 and firststep <= 40:
50 firststep = 50
50 firststep = 50
51 yield firststep
51 yield firststep
52 assert step > 0
52 assert step > 0
53 assert firststep > 0
53 assert firststep > 0
54 while step <= firststep:
54 while step <= firststep:
55 step *= 10
55 step *= 10
56 while True:
56 while True:
57 yield 1 * step
57 yield 1 * step
58 yield 3 * step
58 yield 3 * step
59 step *= 10
59 step *= 10
60
60
61 class revnav(object):
61 class revnav(object):
62
62
63 def __init__(self, repo):
63 def __init__(self, repo):
64 """Navigation generation object
64 """Navigation generation object
65
65
66 :repo: repo object we generate nav for
66 :repo: repo object we generate nav for
67 """
67 """
68 # used for hex generation
68 # used for hex generation
69 self._revlog = repo.changelog
69 self._revlog = repo.changelog
70
70
71 def __nonzero__(self):
71 def __nonzero__(self):
72 """return True if any revision to navigate over"""
72 """return True if any revision to navigate over"""
73 return self._first() is not None
73 return self._first() is not None
74
74
75 def _first(self):
75 def _first(self):
76 """return the minimum non-filtered changeset or None"""
76 """return the minimum non-filtered changeset or None"""
77 try:
77 try:
78 return next(iter(self._revlog))
78 return next(iter(self._revlog))
79 except StopIteration:
79 except StopIteration:
80 return None
80 return None
81
81
82 def hex(self, rev):
82 def hex(self, rev):
83 return hex(self._revlog.node(rev))
83 return hex(self._revlog.node(rev))
84
84
85 def gen(self, pos, pagelen, limit):
85 def gen(self, pos, pagelen, limit):
86 """computes label and revision id for navigation link
86 """computes label and revision id for navigation link
87
87
88 :pos: is the revision relative to which we generate navigation.
88 :pos: is the revision relative to which we generate navigation.
89 :pagelen: the size of each navigation page
89 :pagelen: the size of each navigation page
90 :limit: how far shall we link
90 :limit: how far shall we link
91
91
92 The return is:
92 The return is:
93 - a single element tuple
93 - a single element tuple
94 - containing a dictionary with a `before` and `after` key
94 - containing a dictionary with a `before` and `after` key
95 - values are generator functions taking arbitrary number of kwargs
95 - values are generator functions taking arbitrary number of kwargs
96 - yield items are dictionaries with `label` and `node` keys
96 - yield items are dictionaries with `label` and `node` keys
97 """
97 """
98 if not self:
98 if not self:
99 # empty repo
99 # empty repo
100 return ({'before': (), 'after': ()},)
100 return ({'before': (), 'after': ()},)
101
101
102 targets = []
102 targets = []
103 for f in _navseq(1, pagelen):
103 for f in _navseq(1, pagelen):
104 if f > limit:
104 if f > limit:
105 break
105 break
106 targets.append(pos + f)
106 targets.append(pos + f)
107 targets.append(pos - f)
107 targets.append(pos - f)
108 targets.sort()
108 targets.sort()
109
109
110 first = self._first()
110 first = self._first()
111 navbefore = [("(%i)" % first, self.hex(first))]
111 navbefore = [("(%i)" % first, self.hex(first))]
112 navafter = []
112 navafter = []
113 for rev in targets:
113 for rev in targets:
114 if rev not in self._revlog:
114 if rev not in self._revlog:
115 continue
115 continue
116 if pos < rev < limit:
116 if pos < rev < limit:
117 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
117 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
118 if 0 < rev < pos:
118 if 0 < rev < pos:
119 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
119 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
120
120
121
121
122 navafter.append(("tip", "tip"))
122 navafter.append(("tip", "tip"))
123
123
124 data = lambda i: {"label": i[0], "node": i[1]}
124 data = lambda i: {"label": i[0], "node": i[1]}
125 return ({'before': lambda **map: (data(i) for i in navbefore),
125 return ({'before': lambda **map: (data(i) for i in navbefore),
126 'after': lambda **map: (data(i) for i in navafter)},)
126 'after': lambda **map: (data(i) for i in navafter)},)
127
127
128 class filerevnav(revnav):
128 class filerevnav(revnav):
129
129
130 def __init__(self, repo, path):
130 def __init__(self, repo, path):
131 """Navigation generation object
131 """Navigation generation object
132
132
133 :repo: repo object we generate nav for
133 :repo: repo object we generate nav for
134 :path: path of the file we generate nav for
134 :path: path of the file we generate nav for
135 """
135 """
136 # used for iteration
136 # used for iteration
137 self._changelog = repo.unfiltered().changelog
137 self._changelog = repo.unfiltered().changelog
138 # used for hex generation
138 # used for hex generation
139 self._revlog = repo.file(path)
139 self._revlog = repo.file(path)
140
140
141 def hex(self, rev):
141 def hex(self, rev):
142 return hex(self._changelog.node(self._revlog.linkrev(rev)))
142 return hex(self._changelog.node(self._revlog.linkrev(rev)))
143
143
144 class _siblings(object):
144 class _siblings(object):
145 def __init__(self, siblings=[], hiderev=None):
145 def __init__(self, siblings=[], hiderev=None):
146 self.siblings = [s for s in siblings if s.node() != nullid]
146 self.siblings = [s for s in siblings if s.node() != nullid]
147 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
147 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
148 self.siblings = []
148 self.siblings = []
149
149
150 def __iter__(self):
150 def __iter__(self):
151 for s in self.siblings:
151 for s in self.siblings:
152 d = {
152 d = {
153 'node': s.hex(),
153 'node': s.hex(),
154 'rev': s.rev(),
154 'rev': s.rev(),
155 'user': s.user(),
155 'user': s.user(),
156 'date': s.date(),
156 'date': s.date(),
157 'description': s.description(),
157 'description': s.description(),
158 'branch': s.branch(),
158 'branch': s.branch(),
159 }
159 }
160 if util.safehasattr(s, 'path'):
160 if util.safehasattr(s, 'path'):
161 d['file'] = s.path()
161 d['file'] = s.path()
162 yield d
162 yield d
163
163
164 def __len__(self):
164 def __len__(self):
165 return len(self.siblings)
165 return len(self.siblings)
166
166
167 def annotate(fctx, ui):
168 diffopts = patch.difffeatureopts(ui, untrusted=True,
169 section='annotate', whitespace=True)
170 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
171
167 def parents(ctx, hide=None):
172 def parents(ctx, hide=None):
168 if isinstance(ctx, context.basefilectx):
173 if isinstance(ctx, context.basefilectx):
169 introrev = ctx.introrev()
174 introrev = ctx.introrev()
170 if ctx.changectx().rev() != introrev:
175 if ctx.changectx().rev() != introrev:
171 return _siblings([ctx.repo()[introrev]], hide)
176 return _siblings([ctx.repo()[introrev]], hide)
172 return _siblings(ctx.parents(), hide)
177 return _siblings(ctx.parents(), hide)
173
178
174 def children(ctx, hide=None):
179 def children(ctx, hide=None):
175 return _siblings(ctx.children(), hide)
180 return _siblings(ctx.children(), hide)
176
181
177 def renamelink(fctx):
182 def renamelink(fctx):
178 r = fctx.renamed()
183 r = fctx.renamed()
179 if r:
184 if r:
180 return [{'file': r[0], 'node': hex(r[1])}]
185 return [{'file': r[0], 'node': hex(r[1])}]
181 return []
186 return []
182
187
183 def nodetagsdict(repo, node):
188 def nodetagsdict(repo, node):
184 return [{"name": i} for i in repo.nodetags(node)]
189 return [{"name": i} for i in repo.nodetags(node)]
185
190
186 def nodebookmarksdict(repo, node):
191 def nodebookmarksdict(repo, node):
187 return [{"name": i} for i in repo.nodebookmarks(node)]
192 return [{"name": i} for i in repo.nodebookmarks(node)]
188
193
189 def nodebranchdict(repo, ctx):
194 def nodebranchdict(repo, ctx):
190 branches = []
195 branches = []
191 branch = ctx.branch()
196 branch = ctx.branch()
192 # If this is an empty repo, ctx.node() == nullid,
197 # If this is an empty repo, ctx.node() == nullid,
193 # ctx.branch() == 'default'.
198 # ctx.branch() == 'default'.
194 try:
199 try:
195 branchnode = repo.branchtip(branch)
200 branchnode = repo.branchtip(branch)
196 except error.RepoLookupError:
201 except error.RepoLookupError:
197 branchnode = None
202 branchnode = None
198 if branchnode == ctx.node():
203 if branchnode == ctx.node():
199 branches.append({"name": branch})
204 branches.append({"name": branch})
200 return branches
205 return branches
201
206
202 def nodeinbranch(repo, ctx):
207 def nodeinbranch(repo, ctx):
203 branches = []
208 branches = []
204 branch = ctx.branch()
209 branch = ctx.branch()
205 try:
210 try:
206 branchnode = repo.branchtip(branch)
211 branchnode = repo.branchtip(branch)
207 except error.RepoLookupError:
212 except error.RepoLookupError:
208 branchnode = None
213 branchnode = None
209 if branch != 'default' and branchnode != ctx.node():
214 if branch != 'default' and branchnode != ctx.node():
210 branches.append({"name": branch})
215 branches.append({"name": branch})
211 return branches
216 return branches
212
217
213 def nodebranchnodefault(ctx):
218 def nodebranchnodefault(ctx):
214 branches = []
219 branches = []
215 branch = ctx.branch()
220 branch = ctx.branch()
216 if branch != 'default':
221 if branch != 'default':
217 branches.append({"name": branch})
222 branches.append({"name": branch})
218 return branches
223 return branches
219
224
220 def showtag(repo, tmpl, t1, node=nullid, **args):
225 def showtag(repo, tmpl, t1, node=nullid, **args):
221 for t in repo.nodetags(node):
226 for t in repo.nodetags(node):
222 yield tmpl(t1, tag=t, **args)
227 yield tmpl(t1, tag=t, **args)
223
228
224 def showbookmark(repo, tmpl, t1, node=nullid, **args):
229 def showbookmark(repo, tmpl, t1, node=nullid, **args):
225 for t in repo.nodebookmarks(node):
230 for t in repo.nodebookmarks(node):
226 yield tmpl(t1, bookmark=t, **args)
231 yield tmpl(t1, bookmark=t, **args)
227
232
228 def branchentries(repo, stripecount, limit=0):
233 def branchentries(repo, stripecount, limit=0):
229 tips = []
234 tips = []
230 heads = repo.heads()
235 heads = repo.heads()
231 parity = paritygen(stripecount)
236 parity = paritygen(stripecount)
232 sortkey = lambda item: (not item[1], item[0].rev())
237 sortkey = lambda item: (not item[1], item[0].rev())
233
238
234 def entries(**map):
239 def entries(**map):
235 count = 0
240 count = 0
236 if not tips:
241 if not tips:
237 for tag, hs, tip, closed in repo.branchmap().iterbranches():
242 for tag, hs, tip, closed in repo.branchmap().iterbranches():
238 tips.append((repo[tip], closed))
243 tips.append((repo[tip], closed))
239 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
244 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
240 if limit > 0 and count >= limit:
245 if limit > 0 and count >= limit:
241 return
246 return
242 count += 1
247 count += 1
243 if closed:
248 if closed:
244 status = 'closed'
249 status = 'closed'
245 elif ctx.node() not in heads:
250 elif ctx.node() not in heads:
246 status = 'inactive'
251 status = 'inactive'
247 else:
252 else:
248 status = 'open'
253 status = 'open'
249 yield {
254 yield {
250 'parity': next(parity),
255 'parity': next(parity),
251 'branch': ctx.branch(),
256 'branch': ctx.branch(),
252 'status': status,
257 'status': status,
253 'node': ctx.hex(),
258 'node': ctx.hex(),
254 'date': ctx.date()
259 'date': ctx.date()
255 }
260 }
256
261
257 return entries
262 return entries
258
263
259 def cleanpath(repo, path):
264 def cleanpath(repo, path):
260 path = path.lstrip('/')
265 path = path.lstrip('/')
261 return pathutil.canonpath(repo.root, '', path)
266 return pathutil.canonpath(repo.root, '', path)
262
267
263 def changeidctx(repo, changeid):
268 def changeidctx(repo, changeid):
264 try:
269 try:
265 ctx = repo[changeid]
270 ctx = repo[changeid]
266 except error.RepoError:
271 except error.RepoError:
267 man = repo.manifest
272 man = repo.manifest
268 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
273 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
269
274
270 return ctx
275 return ctx
271
276
272 def changectx(repo, req):
277 def changectx(repo, req):
273 changeid = "tip"
278 changeid = "tip"
274 if 'node' in req.form:
279 if 'node' in req.form:
275 changeid = req.form['node'][0]
280 changeid = req.form['node'][0]
276 ipos = changeid.find(':')
281 ipos = changeid.find(':')
277 if ipos != -1:
282 if ipos != -1:
278 changeid = changeid[(ipos + 1):]
283 changeid = changeid[(ipos + 1):]
279 elif 'manifest' in req.form:
284 elif 'manifest' in req.form:
280 changeid = req.form['manifest'][0]
285 changeid = req.form['manifest'][0]
281
286
282 return changeidctx(repo, changeid)
287 return changeidctx(repo, changeid)
283
288
284 def basechangectx(repo, req):
289 def basechangectx(repo, req):
285 if 'node' in req.form:
290 if 'node' in req.form:
286 changeid = req.form['node'][0]
291 changeid = req.form['node'][0]
287 ipos = changeid.find(':')
292 ipos = changeid.find(':')
288 if ipos != -1:
293 if ipos != -1:
289 changeid = changeid[:ipos]
294 changeid = changeid[:ipos]
290 return changeidctx(repo, changeid)
295 return changeidctx(repo, changeid)
291
296
292 return None
297 return None
293
298
294 def filectx(repo, req):
299 def filectx(repo, req):
295 if 'file' not in req.form:
300 if 'file' not in req.form:
296 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
301 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
297 path = cleanpath(repo, req.form['file'][0])
302 path = cleanpath(repo, req.form['file'][0])
298 if 'node' in req.form:
303 if 'node' in req.form:
299 changeid = req.form['node'][0]
304 changeid = req.form['node'][0]
300 elif 'filenode' in req.form:
305 elif 'filenode' in req.form:
301 changeid = req.form['filenode'][0]
306 changeid = req.form['filenode'][0]
302 else:
307 else:
303 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
308 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
304 try:
309 try:
305 fctx = repo[changeid][path]
310 fctx = repo[changeid][path]
306 except error.RepoError:
311 except error.RepoError:
307 fctx = repo.filectx(path, fileid=changeid)
312 fctx = repo.filectx(path, fileid=changeid)
308
313
309 return fctx
314 return fctx
310
315
311 def commonentry(repo, ctx):
316 def commonentry(repo, ctx):
312 node = ctx.node()
317 node = ctx.node()
313 return {
318 return {
314 'rev': ctx.rev(),
319 'rev': ctx.rev(),
315 'node': hex(node),
320 'node': hex(node),
316 'author': ctx.user(),
321 'author': ctx.user(),
317 'desc': ctx.description(),
322 'desc': ctx.description(),
318 'date': ctx.date(),
323 'date': ctx.date(),
319 'extra': ctx.extra(),
324 'extra': ctx.extra(),
320 'phase': ctx.phasestr(),
325 'phase': ctx.phasestr(),
321 'branch': nodebranchnodefault(ctx),
326 'branch': nodebranchnodefault(ctx),
322 'inbranch': nodeinbranch(repo, ctx),
327 'inbranch': nodeinbranch(repo, ctx),
323 'branches': nodebranchdict(repo, ctx),
328 'branches': nodebranchdict(repo, ctx),
324 'tags': nodetagsdict(repo, node),
329 'tags': nodetagsdict(repo, node),
325 'bookmarks': nodebookmarksdict(repo, node),
330 'bookmarks': nodebookmarksdict(repo, node),
326 'parent': lambda **x: parents(ctx),
331 'parent': lambda **x: parents(ctx),
327 'child': lambda **x: children(ctx),
332 'child': lambda **x: children(ctx),
328 }
333 }
329
334
330 def changelistentry(web, ctx, tmpl):
335 def changelistentry(web, ctx, tmpl):
331 '''Obtain a dictionary to be used for entries in a changelist.
336 '''Obtain a dictionary to be used for entries in a changelist.
332
337
333 This function is called when producing items for the "entries" list passed
338 This function is called when producing items for the "entries" list passed
334 to the "shortlog" and "changelog" templates.
339 to the "shortlog" and "changelog" templates.
335 '''
340 '''
336 repo = web.repo
341 repo = web.repo
337 rev = ctx.rev()
342 rev = ctx.rev()
338 n = ctx.node()
343 n = ctx.node()
339 showtags = showtag(repo, tmpl, 'changelogtag', n)
344 showtags = showtag(repo, tmpl, 'changelogtag', n)
340 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
345 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
341
346
342 entry = commonentry(repo, ctx)
347 entry = commonentry(repo, ctx)
343 entry.update(
348 entry.update(
344 allparents=lambda **x: parents(ctx),
349 allparents=lambda **x: parents(ctx),
345 parent=lambda **x: parents(ctx, rev - 1),
350 parent=lambda **x: parents(ctx, rev - 1),
346 child=lambda **x: children(ctx, rev + 1),
351 child=lambda **x: children(ctx, rev + 1),
347 changelogtag=showtags,
352 changelogtag=showtags,
348 files=files,
353 files=files,
349 )
354 )
350 return entry
355 return entry
351
356
352 def symrevorshortnode(req, ctx):
357 def symrevorshortnode(req, ctx):
353 if 'node' in req.form:
358 if 'node' in req.form:
354 return templatefilters.revescape(req.form['node'][0])
359 return templatefilters.revescape(req.form['node'][0])
355 else:
360 else:
356 return short(ctx.node())
361 return short(ctx.node())
357
362
358 def changesetentry(web, req, tmpl, ctx):
363 def changesetentry(web, req, tmpl, ctx):
359 '''Obtain a dictionary to be used to render the "changeset" template.'''
364 '''Obtain a dictionary to be used to render the "changeset" template.'''
360
365
361 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
366 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
362 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
367 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
363 ctx.node())
368 ctx.node())
364 showbranch = nodebranchnodefault(ctx)
369 showbranch = nodebranchnodefault(ctx)
365
370
366 files = []
371 files = []
367 parity = paritygen(web.stripecount)
372 parity = paritygen(web.stripecount)
368 for blockno, f in enumerate(ctx.files()):
373 for blockno, f in enumerate(ctx.files()):
369 template = f in ctx and 'filenodelink' or 'filenolink'
374 template = f in ctx and 'filenodelink' or 'filenolink'
370 files.append(tmpl(template,
375 files.append(tmpl(template,
371 node=ctx.hex(), file=f, blockno=blockno + 1,
376 node=ctx.hex(), file=f, blockno=blockno + 1,
372 parity=next(parity)))
377 parity=next(parity)))
373
378
374 basectx = basechangectx(web.repo, req)
379 basectx = basechangectx(web.repo, req)
375 if basectx is None:
380 if basectx is None:
376 basectx = ctx.p1()
381 basectx = ctx.p1()
377
382
378 style = web.config('web', 'style', 'paper')
383 style = web.config('web', 'style', 'paper')
379 if 'style' in req.form:
384 if 'style' in req.form:
380 style = req.form['style'][0]
385 style = req.form['style'][0]
381
386
382 parity = paritygen(web.stripecount)
387 parity = paritygen(web.stripecount)
383 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
388 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
384
389
385 parity = paritygen(web.stripecount)
390 parity = paritygen(web.stripecount)
386 diffstatsgen = diffstatgen(ctx, basectx)
391 diffstatsgen = diffstatgen(ctx, basectx)
387 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
392 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
388
393
389 return dict(
394 return dict(
390 diff=diff,
395 diff=diff,
391 symrev=symrevorshortnode(req, ctx),
396 symrev=symrevorshortnode(req, ctx),
392 basenode=basectx.hex(),
397 basenode=basectx.hex(),
393 changesettag=showtags,
398 changesettag=showtags,
394 changesetbookmark=showbookmarks,
399 changesetbookmark=showbookmarks,
395 changesetbranch=showbranch,
400 changesetbranch=showbranch,
396 files=files,
401 files=files,
397 diffsummary=lambda **x: diffsummary(diffstatsgen),
402 diffsummary=lambda **x: diffsummary(diffstatsgen),
398 diffstat=diffstats,
403 diffstat=diffstats,
399 archives=web.archivelist(ctx.hex()),
404 archives=web.archivelist(ctx.hex()),
400 **commonentry(web.repo, ctx))
405 **commonentry(web.repo, ctx))
401
406
402 def listfilediffs(tmpl, files, node, max):
407 def listfilediffs(tmpl, files, node, max):
403 for f in files[:max]:
408 for f in files[:max]:
404 yield tmpl('filedifflink', node=hex(node), file=f)
409 yield tmpl('filedifflink', node=hex(node), file=f)
405 if len(files) > max:
410 if len(files) > max:
406 yield tmpl('fileellipses')
411 yield tmpl('fileellipses')
407
412
408 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
413 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
409
414
410 def countgen():
415 def countgen():
411 start = 1
416 start = 1
412 while True:
417 while True:
413 yield start
418 yield start
414 start += 1
419 start += 1
415
420
416 blockcount = countgen()
421 blockcount = countgen()
417 def prettyprintlines(diff, blockno):
422 def prettyprintlines(diff, blockno):
418 for lineno, l in enumerate(diff.splitlines(True)):
423 for lineno, l in enumerate(diff.splitlines(True)):
419 difflineno = "%d.%d" % (blockno, lineno + 1)
424 difflineno = "%d.%d" % (blockno, lineno + 1)
420 if l.startswith('+'):
425 if l.startswith('+'):
421 ltype = "difflineplus"
426 ltype = "difflineplus"
422 elif l.startswith('-'):
427 elif l.startswith('-'):
423 ltype = "difflineminus"
428 ltype = "difflineminus"
424 elif l.startswith('@'):
429 elif l.startswith('@'):
425 ltype = "difflineat"
430 ltype = "difflineat"
426 else:
431 else:
427 ltype = "diffline"
432 ltype = "diffline"
428 yield tmpl(ltype,
433 yield tmpl(ltype,
429 line=l,
434 line=l,
430 lineno=lineno + 1,
435 lineno=lineno + 1,
431 lineid="l%s" % difflineno,
436 lineid="l%s" % difflineno,
432 linenumber="% 8s" % difflineno)
437 linenumber="% 8s" % difflineno)
433
438
434 if files:
439 if files:
435 m = match.exact(repo.root, repo.getcwd(), files)
440 m = match.exact(repo.root, repo.getcwd(), files)
436 else:
441 else:
437 m = match.always(repo.root, repo.getcwd())
442 m = match.always(repo.root, repo.getcwd())
438
443
439 diffopts = patch.diffopts(repo.ui, untrusted=True)
444 diffopts = patch.diffopts(repo.ui, untrusted=True)
440 if basectx is None:
445 if basectx is None:
441 parents = ctx.parents()
446 parents = ctx.parents()
442 if parents:
447 if parents:
443 node1 = parents[0].node()
448 node1 = parents[0].node()
444 else:
449 else:
445 node1 = nullid
450 node1 = nullid
446 else:
451 else:
447 node1 = basectx.node()
452 node1 = basectx.node()
448 node2 = ctx.node()
453 node2 = ctx.node()
449
454
450 block = []
455 block = []
451 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
456 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
452 if chunk.startswith('diff') and block:
457 if chunk.startswith('diff') and block:
453 blockno = next(blockcount)
458 blockno = next(blockcount)
454 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
459 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
455 lines=prettyprintlines(''.join(block), blockno))
460 lines=prettyprintlines(''.join(block), blockno))
456 block = []
461 block = []
457 if chunk.startswith('diff') and style != 'raw':
462 if chunk.startswith('diff') and style != 'raw':
458 chunk = ''.join(chunk.splitlines(True)[1:])
463 chunk = ''.join(chunk.splitlines(True)[1:])
459 block.append(chunk)
464 block.append(chunk)
460 blockno = next(blockcount)
465 blockno = next(blockcount)
461 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
466 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
462 lines=prettyprintlines(''.join(block), blockno))
467 lines=prettyprintlines(''.join(block), blockno))
463
468
464 def compare(tmpl, context, leftlines, rightlines):
469 def compare(tmpl, context, leftlines, rightlines):
465 '''Generator function that provides side-by-side comparison data.'''
470 '''Generator function that provides side-by-side comparison data.'''
466
471
467 def compline(type, leftlineno, leftline, rightlineno, rightline):
472 def compline(type, leftlineno, leftline, rightlineno, rightline):
468 lineid = leftlineno and ("l%s" % leftlineno) or ''
473 lineid = leftlineno and ("l%s" % leftlineno) or ''
469 lineid += rightlineno and ("r%s" % rightlineno) or ''
474 lineid += rightlineno and ("r%s" % rightlineno) or ''
470 return tmpl('comparisonline',
475 return tmpl('comparisonline',
471 type=type,
476 type=type,
472 lineid=lineid,
477 lineid=lineid,
473 leftlineno=leftlineno,
478 leftlineno=leftlineno,
474 leftlinenumber="% 6s" % (leftlineno or ''),
479 leftlinenumber="% 6s" % (leftlineno or ''),
475 leftline=leftline or '',
480 leftline=leftline or '',
476 rightlineno=rightlineno,
481 rightlineno=rightlineno,
477 rightlinenumber="% 6s" % (rightlineno or ''),
482 rightlinenumber="% 6s" % (rightlineno or ''),
478 rightline=rightline or '')
483 rightline=rightline or '')
479
484
480 def getblock(opcodes):
485 def getblock(opcodes):
481 for type, llo, lhi, rlo, rhi in opcodes:
486 for type, llo, lhi, rlo, rhi in opcodes:
482 len1 = lhi - llo
487 len1 = lhi - llo
483 len2 = rhi - rlo
488 len2 = rhi - rlo
484 count = min(len1, len2)
489 count = min(len1, len2)
485 for i in xrange(count):
490 for i in xrange(count):
486 yield compline(type=type,
491 yield compline(type=type,
487 leftlineno=llo + i + 1,
492 leftlineno=llo + i + 1,
488 leftline=leftlines[llo + i],
493 leftline=leftlines[llo + i],
489 rightlineno=rlo + i + 1,
494 rightlineno=rlo + i + 1,
490 rightline=rightlines[rlo + i])
495 rightline=rightlines[rlo + i])
491 if len1 > len2:
496 if len1 > len2:
492 for i in xrange(llo + count, lhi):
497 for i in xrange(llo + count, lhi):
493 yield compline(type=type,
498 yield compline(type=type,
494 leftlineno=i + 1,
499 leftlineno=i + 1,
495 leftline=leftlines[i],
500 leftline=leftlines[i],
496 rightlineno=None,
501 rightlineno=None,
497 rightline=None)
502 rightline=None)
498 elif len2 > len1:
503 elif len2 > len1:
499 for i in xrange(rlo + count, rhi):
504 for i in xrange(rlo + count, rhi):
500 yield compline(type=type,
505 yield compline(type=type,
501 leftlineno=None,
506 leftlineno=None,
502 leftline=None,
507 leftline=None,
503 rightlineno=i + 1,
508 rightlineno=i + 1,
504 rightline=rightlines[i])
509 rightline=rightlines[i])
505
510
506 s = difflib.SequenceMatcher(None, leftlines, rightlines)
511 s = difflib.SequenceMatcher(None, leftlines, rightlines)
507 if context < 0:
512 if context < 0:
508 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
513 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
509 else:
514 else:
510 for oc in s.get_grouped_opcodes(n=context):
515 for oc in s.get_grouped_opcodes(n=context):
511 yield tmpl('comparisonblock', lines=getblock(oc))
516 yield tmpl('comparisonblock', lines=getblock(oc))
512
517
513 def diffstatgen(ctx, basectx):
518 def diffstatgen(ctx, basectx):
514 '''Generator function that provides the diffstat data.'''
519 '''Generator function that provides the diffstat data.'''
515
520
516 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
521 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
517 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
522 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
518 while True:
523 while True:
519 yield stats, maxname, maxtotal, addtotal, removetotal, binary
524 yield stats, maxname, maxtotal, addtotal, removetotal, binary
520
525
521 def diffsummary(statgen):
526 def diffsummary(statgen):
522 '''Return a short summary of the diff.'''
527 '''Return a short summary of the diff.'''
523
528
524 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
529 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
525 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
530 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
526 len(stats), addtotal, removetotal)
531 len(stats), addtotal, removetotal)
527
532
528 def diffstat(tmpl, ctx, statgen, parity):
533 def diffstat(tmpl, ctx, statgen, parity):
529 '''Return a diffstat template for each file in the diff.'''
534 '''Return a diffstat template for each file in the diff.'''
530
535
531 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
536 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
532 files = ctx.files()
537 files = ctx.files()
533
538
534 def pct(i):
539 def pct(i):
535 if maxtotal == 0:
540 if maxtotal == 0:
536 return 0
541 return 0
537 return (float(i) / maxtotal) * 100
542 return (float(i) / maxtotal) * 100
538
543
539 fileno = 0
544 fileno = 0
540 for filename, adds, removes, isbinary in stats:
545 for filename, adds, removes, isbinary in stats:
541 template = filename in files and 'diffstatlink' or 'diffstatnolink'
546 template = filename in files and 'diffstatlink' or 'diffstatnolink'
542 total = adds + removes
547 total = adds + removes
543 fileno += 1
548 fileno += 1
544 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
549 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
545 total=total, addpct=pct(adds), removepct=pct(removes),
550 total=total, addpct=pct(adds), removepct=pct(removes),
546 parity=next(parity))
551 parity=next(parity))
547
552
548 class sessionvars(object):
553 class sessionvars(object):
549 def __init__(self, vars, start='?'):
554 def __init__(self, vars, start='?'):
550 self.start = start
555 self.start = start
551 self.vars = vars
556 self.vars = vars
552 def __getitem__(self, key):
557 def __getitem__(self, key):
553 return self.vars[key]
558 return self.vars[key]
554 def __setitem__(self, key, value):
559 def __setitem__(self, key, value):
555 self.vars[key] = value
560 self.vars[key] = value
556 def __copy__(self):
561 def __copy__(self):
557 return sessionvars(copy.copy(self.vars), self.start)
562 return sessionvars(copy.copy(self.vars), self.start)
558 def __iter__(self):
563 def __iter__(self):
559 separator = self.start
564 separator = self.start
560 for key, value in sorted(self.vars.iteritems()):
565 for key, value in sorted(self.vars.iteritems()):
561 yield {'name': key, 'value': str(value), 'separator': separator}
566 yield {'name': key, 'value': str(value), 'separator': separator}
562 separator = '&'
567 separator = '&'
563
568
564 class wsgiui(uimod.ui):
569 class wsgiui(uimod.ui):
565 # default termwidth breaks under mod_wsgi
570 # default termwidth breaks under mod_wsgi
566 def termwidth(self):
571 def termwidth(self):
567 return 80
572 return 80
568
573
569 def getwebsubs(repo):
574 def getwebsubs(repo):
570 websubtable = []
575 websubtable = []
571 websubdefs = repo.ui.configitems('websub')
576 websubdefs = repo.ui.configitems('websub')
572 # we must maintain interhg backwards compatibility
577 # we must maintain interhg backwards compatibility
573 websubdefs += repo.ui.configitems('interhg')
578 websubdefs += repo.ui.configitems('interhg')
574 for key, pattern in websubdefs:
579 for key, pattern in websubdefs:
575 # grab the delimiter from the character after the "s"
580 # grab the delimiter from the character after the "s"
576 unesc = pattern[1]
581 unesc = pattern[1]
577 delim = re.escape(unesc)
582 delim = re.escape(unesc)
578
583
579 # identify portions of the pattern, taking care to avoid escaped
584 # identify portions of the pattern, taking care to avoid escaped
580 # delimiters. the replace format and flags are optional, but
585 # delimiters. the replace format and flags are optional, but
581 # delimiters are required.
586 # delimiters are required.
582 match = re.match(
587 match = re.match(
583 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
588 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
584 % (delim, delim, delim), pattern)
589 % (delim, delim, delim), pattern)
585 if not match:
590 if not match:
586 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
591 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
587 % (key, pattern))
592 % (key, pattern))
588 continue
593 continue
589
594
590 # we need to unescape the delimiter for regexp and format
595 # we need to unescape the delimiter for regexp and format
591 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
596 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
592 regexp = delim_re.sub(unesc, match.group(1))
597 regexp = delim_re.sub(unesc, match.group(1))
593 format = delim_re.sub(unesc, match.group(2))
598 format = delim_re.sub(unesc, match.group(2))
594
599
595 # the pattern allows for 6 regexp flags, so set them if necessary
600 # the pattern allows for 6 regexp flags, so set them if necessary
596 flagin = match.group(3)
601 flagin = match.group(3)
597 flags = 0
602 flags = 0
598 if flagin:
603 if flagin:
599 for flag in flagin.upper():
604 for flag in flagin.upper():
600 flags |= re.__dict__[flag]
605 flags |= re.__dict__[flag]
601
606
602 try:
607 try:
603 regexp = re.compile(regexp, flags)
608 regexp = re.compile(regexp, flags)
604 websubtable.append((regexp, format))
609 websubtable.append((regexp, format))
605 except re.error:
610 except re.error:
606 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
611 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
607 % (key, regexp))
612 % (key, regexp))
608 return websubtable
613 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now