##// END OF EJS Templates
hgweb: generate last change date for an empty atom-bookmarks feed (issue5022)...
av6 -
r28712:80e92247 default
parent child Browse files
Show More
@@ -1,1298 +1,1304
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 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
609 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
610 i = sorted(i, key=sortkey, reverse=True)
610 i = sorted(i, key=sortkey, reverse=True)
611 parity = paritygen(web.stripecount)
611 parity = paritygen(web.stripecount)
612
612
613 def entries(latestonly, **map):
613 def entries(latestonly, **map):
614 t = i
614 t = i
615 if latestonly:
615 if latestonly:
616 t = i[:1]
616 t = i[:1]
617 for k, n in t:
617 for k, n in t:
618 yield {"parity": parity.next(),
618 yield {"parity": parity.next(),
619 "bookmark": k,
619 "bookmark": k,
620 "date": web.repo[n].date(),
620 "date": web.repo[n].date(),
621 "node": hex(n)}
621 "node": hex(n)}
622
622
623 if i:
624 latestrev = i[0][1]
625 else:
626 latestrev = -1
627
623 return tmpl("bookmarks",
628 return tmpl("bookmarks",
624 node=hex(web.repo.changelog.tip()),
629 node=hex(web.repo.changelog.tip()),
630 lastchange=[{"date": web.repo[latestrev].date()}],
625 entries=lambda **x: entries(latestonly=False, **x),
631 entries=lambda **x: entries(latestonly=False, **x),
626 latestentry=lambda **x: entries(latestonly=True, **x))
632 latestentry=lambda **x: entries(latestonly=True, **x))
627
633
628 @webcommand('branches')
634 @webcommand('branches')
629 def branches(web, req, tmpl):
635 def branches(web, req, tmpl):
630 """
636 """
631 /branches
637 /branches
632 ---------
638 ---------
633
639
634 Show information about branches.
640 Show information about branches.
635
641
636 All known branches are contained in the output, even closed branches.
642 All known branches are contained in the output, even closed branches.
637
643
638 No arguments are accepted.
644 No arguments are accepted.
639
645
640 The ``branches`` template is rendered.
646 The ``branches`` template is rendered.
641 """
647 """
642 entries = webutil.branchentries(web.repo, web.stripecount)
648 entries = webutil.branchentries(web.repo, web.stripecount)
643 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
649 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
644 return tmpl('branches', node=hex(web.repo.changelog.tip()),
650 return tmpl('branches', node=hex(web.repo.changelog.tip()),
645 entries=entries, latestentry=latestentry)
651 entries=entries, latestentry=latestentry)
646
652
647 @webcommand('summary')
653 @webcommand('summary')
648 def summary(web, req, tmpl):
654 def summary(web, req, tmpl):
649 """
655 """
650 /summary
656 /summary
651 --------
657 --------
652
658
653 Show a summary of repository state.
659 Show a summary of repository state.
654
660
655 Information about the latest changesets, bookmarks, tags, and branches
661 Information about the latest changesets, bookmarks, tags, and branches
656 is captured by this handler.
662 is captured by this handler.
657
663
658 The ``summary`` template is rendered.
664 The ``summary`` template is rendered.
659 """
665 """
660 i = reversed(web.repo.tagslist())
666 i = reversed(web.repo.tagslist())
661
667
662 def tagentries(**map):
668 def tagentries(**map):
663 parity = paritygen(web.stripecount)
669 parity = paritygen(web.stripecount)
664 count = 0
670 count = 0
665 for k, n in i:
671 for k, n in i:
666 if k == "tip": # skip tip
672 if k == "tip": # skip tip
667 continue
673 continue
668
674
669 count += 1
675 count += 1
670 if count > 10: # limit to 10 tags
676 if count > 10: # limit to 10 tags
671 break
677 break
672
678
673 yield tmpl("tagentry",
679 yield tmpl("tagentry",
674 parity=parity.next(),
680 parity=parity.next(),
675 tag=k,
681 tag=k,
676 node=hex(n),
682 node=hex(n),
677 date=web.repo[n].date())
683 date=web.repo[n].date())
678
684
679 def bookmarks(**map):
685 def bookmarks(**map):
680 parity = paritygen(web.stripecount)
686 parity = paritygen(web.stripecount)
681 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
687 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
682 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
688 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
683 marks = sorted(marks, key=sortkey, reverse=True)
689 marks = sorted(marks, key=sortkey, reverse=True)
684 for k, n in marks[:10]: # limit to 10 bookmarks
690 for k, n in marks[:10]: # limit to 10 bookmarks
685 yield {'parity': parity.next(),
691 yield {'parity': parity.next(),
686 'bookmark': k,
692 'bookmark': k,
687 'date': web.repo[n].date(),
693 'date': web.repo[n].date(),
688 'node': hex(n)}
694 'node': hex(n)}
689
695
690 def changelist(**map):
696 def changelist(**map):
691 parity = paritygen(web.stripecount, offset=start - end)
697 parity = paritygen(web.stripecount, offset=start - end)
692 l = [] # build a list in forward order for efficiency
698 l = [] # build a list in forward order for efficiency
693 revs = []
699 revs = []
694 if start < end:
700 if start < end:
695 revs = web.repo.changelog.revs(start, end - 1)
701 revs = web.repo.changelog.revs(start, end - 1)
696 for i in revs:
702 for i in revs:
697 ctx = web.repo[i]
703 ctx = web.repo[i]
698
704
699 l.append(tmpl(
705 l.append(tmpl(
700 'shortlogentry',
706 'shortlogentry',
701 parity=parity.next(),
707 parity=parity.next(),
702 **webutil.commonentry(web.repo, ctx)))
708 **webutil.commonentry(web.repo, ctx)))
703
709
704 l.reverse()
710 l.reverse()
705 yield l
711 yield l
706
712
707 tip = web.repo['tip']
713 tip = web.repo['tip']
708 count = len(web.repo)
714 count = len(web.repo)
709 start = max(0, count - web.maxchanges)
715 start = max(0, count - web.maxchanges)
710 end = min(count, start + web.maxchanges)
716 end = min(count, start + web.maxchanges)
711
717
712 return tmpl("summary",
718 return tmpl("summary",
713 desc=web.config("web", "description", "unknown"),
719 desc=web.config("web", "description", "unknown"),
714 owner=get_contact(web.config) or "unknown",
720 owner=get_contact(web.config) or "unknown",
715 lastchange=tip.date(),
721 lastchange=tip.date(),
716 tags=tagentries,
722 tags=tagentries,
717 bookmarks=bookmarks,
723 bookmarks=bookmarks,
718 branches=webutil.branchentries(web.repo, web.stripecount, 10),
724 branches=webutil.branchentries(web.repo, web.stripecount, 10),
719 shortlog=changelist,
725 shortlog=changelist,
720 node=tip.hex(),
726 node=tip.hex(),
721 symrev='tip',
727 symrev='tip',
722 archives=web.archivelist("tip"))
728 archives=web.archivelist("tip"))
723
729
724 @webcommand('filediff')
730 @webcommand('filediff')
725 def filediff(web, req, tmpl):
731 def filediff(web, req, tmpl):
726 """
732 """
727 /diff/{revision}/{path}
733 /diff/{revision}/{path}
728 -----------------------
734 -----------------------
729
735
730 Show how a file changed in a particular commit.
736 Show how a file changed in a particular commit.
731
737
732 The ``filediff`` template is rendered.
738 The ``filediff`` template is rendered.
733
739
734 This handler is registered under both the ``/diff`` and ``/filediff``
740 This handler is registered under both the ``/diff`` and ``/filediff``
735 paths. ``/diff`` is used in modern code.
741 paths. ``/diff`` is used in modern code.
736 """
742 """
737 fctx, ctx = None, None
743 fctx, ctx = None, None
738 try:
744 try:
739 fctx = webutil.filectx(web.repo, req)
745 fctx = webutil.filectx(web.repo, req)
740 except LookupError:
746 except LookupError:
741 ctx = webutil.changectx(web.repo, req)
747 ctx = webutil.changectx(web.repo, req)
742 path = webutil.cleanpath(web.repo, req.form['file'][0])
748 path = webutil.cleanpath(web.repo, req.form['file'][0])
743 if path not in ctx.files():
749 if path not in ctx.files():
744 raise
750 raise
745
751
746 if fctx is not None:
752 if fctx is not None:
747 path = fctx.path()
753 path = fctx.path()
748 ctx = fctx.changectx()
754 ctx = fctx.changectx()
749
755
750 parity = paritygen(web.stripecount)
756 parity = paritygen(web.stripecount)
751 style = web.config('web', 'style', 'paper')
757 style = web.config('web', 'style', 'paper')
752 if 'style' in req.form:
758 if 'style' in req.form:
753 style = req.form['style'][0]
759 style = req.form['style'][0]
754
760
755 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
761 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
756 if fctx is not None:
762 if fctx is not None:
757 rename = webutil.renamelink(fctx)
763 rename = webutil.renamelink(fctx)
758 ctx = fctx
764 ctx = fctx
759 else:
765 else:
760 rename = []
766 rename = []
761 ctx = ctx
767 ctx = ctx
762 return tmpl("filediff",
768 return tmpl("filediff",
763 file=path,
769 file=path,
764 symrev=webutil.symrevorshortnode(req, ctx),
770 symrev=webutil.symrevorshortnode(req, ctx),
765 rename=rename,
771 rename=rename,
766 diff=diffs,
772 diff=diffs,
767 **webutil.commonentry(web.repo, ctx))
773 **webutil.commonentry(web.repo, ctx))
768
774
769 diff = webcommand('diff')(filediff)
775 diff = webcommand('diff')(filediff)
770
776
771 @webcommand('comparison')
777 @webcommand('comparison')
772 def comparison(web, req, tmpl):
778 def comparison(web, req, tmpl):
773 """
779 """
774 /comparison/{revision}/{path}
780 /comparison/{revision}/{path}
775 -----------------------------
781 -----------------------------
776
782
777 Show a comparison between the old and new versions of a file from changes
783 Show a comparison between the old and new versions of a file from changes
778 made on a particular revision.
784 made on a particular revision.
779
785
780 This is similar to the ``diff`` handler. However, this form features
786 This is similar to the ``diff`` handler. However, this form features
781 a split or side-by-side diff rather than a unified diff.
787 a split or side-by-side diff rather than a unified diff.
782
788
783 The ``context`` query string argument can be used to control the lines of
789 The ``context`` query string argument can be used to control the lines of
784 context in the diff.
790 context in the diff.
785
791
786 The ``filecomparison`` template is rendered.
792 The ``filecomparison`` template is rendered.
787 """
793 """
788 ctx = webutil.changectx(web.repo, req)
794 ctx = webutil.changectx(web.repo, req)
789 if 'file' not in req.form:
795 if 'file' not in req.form:
790 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
796 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
791 path = webutil.cleanpath(web.repo, req.form['file'][0])
797 path = webutil.cleanpath(web.repo, req.form['file'][0])
792
798
793 parsecontext = lambda v: v == 'full' and -1 or int(v)
799 parsecontext = lambda v: v == 'full' and -1 or int(v)
794 if 'context' in req.form:
800 if 'context' in req.form:
795 context = parsecontext(req.form['context'][0])
801 context = parsecontext(req.form['context'][0])
796 else:
802 else:
797 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
803 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
798
804
799 def filelines(f):
805 def filelines(f):
800 if util.binary(f.data()):
806 if util.binary(f.data()):
801 mt = mimetypes.guess_type(f.path())[0]
807 mt = mimetypes.guess_type(f.path())[0]
802 if not mt:
808 if not mt:
803 mt = 'application/octet-stream'
809 mt = 'application/octet-stream'
804 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
810 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
805 return f.data().splitlines()
811 return f.data().splitlines()
806
812
807 fctx = None
813 fctx = None
808 parent = ctx.p1()
814 parent = ctx.p1()
809 leftrev = parent.rev()
815 leftrev = parent.rev()
810 leftnode = parent.node()
816 leftnode = parent.node()
811 rightrev = ctx.rev()
817 rightrev = ctx.rev()
812 rightnode = ctx.node()
818 rightnode = ctx.node()
813 if path in ctx:
819 if path in ctx:
814 fctx = ctx[path]
820 fctx = ctx[path]
815 rightlines = filelines(fctx)
821 rightlines = filelines(fctx)
816 if path not in parent:
822 if path not in parent:
817 leftlines = ()
823 leftlines = ()
818 else:
824 else:
819 pfctx = parent[path]
825 pfctx = parent[path]
820 leftlines = filelines(pfctx)
826 leftlines = filelines(pfctx)
821 else:
827 else:
822 rightlines = ()
828 rightlines = ()
823 pfctx = ctx.parents()[0][path]
829 pfctx = ctx.parents()[0][path]
824 leftlines = filelines(pfctx)
830 leftlines = filelines(pfctx)
825
831
826 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
832 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
827 if fctx is not None:
833 if fctx is not None:
828 rename = webutil.renamelink(fctx)
834 rename = webutil.renamelink(fctx)
829 ctx = fctx
835 ctx = fctx
830 else:
836 else:
831 rename = []
837 rename = []
832 ctx = ctx
838 ctx = ctx
833 return tmpl('filecomparison',
839 return tmpl('filecomparison',
834 file=path,
840 file=path,
835 symrev=webutil.symrevorshortnode(req, ctx),
841 symrev=webutil.symrevorshortnode(req, ctx),
836 rename=rename,
842 rename=rename,
837 leftrev=leftrev,
843 leftrev=leftrev,
838 leftnode=hex(leftnode),
844 leftnode=hex(leftnode),
839 rightrev=rightrev,
845 rightrev=rightrev,
840 rightnode=hex(rightnode),
846 rightnode=hex(rightnode),
841 comparison=comparison,
847 comparison=comparison,
842 **webutil.commonentry(web.repo, ctx))
848 **webutil.commonentry(web.repo, ctx))
843
849
844 @webcommand('annotate')
850 @webcommand('annotate')
845 def annotate(web, req, tmpl):
851 def annotate(web, req, tmpl):
846 """
852 """
847 /annotate/{revision}/{path}
853 /annotate/{revision}/{path}
848 ---------------------------
854 ---------------------------
849
855
850 Show changeset information for each line in a file.
856 Show changeset information for each line in a file.
851
857
852 The ``fileannotate`` template is rendered.
858 The ``fileannotate`` template is rendered.
853 """
859 """
854 fctx = webutil.filectx(web.repo, req)
860 fctx = webutil.filectx(web.repo, req)
855 f = fctx.path()
861 f = fctx.path()
856 parity = paritygen(web.stripecount)
862 parity = paritygen(web.stripecount)
857 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
863 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
858 section='annotate', whitespace=True)
864 section='annotate', whitespace=True)
859
865
860 def annotate(**map):
866 def annotate(**map):
861 last = None
867 last = None
862 if util.binary(fctx.data()):
868 if util.binary(fctx.data()):
863 mt = (mimetypes.guess_type(fctx.path())[0]
869 mt = (mimetypes.guess_type(fctx.path())[0]
864 or 'application/octet-stream')
870 or 'application/octet-stream')
865 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
871 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
866 '(binary:%s)' % mt)])
872 '(binary:%s)' % mt)])
867 else:
873 else:
868 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
874 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
869 diffopts=diffopts))
875 diffopts=diffopts))
870 for lineno, ((f, targetline), l) in lines:
876 for lineno, ((f, targetline), l) in lines:
871 fnode = f.filenode()
877 fnode = f.filenode()
872
878
873 if last != fnode:
879 if last != fnode:
874 last = fnode
880 last = fnode
875
881
876 yield {"parity": parity.next(),
882 yield {"parity": parity.next(),
877 "node": f.hex(),
883 "node": f.hex(),
878 "rev": f.rev(),
884 "rev": f.rev(),
879 "author": f.user(),
885 "author": f.user(),
880 "desc": f.description(),
886 "desc": f.description(),
881 "extra": f.extra(),
887 "extra": f.extra(),
882 "file": f.path(),
888 "file": f.path(),
883 "targetline": targetline,
889 "targetline": targetline,
884 "line": l,
890 "line": l,
885 "lineno": lineno + 1,
891 "lineno": lineno + 1,
886 "lineid": "l%d" % (lineno + 1),
892 "lineid": "l%d" % (lineno + 1),
887 "linenumber": "% 6d" % (lineno + 1),
893 "linenumber": "% 6d" % (lineno + 1),
888 "revdate": f.date()}
894 "revdate": f.date()}
889
895
890 return tmpl("fileannotate",
896 return tmpl("fileannotate",
891 file=f,
897 file=f,
892 annotate=annotate,
898 annotate=annotate,
893 path=webutil.up(f),
899 path=webutil.up(f),
894 symrev=webutil.symrevorshortnode(req, fctx),
900 symrev=webutil.symrevorshortnode(req, fctx),
895 rename=webutil.renamelink(fctx),
901 rename=webutil.renamelink(fctx),
896 permissions=fctx.manifest().flags(f),
902 permissions=fctx.manifest().flags(f),
897 **webutil.commonentry(web.repo, fctx))
903 **webutil.commonentry(web.repo, fctx))
898
904
899 @webcommand('filelog')
905 @webcommand('filelog')
900 def filelog(web, req, tmpl):
906 def filelog(web, req, tmpl):
901 """
907 """
902 /filelog/{revision}/{path}
908 /filelog/{revision}/{path}
903 --------------------------
909 --------------------------
904
910
905 Show information about the history of a file in the repository.
911 Show information about the history of a file in the repository.
906
912
907 The ``revcount`` query string argument can be defined to control the
913 The ``revcount`` query string argument can be defined to control the
908 maximum number of entries to show.
914 maximum number of entries to show.
909
915
910 The ``filelog`` template will be rendered.
916 The ``filelog`` template will be rendered.
911 """
917 """
912
918
913 try:
919 try:
914 fctx = webutil.filectx(web.repo, req)
920 fctx = webutil.filectx(web.repo, req)
915 f = fctx.path()
921 f = fctx.path()
916 fl = fctx.filelog()
922 fl = fctx.filelog()
917 except error.LookupError:
923 except error.LookupError:
918 f = webutil.cleanpath(web.repo, req.form['file'][0])
924 f = webutil.cleanpath(web.repo, req.form['file'][0])
919 fl = web.repo.file(f)
925 fl = web.repo.file(f)
920 numrevs = len(fl)
926 numrevs = len(fl)
921 if not numrevs: # file doesn't exist at all
927 if not numrevs: # file doesn't exist at all
922 raise
928 raise
923 rev = webutil.changectx(web.repo, req).rev()
929 rev = webutil.changectx(web.repo, req).rev()
924 first = fl.linkrev(0)
930 first = fl.linkrev(0)
925 if rev < first: # current rev is from before file existed
931 if rev < first: # current rev is from before file existed
926 raise
932 raise
927 frev = numrevs - 1
933 frev = numrevs - 1
928 while fl.linkrev(frev) > rev:
934 while fl.linkrev(frev) > rev:
929 frev -= 1
935 frev -= 1
930 fctx = web.repo.filectx(f, fl.linkrev(frev))
936 fctx = web.repo.filectx(f, fl.linkrev(frev))
931
937
932 revcount = web.maxshortchanges
938 revcount = web.maxshortchanges
933 if 'revcount' in req.form:
939 if 'revcount' in req.form:
934 try:
940 try:
935 revcount = int(req.form.get('revcount', [revcount])[0])
941 revcount = int(req.form.get('revcount', [revcount])[0])
936 revcount = max(revcount, 1)
942 revcount = max(revcount, 1)
937 tmpl.defaults['sessionvars']['revcount'] = revcount
943 tmpl.defaults['sessionvars']['revcount'] = revcount
938 except ValueError:
944 except ValueError:
939 pass
945 pass
940
946
941 lessvars = copy.copy(tmpl.defaults['sessionvars'])
947 lessvars = copy.copy(tmpl.defaults['sessionvars'])
942 lessvars['revcount'] = max(revcount / 2, 1)
948 lessvars['revcount'] = max(revcount / 2, 1)
943 morevars = copy.copy(tmpl.defaults['sessionvars'])
949 morevars = copy.copy(tmpl.defaults['sessionvars'])
944 morevars['revcount'] = revcount * 2
950 morevars['revcount'] = revcount * 2
945
951
946 count = fctx.filerev() + 1
952 count = fctx.filerev() + 1
947 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
953 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
948 end = min(count, start + revcount) # last rev on this page
954 end = min(count, start + revcount) # last rev on this page
949 parity = paritygen(web.stripecount, offset=start - end)
955 parity = paritygen(web.stripecount, offset=start - end)
950
956
951 def entries():
957 def entries():
952 l = []
958 l = []
953
959
954 repo = web.repo
960 repo = web.repo
955 revs = fctx.filelog().revs(start, end - 1)
961 revs = fctx.filelog().revs(start, end - 1)
956 for i in revs:
962 for i in revs:
957 iterfctx = fctx.filectx(i)
963 iterfctx = fctx.filectx(i)
958
964
959 l.append(dict(
965 l.append(dict(
960 parity=parity.next(),
966 parity=parity.next(),
961 filerev=i,
967 filerev=i,
962 file=f,
968 file=f,
963 rename=webutil.renamelink(iterfctx),
969 rename=webutil.renamelink(iterfctx),
964 **webutil.commonentry(repo, iterfctx)))
970 **webutil.commonentry(repo, iterfctx)))
965 for e in reversed(l):
971 for e in reversed(l):
966 yield e
972 yield e
967
973
968 entries = list(entries())
974 entries = list(entries())
969 latestentry = entries[:1]
975 latestentry = entries[:1]
970
976
971 revnav = webutil.filerevnav(web.repo, fctx.path())
977 revnav = webutil.filerevnav(web.repo, fctx.path())
972 nav = revnav.gen(end - 1, revcount, count)
978 nav = revnav.gen(end - 1, revcount, count)
973 return tmpl("filelog",
979 return tmpl("filelog",
974 file=f,
980 file=f,
975 nav=nav,
981 nav=nav,
976 symrev=webutil.symrevorshortnode(req, fctx),
982 symrev=webutil.symrevorshortnode(req, fctx),
977 entries=entries,
983 entries=entries,
978 latestentry=latestentry,
984 latestentry=latestentry,
979 revcount=revcount,
985 revcount=revcount,
980 morevars=morevars,
986 morevars=morevars,
981 lessvars=lessvars,
987 lessvars=lessvars,
982 **webutil.commonentry(web.repo, fctx))
988 **webutil.commonentry(web.repo, fctx))
983
989
984 @webcommand('archive')
990 @webcommand('archive')
985 def archive(web, req, tmpl):
991 def archive(web, req, tmpl):
986 """
992 """
987 /archive/{revision}.{format}[/{path}]
993 /archive/{revision}.{format}[/{path}]
988 -------------------------------------
994 -------------------------------------
989
995
990 Obtain an archive of repository content.
996 Obtain an archive of repository content.
991
997
992 The content and type of the archive is defined by a URL path parameter.
998 The content and type of the archive is defined by a URL path parameter.
993 ``format`` is the file extension of the archive type to be generated. e.g.
999 ``format`` is the file extension of the archive type to be generated. e.g.
994 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1000 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
995 server configuration.
1001 server configuration.
996
1002
997 The optional ``path`` URL parameter controls content to include in the
1003 The optional ``path`` URL parameter controls content to include in the
998 archive. If omitted, every file in the specified revision is present in the
1004 archive. If omitted, every file in the specified revision is present in the
999 archive. If included, only the specified file or contents of the specified
1005 archive. If included, only the specified file or contents of the specified
1000 directory will be included in the archive.
1006 directory will be included in the archive.
1001
1007
1002 No template is used for this handler. Raw, binary content is generated.
1008 No template is used for this handler. Raw, binary content is generated.
1003 """
1009 """
1004
1010
1005 type_ = req.form.get('type', [None])[0]
1011 type_ = req.form.get('type', [None])[0]
1006 allowed = web.configlist("web", "allow_archive")
1012 allowed = web.configlist("web", "allow_archive")
1007 key = req.form['node'][0]
1013 key = req.form['node'][0]
1008
1014
1009 if type_ not in web.archives:
1015 if type_ not in web.archives:
1010 msg = 'Unsupported archive type: %s' % type_
1016 msg = 'Unsupported archive type: %s' % type_
1011 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1017 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1012
1018
1013 if not ((type_ in allowed or
1019 if not ((type_ in allowed or
1014 web.configbool("web", "allow" + type_, False))):
1020 web.configbool("web", "allow" + type_, False))):
1015 msg = 'Archive type not allowed: %s' % type_
1021 msg = 'Archive type not allowed: %s' % type_
1016 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1022 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1017
1023
1018 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1024 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1019 cnode = web.repo.lookup(key)
1025 cnode = web.repo.lookup(key)
1020 arch_version = key
1026 arch_version = key
1021 if cnode == key or key == 'tip':
1027 if cnode == key or key == 'tip':
1022 arch_version = short(cnode)
1028 arch_version = short(cnode)
1023 name = "%s-%s" % (reponame, arch_version)
1029 name = "%s-%s" % (reponame, arch_version)
1024
1030
1025 ctx = webutil.changectx(web.repo, req)
1031 ctx = webutil.changectx(web.repo, req)
1026 pats = []
1032 pats = []
1027 matchfn = scmutil.match(ctx, [])
1033 matchfn = scmutil.match(ctx, [])
1028 file = req.form.get('file', None)
1034 file = req.form.get('file', None)
1029 if file:
1035 if file:
1030 pats = ['path:' + file[0]]
1036 pats = ['path:' + file[0]]
1031 matchfn = scmutil.match(ctx, pats, default='path')
1037 matchfn = scmutil.match(ctx, pats, default='path')
1032 if pats:
1038 if pats:
1033 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1039 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1034 if not files:
1040 if not files:
1035 raise ErrorResponse(HTTP_NOT_FOUND,
1041 raise ErrorResponse(HTTP_NOT_FOUND,
1036 'file(s) not found: %s' % file[0])
1042 'file(s) not found: %s' % file[0])
1037
1043
1038 mimetype, artype, extension, encoding = web.archivespecs[type_]
1044 mimetype, artype, extension, encoding = web.archivespecs[type_]
1039 headers = [
1045 headers = [
1040 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1046 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1041 ]
1047 ]
1042 if encoding:
1048 if encoding:
1043 headers.append(('Content-Encoding', encoding))
1049 headers.append(('Content-Encoding', encoding))
1044 req.headers.extend(headers)
1050 req.headers.extend(headers)
1045 req.respond(HTTP_OK, mimetype)
1051 req.respond(HTTP_OK, mimetype)
1046
1052
1047 archival.archive(web.repo, req, cnode, artype, prefix=name,
1053 archival.archive(web.repo, req, cnode, artype, prefix=name,
1048 matchfn=matchfn,
1054 matchfn=matchfn,
1049 subrepos=web.configbool("web", "archivesubrepos"))
1055 subrepos=web.configbool("web", "archivesubrepos"))
1050 return []
1056 return []
1051
1057
1052
1058
1053 @webcommand('static')
1059 @webcommand('static')
1054 def static(web, req, tmpl):
1060 def static(web, req, tmpl):
1055 fname = req.form['file'][0]
1061 fname = req.form['file'][0]
1056 # a repo owner may set web.static in .hg/hgrc to get any file
1062 # a repo owner may set web.static in .hg/hgrc to get any file
1057 # readable by the user running the CGI script
1063 # readable by the user running the CGI script
1058 static = web.config("web", "static", None, untrusted=False)
1064 static = web.config("web", "static", None, untrusted=False)
1059 if not static:
1065 if not static:
1060 tp = web.templatepath or templater.templatepaths()
1066 tp = web.templatepath or templater.templatepaths()
1061 if isinstance(tp, str):
1067 if isinstance(tp, str):
1062 tp = [tp]
1068 tp = [tp]
1063 static = [os.path.join(p, 'static') for p in tp]
1069 static = [os.path.join(p, 'static') for p in tp]
1064 staticfile(static, fname, req)
1070 staticfile(static, fname, req)
1065 return []
1071 return []
1066
1072
1067 @webcommand('graph')
1073 @webcommand('graph')
1068 def graph(web, req, tmpl):
1074 def graph(web, req, tmpl):
1069 """
1075 """
1070 /graph[/{revision}]
1076 /graph[/{revision}]
1071 -------------------
1077 -------------------
1072
1078
1073 Show information about the graphical topology of the repository.
1079 Show information about the graphical topology of the repository.
1074
1080
1075 Information rendered by this handler can be used to create visual
1081 Information rendered by this handler can be used to create visual
1076 representations of repository topology.
1082 representations of repository topology.
1077
1083
1078 The ``revision`` URL parameter controls the starting changeset.
1084 The ``revision`` URL parameter controls the starting changeset.
1079
1085
1080 The ``revcount`` query string argument can define the number of changesets
1086 The ``revcount`` query string argument can define the number of changesets
1081 to show information for.
1087 to show information for.
1082
1088
1083 This handler will render the ``graph`` template.
1089 This handler will render the ``graph`` template.
1084 """
1090 """
1085
1091
1086 if 'node' in req.form:
1092 if 'node' in req.form:
1087 ctx = webutil.changectx(web.repo, req)
1093 ctx = webutil.changectx(web.repo, req)
1088 symrev = webutil.symrevorshortnode(req, ctx)
1094 symrev = webutil.symrevorshortnode(req, ctx)
1089 else:
1095 else:
1090 ctx = web.repo['tip']
1096 ctx = web.repo['tip']
1091 symrev = 'tip'
1097 symrev = 'tip'
1092 rev = ctx.rev()
1098 rev = ctx.rev()
1093
1099
1094 bg_height = 39
1100 bg_height = 39
1095 revcount = web.maxshortchanges
1101 revcount = web.maxshortchanges
1096 if 'revcount' in req.form:
1102 if 'revcount' in req.form:
1097 try:
1103 try:
1098 revcount = int(req.form.get('revcount', [revcount])[0])
1104 revcount = int(req.form.get('revcount', [revcount])[0])
1099 revcount = max(revcount, 1)
1105 revcount = max(revcount, 1)
1100 tmpl.defaults['sessionvars']['revcount'] = revcount
1106 tmpl.defaults['sessionvars']['revcount'] = revcount
1101 except ValueError:
1107 except ValueError:
1102 pass
1108 pass
1103
1109
1104 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1110 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1105 lessvars['revcount'] = max(revcount / 2, 1)
1111 lessvars['revcount'] = max(revcount / 2, 1)
1106 morevars = copy.copy(tmpl.defaults['sessionvars'])
1112 morevars = copy.copy(tmpl.defaults['sessionvars'])
1107 morevars['revcount'] = revcount * 2
1113 morevars['revcount'] = revcount * 2
1108
1114
1109 count = len(web.repo)
1115 count = len(web.repo)
1110 pos = rev
1116 pos = rev
1111
1117
1112 uprev = min(max(0, count - 1), rev + revcount)
1118 uprev = min(max(0, count - 1), rev + revcount)
1113 downrev = max(0, rev - revcount)
1119 downrev = max(0, rev - revcount)
1114 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1120 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1115
1121
1116 tree = []
1122 tree = []
1117 if pos != -1:
1123 if pos != -1:
1118 allrevs = web.repo.changelog.revs(pos, 0)
1124 allrevs = web.repo.changelog.revs(pos, 0)
1119 revs = []
1125 revs = []
1120 for i in allrevs:
1126 for i in allrevs:
1121 revs.append(i)
1127 revs.append(i)
1122 if len(revs) >= revcount:
1128 if len(revs) >= revcount:
1123 break
1129 break
1124
1130
1125 # We have to feed a baseset to dagwalker as it is expecting smartset
1131 # We have to feed a baseset to dagwalker as it is expecting smartset
1126 # object. This does not have a big impact on hgweb performance itself
1132 # object. This does not have a big impact on hgweb performance itself
1127 # since hgweb graphing code is not itself lazy yet.
1133 # since hgweb graphing code is not itself lazy yet.
1128 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1134 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1129 # As we said one line above... not lazy.
1135 # As we said one line above... not lazy.
1130 tree = list(graphmod.colored(dag, web.repo))
1136 tree = list(graphmod.colored(dag, web.repo))
1131
1137
1132 def getcolumns(tree):
1138 def getcolumns(tree):
1133 cols = 0
1139 cols = 0
1134 for (id, type, ctx, vtx, edges) in tree:
1140 for (id, type, ctx, vtx, edges) in tree:
1135 if type != graphmod.CHANGESET:
1141 if type != graphmod.CHANGESET:
1136 continue
1142 continue
1137 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1143 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1138 max([edge[1] for edge in edges] or [0]))
1144 max([edge[1] for edge in edges] or [0]))
1139 return cols
1145 return cols
1140
1146
1141 def graphdata(usetuples, encodestr):
1147 def graphdata(usetuples, encodestr):
1142 data = []
1148 data = []
1143
1149
1144 row = 0
1150 row = 0
1145 for (id, type, ctx, vtx, edges) in tree:
1151 for (id, type, ctx, vtx, edges) in tree:
1146 if type != graphmod.CHANGESET:
1152 if type != graphmod.CHANGESET:
1147 continue
1153 continue
1148 node = str(ctx)
1154 node = str(ctx)
1149 age = encodestr(templatefilters.age(ctx.date()))
1155 age = encodestr(templatefilters.age(ctx.date()))
1150 desc = templatefilters.firstline(encodestr(ctx.description()))
1156 desc = templatefilters.firstline(encodestr(ctx.description()))
1151 desc = cgi.escape(templatefilters.nonempty(desc))
1157 desc = cgi.escape(templatefilters.nonempty(desc))
1152 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1158 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1153 branch = cgi.escape(encodestr(ctx.branch()))
1159 branch = cgi.escape(encodestr(ctx.branch()))
1154 try:
1160 try:
1155 branchnode = web.repo.branchtip(branch)
1161 branchnode = web.repo.branchtip(branch)
1156 except error.RepoLookupError:
1162 except error.RepoLookupError:
1157 branchnode = None
1163 branchnode = None
1158 branch = branch, branchnode == ctx.node()
1164 branch = branch, branchnode == ctx.node()
1159
1165
1160 if usetuples:
1166 if usetuples:
1161 data.append((node, vtx, edges, desc, user, age, branch,
1167 data.append((node, vtx, edges, desc, user, age, branch,
1162 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1168 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1163 [cgi.escape(encodestr(x))
1169 [cgi.escape(encodestr(x))
1164 for x in ctx.bookmarks()]))
1170 for x in ctx.bookmarks()]))
1165 else:
1171 else:
1166 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1172 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1167 'color': (edge[2] - 1) % 6 + 1,
1173 'color': (edge[2] - 1) % 6 + 1,
1168 'width': edge[3], 'bcolor': edge[4]}
1174 'width': edge[3], 'bcolor': edge[4]}
1169 for edge in edges]
1175 for edge in edges]
1170
1176
1171 data.append(
1177 data.append(
1172 {'node': node,
1178 {'node': node,
1173 'col': vtx[0],
1179 'col': vtx[0],
1174 'color': (vtx[1] - 1) % 6 + 1,
1180 'color': (vtx[1] - 1) % 6 + 1,
1175 'edges': edgedata,
1181 'edges': edgedata,
1176 'row': row,
1182 'row': row,
1177 'nextrow': row + 1,
1183 'nextrow': row + 1,
1178 'desc': desc,
1184 'desc': desc,
1179 'user': user,
1185 'user': user,
1180 'age': age,
1186 'age': age,
1181 'bookmarks': webutil.nodebookmarksdict(
1187 'bookmarks': webutil.nodebookmarksdict(
1182 web.repo, ctx.node()),
1188 web.repo, ctx.node()),
1183 'branches': webutil.nodebranchdict(web.repo, ctx),
1189 'branches': webutil.nodebranchdict(web.repo, ctx),
1184 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1190 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1185 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1191 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1186
1192
1187 row += 1
1193 row += 1
1188
1194
1189 return data
1195 return data
1190
1196
1191 cols = getcolumns(tree)
1197 cols = getcolumns(tree)
1192 rows = len(tree)
1198 rows = len(tree)
1193 canvasheight = (rows + 1) * bg_height - 27
1199 canvasheight = (rows + 1) * bg_height - 27
1194
1200
1195 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1201 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1196 uprev=uprev,
1202 uprev=uprev,
1197 lessvars=lessvars, morevars=morevars, downrev=downrev,
1203 lessvars=lessvars, morevars=morevars, downrev=downrev,
1198 cols=cols, rows=rows,
1204 cols=cols, rows=rows,
1199 canvaswidth=(cols + 1) * bg_height,
1205 canvaswidth=(cols + 1) * bg_height,
1200 truecanvasheight=rows * bg_height,
1206 truecanvasheight=rows * bg_height,
1201 canvasheight=canvasheight, bg_height=bg_height,
1207 canvasheight=canvasheight, bg_height=bg_height,
1202 # {jsdata} will be passed to |json, so it must be in utf-8
1208 # {jsdata} will be passed to |json, so it must be in utf-8
1203 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1209 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1204 nodes=lambda **x: graphdata(False, str),
1210 nodes=lambda **x: graphdata(False, str),
1205 node=ctx.hex(), changenav=changenav)
1211 node=ctx.hex(), changenav=changenav)
1206
1212
1207 def _getdoc(e):
1213 def _getdoc(e):
1208 doc = e[0].__doc__
1214 doc = e[0].__doc__
1209 if doc:
1215 if doc:
1210 doc = _(doc).partition('\n')[0]
1216 doc = _(doc).partition('\n')[0]
1211 else:
1217 else:
1212 doc = _('(no help text available)')
1218 doc = _('(no help text available)')
1213 return doc
1219 return doc
1214
1220
1215 @webcommand('help')
1221 @webcommand('help')
1216 def help(web, req, tmpl):
1222 def help(web, req, tmpl):
1217 """
1223 """
1218 /help[/{topic}]
1224 /help[/{topic}]
1219 ---------------
1225 ---------------
1220
1226
1221 Render help documentation.
1227 Render help documentation.
1222
1228
1223 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1229 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1224 is defined, that help topic will be rendered. If not, an index of
1230 is defined, that help topic will be rendered. If not, an index of
1225 available help topics will be rendered.
1231 available help topics will be rendered.
1226
1232
1227 The ``help`` template will be rendered when requesting help for a topic.
1233 The ``help`` template will be rendered when requesting help for a topic.
1228 ``helptopics`` will be rendered for the index of help topics.
1234 ``helptopics`` will be rendered for the index of help topics.
1229 """
1235 """
1230 from .. import commands, help as helpmod # avoid cycle
1236 from .. import commands, help as helpmod # avoid cycle
1231
1237
1232 topicname = req.form.get('node', [None])[0]
1238 topicname = req.form.get('node', [None])[0]
1233 if not topicname:
1239 if not topicname:
1234 def topics(**map):
1240 def topics(**map):
1235 for entries, summary, _doc in helpmod.helptable:
1241 for entries, summary, _doc in helpmod.helptable:
1236 yield {'topic': entries[0], 'summary': summary}
1242 yield {'topic': entries[0], 'summary': summary}
1237
1243
1238 early, other = [], []
1244 early, other = [], []
1239 primary = lambda s: s.partition('|')[0]
1245 primary = lambda s: s.partition('|')[0]
1240 for c, e in commands.table.iteritems():
1246 for c, e in commands.table.iteritems():
1241 doc = _getdoc(e)
1247 doc = _getdoc(e)
1242 if 'DEPRECATED' in doc or c.startswith('debug'):
1248 if 'DEPRECATED' in doc or c.startswith('debug'):
1243 continue
1249 continue
1244 cmd = primary(c)
1250 cmd = primary(c)
1245 if cmd.startswith('^'):
1251 if cmd.startswith('^'):
1246 early.append((cmd[1:], doc))
1252 early.append((cmd[1:], doc))
1247 else:
1253 else:
1248 other.append((cmd, doc))
1254 other.append((cmd, doc))
1249
1255
1250 early.sort()
1256 early.sort()
1251 other.sort()
1257 other.sort()
1252
1258
1253 def earlycommands(**map):
1259 def earlycommands(**map):
1254 for c, doc in early:
1260 for c, doc in early:
1255 yield {'topic': c, 'summary': doc}
1261 yield {'topic': c, 'summary': doc}
1256
1262
1257 def othercommands(**map):
1263 def othercommands(**map):
1258 for c, doc in other:
1264 for c, doc in other:
1259 yield {'topic': c, 'summary': doc}
1265 yield {'topic': c, 'summary': doc}
1260
1266
1261 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1267 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1262 othercommands=othercommands, title='Index')
1268 othercommands=othercommands, title='Index')
1263
1269
1264 # Render an index of sub-topics.
1270 # Render an index of sub-topics.
1265 if topicname in helpmod.subtopics:
1271 if topicname in helpmod.subtopics:
1266 topics = []
1272 topics = []
1267 for entries, summary, _doc in helpmod.subtopics[topicname]:
1273 for entries, summary, _doc in helpmod.subtopics[topicname]:
1268 topics.append({
1274 topics.append({
1269 'topic': '%s.%s' % (topicname, entries[0]),
1275 'topic': '%s.%s' % (topicname, entries[0]),
1270 'basename': entries[0],
1276 'basename': entries[0],
1271 'summary': summary,
1277 'summary': summary,
1272 })
1278 })
1273
1279
1274 return tmpl('helptopics', topics=topics, title=topicname,
1280 return tmpl('helptopics', topics=topics, title=topicname,
1275 subindex=True)
1281 subindex=True)
1276
1282
1277 u = webutil.wsgiui()
1283 u = webutil.wsgiui()
1278 u.verbose = True
1284 u.verbose = True
1279
1285
1280 # Render a page from a sub-topic.
1286 # Render a page from a sub-topic.
1281 if '.' in topicname:
1287 if '.' in topicname:
1282 # TODO implement support for rendering sections, like
1288 # TODO implement support for rendering sections, like
1283 # `hg help` works.
1289 # `hg help` works.
1284 topic, subtopic = topicname.split('.', 1)
1290 topic, subtopic = topicname.split('.', 1)
1285 if topic not in helpmod.subtopics:
1291 if topic not in helpmod.subtopics:
1286 raise ErrorResponse(HTTP_NOT_FOUND)
1292 raise ErrorResponse(HTTP_NOT_FOUND)
1287 else:
1293 else:
1288 topic = topicname
1294 topic = topicname
1289 subtopic = None
1295 subtopic = None
1290
1296
1291 try:
1297 try:
1292 doc = helpmod.help_(u, topic, subtopic=subtopic)
1298 doc = helpmod.help_(u, topic, subtopic=subtopic)
1293 except error.UnknownCommand:
1299 except error.UnknownCommand:
1294 raise ErrorResponse(HTTP_NOT_FOUND)
1300 raise ErrorResponse(HTTP_NOT_FOUND)
1295 return tmpl('help', topic=topicname, doc=doc)
1301 return tmpl('help', topic=topicname, doc=doc)
1296
1302
1297 # tell hggettext to extract docstrings from these functions:
1303 # tell hggettext to extract docstrings from these functions:
1298 i18nfunctions = commands.values()
1304 i18nfunctions = commands.values()
@@ -1,11 +1,11
1 {header}
1 {header}
2 <id>{urlbase}{url|urlescape}</id>
2 <id>{urlbase}{url|urlescape}</id>
3 <link rel="self" href="{urlbase}{url|urlescape}atom-bookmarks"/>
3 <link rel="self" href="{urlbase}{url|urlescape}atom-bookmarks"/>
4 <link rel="alternate" href="{urlbase}{url|urlescape}bookmarks"/>
4 <link rel="alternate" href="{urlbase}{url|urlescape}bookmarks"/>
5 <title>{repo|escape}: bookmarks</title>
5 <title>{repo|escape}: bookmarks</title>
6 <summary>{repo|escape} bookmark history</summary>
6 <summary>{repo|escape} bookmark history</summary>
7 <author><name>Mercurial SCM</name></author>
7 <author><name>Mercurial SCM</name></author>
8 {latestentry%feedupdated}
8 {lastchange%feedupdated}
9
9
10 {entries%bookmarkentry}
10 {entries%bookmarkentry}
11 </feed>
11 </feed>
@@ -1,480 +1,480
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')
464 $ (get-with-headers.py localhost:$HGPORT 'atom-bookmarks')
465 200 Script output follows
465 200 Script output follows
466
466
467 <?xml version="1.0" encoding="ascii"?>
467 <?xml version="1.0" encoding="ascii"?>
468 <feed xmlns="http://www.w3.org/2005/Atom">
468 <feed xmlns="http://www.w3.org/2005/Atom">
469 <id>http://*:$HGPORT/</id> (glob)
469 <id>http://*:$HGPORT/</id> (glob)
470 <link rel="self" href="http://*:$HGPORT/atom-bookmarks"/> (glob)
470 <link rel="self" href="http://*:$HGPORT/atom-bookmarks"/> (glob)
471 <link rel="alternate" href="http://*:$HGPORT/bookmarks"/> (glob)
471 <link rel="alternate" href="http://*:$HGPORT/bookmarks"/> (glob)
472 <title>test: bookmarks</title>
472 <title>test: bookmarks</title>
473 <summary>test bookmark history</summary>
473 <summary>test bookmark history</summary>
474 <author><name>Mercurial SCM</name></author>
474 <author><name>Mercurial SCM</name></author>
475
475 <updated>1970-01-01T00:00:00+00:00</updated>
476
476
477
477
478 </feed>
478 </feed>
479
479
480 $ cd ..
480 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now