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