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