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