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