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