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