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