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