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