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