##// END OF EJS Templates
hgweb: refactor hgweb code
Dirkjan Ochtman -
r6393:894875ea default
parent child Browse files
Show More
@@ -1,98 +1,97
1 """
1 """
2 This is Mercurial extension for syntax highlighting in the file
2 This is Mercurial extension for syntax highlighting in the file
3 revision view of hgweb.
3 revision view of hgweb.
4
4
5 It depends on the pygments syntax highlighting library:
5 It depends on the pygments syntax highlighting library:
6 http://pygments.org/
6 http://pygments.org/
7
7
8 To enable the extension add this to hgrc:
8 To enable the extension add this to hgrc:
9
9
10 [extensions]
10 [extensions]
11 hgext.highlight =
11 hgext.highlight =
12
12
13 There is a single configuration option:
13 There is a single configuration option:
14
14
15 [web]
15 [web]
16 pygments_style = <style>
16 pygments_style = <style>
17
17
18 The default is 'colorful'. If this is changed the corresponding CSS
18 The default is 'colorful'. If this is changed the corresponding CSS
19 file should be re-generated by running
19 file should be re-generated by running
20
20
21 # pygmentize -f html -S <newstyle>
21 # pygmentize -f html -S <newstyle>
22
22
23
23
24 -- Adam Hupp <adam@hupp.org>
24 -- Adam Hupp <adam@hupp.org>
25
25
26
26
27 """
27 """
28
28
29 from mercurial import demandimport
29 from mercurial import demandimport
30 demandimport.ignore.extend(['pkgutil',
30 demandimport.ignore.extend(['pkgutil',
31 'pkg_resources',
31 'pkg_resources',
32 '__main__',])
32 '__main__',])
33
33
34 from mercurial.hgweb.hgweb_mod import hgweb
34 from mercurial.hgweb import webcommands, webutil
35 from mercurial import util
35 from mercurial import util
36 from mercurial.templatefilters import filters
36 from mercurial.templatefilters import filters
37
37
38 from pygments import highlight
38 from pygments import highlight
39 from pygments.util import ClassNotFound
39 from pygments.util import ClassNotFound
40 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
40 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
41 from pygments.formatters import HtmlFormatter
41 from pygments.formatters import HtmlFormatter
42
42
43 SYNTAX_CSS = ('\n<link rel="stylesheet" href="#staticurl#highlight.css" '
43 SYNTAX_CSS = ('\n<link rel="stylesheet" href="#staticurl#highlight.css" '
44 'type="text/css" />')
44 'type="text/css" />')
45
45
46 def pygmentize(self, tmpl, fctx, field):
46 def pygmentize(self, tmpl, fctx, field):
47 # append a <link ...> to the syntax highlighting css
47 # append a <link ...> to the syntax highlighting css
48 old_header = ''.join(tmpl('header'))
48 old_header = ''.join(tmpl('header'))
49 if SYNTAX_CSS not in old_header:
49 if SYNTAX_CSS not in old_header:
50 new_header = old_header + SYNTAX_CSS
50 new_header = old_header + SYNTAX_CSS
51 tmpl.cache['header'] = new_header
51 tmpl.cache['header'] = new_header
52
52
53 text = fctx.data()
53 text = fctx.data()
54 if util.binary(text):
54 if util.binary(text):
55 return
55 return
56
56
57 style = self.config("web", "pygments_style", "colorful")
57 style = self.config("web", "pygments_style", "colorful")
58 # To get multi-line strings right, we can't format line-by-line
58 # To get multi-line strings right, we can't format line-by-line
59 try:
59 try:
60 lexer = guess_lexer_for_filename(fctx.path(), text,
60 lexer = guess_lexer_for_filename(fctx.path(), text,
61 encoding=util._encoding)
61 encoding=util._encoding)
62 except ClassNotFound:
62 except ClassNotFound:
63 try:
63 try:
64 lexer = guess_lexer(text, encoding=util._encoding)
64 lexer = guess_lexer(text, encoding=util._encoding)
65 except ClassNotFound:
65 except ClassNotFound:
66 lexer = TextLexer(encoding=util._encoding)
66 lexer = TextLexer(encoding=util._encoding)
67
67
68 formatter = HtmlFormatter(style=style, encoding=util._encoding)
68 formatter = HtmlFormatter(style=style, encoding=util._encoding)
69
69
70 colorized = highlight(text, lexer, formatter)
70 colorized = highlight(text, lexer, formatter)
71 # strip wrapping div
71 # strip wrapping div
72 colorized = colorized[:colorized.find('\n</pre>')]
72 colorized = colorized[:colorized.find('\n</pre>')]
73 colorized = colorized[colorized.find('<pre>')+5:]
73 colorized = colorized[colorized.find('<pre>')+5:]
74 coloriter = iter(colorized.splitlines())
74 coloriter = iter(colorized.splitlines())
75
75
76 filters['colorize'] = lambda x: coloriter.next()
76 filters['colorize'] = lambda x: coloriter.next()
77
77
78 oldl = tmpl.cache[field]
78 oldl = tmpl.cache[field]
79 newl = oldl.replace('line|escape', 'line|colorize')
79 newl = oldl.replace('line|escape', 'line|colorize')
80 tmpl.cache[field] = newl
80 tmpl.cache[field] = newl
81
81
82 def filerevision_highlight(self, tmpl, fctx):
82 web_filerevision = webcommands._filerevision
83 pygmentize(self, tmpl, fctx, 'fileline')
83 web_annotate = webcommands.annotate
84
85 return realrevision(self, tmpl, fctx)
86
84
87 def fileannotate_highlight(self, tmpl, fctx):
85 def filerevision_highlight(web, tmpl, fctx):
88 pygmentize(self, tmpl, fctx, 'annotateline')
86 pygmentize(web, tmpl, fctx, 'fileline')
87 return web_filerevision(web, tmpl, fctx)
89
88
90 return realannotate(self, tmpl, fctx)
89 def annotate_highlight(web, req, tmpl):
90 fctx = webutil.filectx(web.repo, req)
91 pygmentize(web, tmpl, fctx, 'annotateline')
92 return web_annotate(web, req, tmpl)
91
93
92 # monkeypatch in the new version
94 # monkeypatch in the new version
93 # should be safer than overriding the method in a derived class
95
94 # and then patching the class
96 webcommands._filerevision = filerevision_highlight
95 realrevision = hgweb.filerevision
97 webcommands.annotate = annotate_highlight
96 hgweb.filerevision = filerevision_highlight
97 realannotate = hgweb.fileannotate
98 hgweb.fileannotate = fileannotate_highlight
@@ -1,556 +1,562
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a DSCM
10 # Keyword expansion hack against the grain of a DSCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an audience
14 # files (like LaTeX packages), that are mostly addressed to an audience
15 # not running a version control system.
15 # not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Setup in hgrc:
24 # Setup in hgrc:
25 #
25 #
26 # [extensions]
26 # [extensions]
27 # # enable extension
27 # # enable extension
28 # hgext.keyword =
28 # hgext.keyword =
29 #
29 #
30 # Files to act upon/ignore are specified in the [keyword] section.
30 # Files to act upon/ignore are specified in the [keyword] section.
31 # Customized keyword template mappings in the [keywordmaps] section.
31 # Customized keyword template mappings in the [keywordmaps] section.
32 #
32 #
33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34
34
35 '''keyword expansion in local repositories
35 '''keyword expansion in local repositories
36
36
37 This extension expands RCS/CVS-like or self-customized $Keywords$
37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 in tracked text files selected by your configuration.
38 in tracked text files selected by your configuration.
39
39
40 Keywords are only expanded in local repositories and not stored in
40 Keywords are only expanded in local repositories and not stored in
41 the change history. The mechanism can be regarded as a convenience
41 the change history. The mechanism can be regarded as a convenience
42 for the current user or for archive distribution.
42 for the current user or for archive distribution.
43
43
44 Configuration is done in the [keyword] and [keywordmaps] sections
44 Configuration is done in the [keyword] and [keywordmaps] sections
45 of hgrc files.
45 of hgrc files.
46
46
47 Example:
47 Example:
48
48
49 [keyword]
49 [keyword]
50 # expand keywords in every python file except those matching "x*"
50 # expand keywords in every python file except those matching "x*"
51 **.py =
51 **.py =
52 x* = ignore
52 x* = ignore
53
53
54 Note: the more specific you are in your filename patterns
54 Note: the more specific you are in your filename patterns
55 the less you lose speed in huge repos.
55 the less you lose speed in huge repos.
56
56
57 For [keywordmaps] template mapping and expansion demonstration and
57 For [keywordmaps] template mapping and expansion demonstration and
58 control run "hg kwdemo".
58 control run "hg kwdemo".
59
59
60 An additional date template filter {date|utcdate} is provided.
60 An additional date template filter {date|utcdate} is provided.
61
61
62 The default template mappings (view with "hg kwdemo -d") can be replaced
62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 with customized keywords and templates.
63 with customized keywords and templates.
64 Again, run "hg kwdemo" to control the results of your config changes.
64 Again, run "hg kwdemo" to control the results of your config changes.
65
65
66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 the risk of inadvertedly storing expanded keywords in the change history.
67 the risk of inadvertedly storing expanded keywords in the change history.
68
68
69 To force expansion after enabling it, or a configuration change, run
69 To force expansion after enabling it, or a configuration change, run
70 "hg kwexpand".
70 "hg kwexpand".
71
71
72 Also, when committing with the record extension or using mq's qrecord, be aware
72 Also, when committing with the record extension or using mq's qrecord, be aware
73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 question to update keyword expansions after all changes have been checked in.
74 question to update keyword expansions after all changes have been checked in.
75
75
76 Expansions spanning more than one line and incremental expansions,
76 Expansions spanning more than one line and incremental expansions,
77 like CVS' $Log$, are not supported. A keyword template map
77 like CVS' $Log$, are not supported. A keyword template map
78 "Log = {desc}" expands to the first line of the changeset description.
78 "Log = {desc}" expands to the first line of the changeset description.
79 '''
79 '''
80
80
81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
81 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
82 from mercurial import patch, localrepo, templater, templatefilters, util
82 from mercurial import patch, localrepo, templater, templatefilters, util
83 from mercurial.hgweb import webcommands
83 from mercurial.hgweb import webcommands
84 from mercurial.node import nullid, hex
84 from mercurial.node import nullid, hex
85 from mercurial.i18n import _
85 from mercurial.i18n import _
86 import re, shutil, tempfile, time
86 import re, shutil, tempfile, time
87
87
88 commands.optionalrepo += ' kwdemo'
88 commands.optionalrepo += ' kwdemo'
89
89
90 # hg commands that do not act on keywords
90 # hg commands that do not act on keywords
91 nokwcommands = ('add addremove bundle copy export grep incoming init'
91 nokwcommands = ('add addremove bundle copy export grep incoming init'
92 ' log outgoing push rename rollback tip'
92 ' log outgoing push rename rollback tip'
93 ' convert email glog')
93 ' convert email glog')
94
94
95 # hg commands that trigger expansion only when writing to working dir,
95 # hg commands that trigger expansion only when writing to working dir,
96 # not when reading filelog, and unexpand when reading from working dir
96 # not when reading filelog, and unexpand when reading from working dir
97 restricted = 'record qfold qimport qnew qpush qrefresh qrecord'
97 restricted = 'record qfold qimport qnew qpush qrefresh qrecord'
98
98
99 def utcdate(date):
99 def utcdate(date):
100 '''Returns hgdate in cvs-like UTC format.'''
100 '''Returns hgdate in cvs-like UTC format.'''
101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
102
102
103
103
104 # make keyword tools accessible
104 # make keyword tools accessible
105 kwtools = {'templater': None, 'hgcmd': None}
105 kwtools = {'templater': None, 'hgcmd': None}
106
106
107 # store originals of monkeypatches
107 # store originals of monkeypatches
108 _patchfile_init = patch.patchfile.__init__
108 _patchfile_init = patch.patchfile.__init__
109 _patch_diff = patch.diff
109 _patch_diff = patch.diff
110 _dispatch_parse = dispatch._parse
110 _dispatch_parse = dispatch._parse
111
111
112 def _kwpatchfile_init(self, ui, fname, missing=False):
112 def _kwpatchfile_init(self, ui, fname, missing=False):
113 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
113 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
114 rejects or conflicts due to expanded keywords in working dir.'''
114 rejects or conflicts due to expanded keywords in working dir.'''
115 _patchfile_init(self, ui, fname, missing=missing)
115 _patchfile_init(self, ui, fname, missing=missing)
116 # shrink keywords read from working dir
116 # shrink keywords read from working dir
117 kwt = kwtools['templater']
117 kwt = kwtools['templater']
118 self.lines = kwt.shrinklines(self.fname, self.lines)
118 self.lines = kwt.shrinklines(self.fname, self.lines)
119
119
120 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
120 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
121 fp=None, changes=None, opts=None):
121 fp=None, changes=None, opts=None):
122 '''Monkeypatch patch.diff to avoid expansion except when
122 '''Monkeypatch patch.diff to avoid expansion except when
123 comparing against working dir.'''
123 comparing against working dir.'''
124 if node2 is not None:
124 if node2 is not None:
125 kwtools['templater'].matcher = util.never
125 kwtools['templater'].matcher = util.never
126 elif node1 is not None and node1 != repo.changectx().node():
126 elif node1 is not None and node1 != repo.changectx().node():
127 kwtools['templater'].restrict = True
127 kwtools['templater'].restrict = True
128 _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
128 _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
129 fp=fp, changes=changes, opts=opts)
129 fp=fp, changes=changes, opts=opts)
130
130
131 # monkeypatching hgweb functions changeset and filediff
132 # actual monkeypatching is done at the bottom of reposetup()
133
134 web_changeset = webcommands.changeset
135 web_filediff = webcommands.filediff
136
131 def _kwweb_changeset(web, req, tmpl):
137 def _kwweb_changeset(web, req, tmpl):
132 '''Wraps webcommands.changeset turning off keyword expansion.'''
138 '''Wraps webcommands.changeset turning off keyword expansion.'''
133 kwtools['templater'].matcher = util.never
139 kwtools['templater'].matcher = util.never
134 return web.changeset(tmpl, web.changectx(req))
140 return web_changeset(web, req, tmpl)
135
141
136 def _kwweb_filediff(web, req, tmpl):
142 def _kwweb_filediff(web, req, tmpl):
137 '''Wraps webcommands.filediff turning off keyword expansion.'''
143 '''Wraps webcommands.filediff turning off keyword expansion.'''
138 kwtools['templater'].matcher = util.never
144 kwtools['templater'].matcher = util.never
139 return web.filediff(tmpl, web.filectx(req))
145 return web_filediff(web, req, tmpl)
140
146
141 def _kwdispatch_parse(ui, args):
147 def _kwdispatch_parse(ui, args):
142 '''Monkeypatch dispatch._parse to obtain running hg command.'''
148 '''Monkeypatch dispatch._parse to obtain running hg command.'''
143 cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
149 cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
144 kwtools['hgcmd'] = cmd
150 kwtools['hgcmd'] = cmd
145 return cmd, func, args, options, cmdoptions
151 return cmd, func, args, options, cmdoptions
146
152
147 # dispatch._parse is run before reposetup, so wrap it here
153 # dispatch._parse is run before reposetup, so wrap it here
148 dispatch._parse = _kwdispatch_parse
154 dispatch._parse = _kwdispatch_parse
149
155
150
156
151 class kwtemplater(object):
157 class kwtemplater(object):
152 '''
158 '''
153 Sets up keyword templates, corresponding keyword regex, and
159 Sets up keyword templates, corresponding keyword regex, and
154 provides keyword substitution functions.
160 provides keyword substitution functions.
155 '''
161 '''
156 templates = {
162 templates = {
157 'Revision': '{node|short}',
163 'Revision': '{node|short}',
158 'Author': '{author|user}',
164 'Author': '{author|user}',
159 'Date': '{date|utcdate}',
165 'Date': '{date|utcdate}',
160 'RCSFile': '{file|basename},v',
166 'RCSFile': '{file|basename},v',
161 'Source': '{root}/{file},v',
167 'Source': '{root}/{file},v',
162 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
168 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
163 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
169 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
164 }
170 }
165
171
166 def __init__(self, ui, repo, inc, exc):
172 def __init__(self, ui, repo, inc, exc):
167 self.ui = ui
173 self.ui = ui
168 self.repo = repo
174 self.repo = repo
169 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
175 self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
170 self.restrict = kwtools['hgcmd'] in restricted.split()
176 self.restrict = kwtools['hgcmd'] in restricted.split()
171
177
172 kwmaps = self.ui.configitems('keywordmaps')
178 kwmaps = self.ui.configitems('keywordmaps')
173 if kwmaps: # override default templates
179 if kwmaps: # override default templates
174 kwmaps = [(k, templater.parsestring(v, quoted=False))
180 kwmaps = [(k, templater.parsestring(v, quoted=False))
175 for (k, v) in kwmaps]
181 for (k, v) in kwmaps]
176 self.templates = dict(kwmaps)
182 self.templates = dict(kwmaps)
177 escaped = map(re.escape, self.templates.keys())
183 escaped = map(re.escape, self.templates.keys())
178 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
184 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
179 self.re_kw = re.compile(kwpat)
185 self.re_kw = re.compile(kwpat)
180
186
181 templatefilters.filters['utcdate'] = utcdate
187 templatefilters.filters['utcdate'] = utcdate
182 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
188 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
183 False, '', False)
189 False, '', False)
184
190
185 def getnode(self, path, fnode):
191 def getnode(self, path, fnode):
186 '''Derives changenode from file path and filenode.'''
192 '''Derives changenode from file path and filenode.'''
187 # used by kwfilelog.read and kwexpand
193 # used by kwfilelog.read and kwexpand
188 c = context.filectx(self.repo, path, fileid=fnode)
194 c = context.filectx(self.repo, path, fileid=fnode)
189 return c.node()
195 return c.node()
190
196
191 def substitute(self, data, path, node, subfunc):
197 def substitute(self, data, path, node, subfunc):
192 '''Replaces keywords in data with expanded template.'''
198 '''Replaces keywords in data with expanded template.'''
193 def kwsub(mobj):
199 def kwsub(mobj):
194 kw = mobj.group(1)
200 kw = mobj.group(1)
195 self.ct.use_template(self.templates[kw])
201 self.ct.use_template(self.templates[kw])
196 self.ui.pushbuffer()
202 self.ui.pushbuffer()
197 self.ct.show(changenode=node, root=self.repo.root, file=path)
203 self.ct.show(changenode=node, root=self.repo.root, file=path)
198 ekw = templatefilters.firstline(self.ui.popbuffer())
204 ekw = templatefilters.firstline(self.ui.popbuffer())
199 return '$%s: %s $' % (kw, ekw)
205 return '$%s: %s $' % (kw, ekw)
200 return subfunc(kwsub, data)
206 return subfunc(kwsub, data)
201
207
202 def expand(self, path, node, data):
208 def expand(self, path, node, data):
203 '''Returns data with keywords expanded.'''
209 '''Returns data with keywords expanded.'''
204 if not self.restrict and self.matcher(path) and not util.binary(data):
210 if not self.restrict and self.matcher(path) and not util.binary(data):
205 changenode = self.getnode(path, node)
211 changenode = self.getnode(path, node)
206 return self.substitute(data, path, changenode, self.re_kw.sub)
212 return self.substitute(data, path, changenode, self.re_kw.sub)
207 return data
213 return data
208
214
209 def iskwfile(self, path, islink):
215 def iskwfile(self, path, islink):
210 '''Returns true if path matches [keyword] pattern
216 '''Returns true if path matches [keyword] pattern
211 and is not a symbolic link.
217 and is not a symbolic link.
212 Caveat: localrepository._link fails on Windows.'''
218 Caveat: localrepository._link fails on Windows.'''
213 return self.matcher(path) and not islink(path)
219 return self.matcher(path) and not islink(path)
214
220
215 def overwrite(self, node=None, expand=True, files=None):
221 def overwrite(self, node=None, expand=True, files=None):
216 '''Overwrites selected files expanding/shrinking keywords.'''
222 '''Overwrites selected files expanding/shrinking keywords.'''
217 ctx = self.repo.changectx(node)
223 ctx = self.repo.changectx(node)
218 mf = ctx.manifest()
224 mf = ctx.manifest()
219 if node is not None: # commit
225 if node is not None: # commit
220 files = [f for f in ctx.files() if f in mf]
226 files = [f for f in ctx.files() if f in mf]
221 notify = self.ui.debug
227 notify = self.ui.debug
222 else: # kwexpand/kwshrink
228 else: # kwexpand/kwshrink
223 notify = self.ui.note
229 notify = self.ui.note
224 candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
230 candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
225 if candidates:
231 if candidates:
226 self.restrict = True # do not expand when reading
232 self.restrict = True # do not expand when reading
227 candidates.sort()
233 candidates.sort()
228 action = expand and 'expanding' or 'shrinking'
234 action = expand and 'expanding' or 'shrinking'
229 for f in candidates:
235 for f in candidates:
230 fp = self.repo.file(f)
236 fp = self.repo.file(f)
231 data = fp.read(mf[f])
237 data = fp.read(mf[f])
232 if util.binary(data):
238 if util.binary(data):
233 continue
239 continue
234 if expand:
240 if expand:
235 changenode = node or self.getnode(f, mf[f])
241 changenode = node or self.getnode(f, mf[f])
236 data, found = self.substitute(data, f, changenode,
242 data, found = self.substitute(data, f, changenode,
237 self.re_kw.subn)
243 self.re_kw.subn)
238 else:
244 else:
239 found = self.re_kw.search(data)
245 found = self.re_kw.search(data)
240 if found:
246 if found:
241 notify(_('overwriting %s %s keywords\n') % (f, action))
247 notify(_('overwriting %s %s keywords\n') % (f, action))
242 self.repo.wwrite(f, data, mf.flags(f))
248 self.repo.wwrite(f, data, mf.flags(f))
243 self.repo.dirstate.normal(f)
249 self.repo.dirstate.normal(f)
244 self.restrict = False
250 self.restrict = False
245
251
246 def shrinktext(self, text):
252 def shrinktext(self, text):
247 '''Unconditionally removes all keyword substitutions from text.'''
253 '''Unconditionally removes all keyword substitutions from text.'''
248 return self.re_kw.sub(r'$\1$', text)
254 return self.re_kw.sub(r'$\1$', text)
249
255
250 def shrink(self, fname, text):
256 def shrink(self, fname, text):
251 '''Returns text with all keyword substitutions removed.'''
257 '''Returns text with all keyword substitutions removed.'''
252 if self.matcher(fname) and not util.binary(text):
258 if self.matcher(fname) and not util.binary(text):
253 return self.shrinktext(text)
259 return self.shrinktext(text)
254 return text
260 return text
255
261
256 def shrinklines(self, fname, lines):
262 def shrinklines(self, fname, lines):
257 '''Returns lines with keyword substitutions removed.'''
263 '''Returns lines with keyword substitutions removed.'''
258 if self.matcher(fname):
264 if self.matcher(fname):
259 text = ''.join(lines)
265 text = ''.join(lines)
260 if not util.binary(text):
266 if not util.binary(text):
261 return self.shrinktext(text).splitlines(True)
267 return self.shrinktext(text).splitlines(True)
262 return lines
268 return lines
263
269
264 def wread(self, fname, data):
270 def wread(self, fname, data):
265 '''If in restricted mode returns data read from wdir with
271 '''If in restricted mode returns data read from wdir with
266 keyword substitutions removed.'''
272 keyword substitutions removed.'''
267 return self.restrict and self.shrink(fname, data) or data
273 return self.restrict and self.shrink(fname, data) or data
268
274
269 class kwfilelog(filelog.filelog):
275 class kwfilelog(filelog.filelog):
270 '''
276 '''
271 Subclass of filelog to hook into its read, add, cmp methods.
277 Subclass of filelog to hook into its read, add, cmp methods.
272 Keywords are "stored" unexpanded, and processed on reading.
278 Keywords are "stored" unexpanded, and processed on reading.
273 '''
279 '''
274 def __init__(self, opener, path):
280 def __init__(self, opener, path):
275 super(kwfilelog, self).__init__(opener, path)
281 super(kwfilelog, self).__init__(opener, path)
276 self.kwt = kwtools['templater']
282 self.kwt = kwtools['templater']
277 self.path = path
283 self.path = path
278
284
279 def read(self, node):
285 def read(self, node):
280 '''Expands keywords when reading filelog.'''
286 '''Expands keywords when reading filelog.'''
281 data = super(kwfilelog, self).read(node)
287 data = super(kwfilelog, self).read(node)
282 return self.kwt.expand(self.path, node, data)
288 return self.kwt.expand(self.path, node, data)
283
289
284 def add(self, text, meta, tr, link, p1=None, p2=None):
290 def add(self, text, meta, tr, link, p1=None, p2=None):
285 '''Removes keyword substitutions when adding to filelog.'''
291 '''Removes keyword substitutions when adding to filelog.'''
286 text = self.kwt.shrink(self.path, text)
292 text = self.kwt.shrink(self.path, text)
287 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
293 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
288
294
289 def cmp(self, node, text):
295 def cmp(self, node, text):
290 '''Removes keyword substitutions for comparison.'''
296 '''Removes keyword substitutions for comparison.'''
291 text = self.kwt.shrink(self.path, text)
297 text = self.kwt.shrink(self.path, text)
292 if self.renamed(node):
298 if self.renamed(node):
293 t2 = super(kwfilelog, self).read(node)
299 t2 = super(kwfilelog, self).read(node)
294 return t2 != text
300 return t2 != text
295 return revlog.revlog.cmp(self, node, text)
301 return revlog.revlog.cmp(self, node, text)
296
302
297 def _status(ui, repo, kwt, *pats, **opts):
303 def _status(ui, repo, kwt, *pats, **opts):
298 '''Bails out if [keyword] configuration is not active.
304 '''Bails out if [keyword] configuration is not active.
299 Returns status of working directory.'''
305 Returns status of working directory.'''
300 if kwt:
306 if kwt:
301 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
307 files, match, anypats = cmdutil.matchpats(repo, pats, opts)
302 return repo.status(files=files, match=match, list_clean=True)
308 return repo.status(files=files, match=match, list_clean=True)
303 if ui.configitems('keyword'):
309 if ui.configitems('keyword'):
304 raise util.Abort(_('[keyword] patterns cannot match'))
310 raise util.Abort(_('[keyword] patterns cannot match'))
305 raise util.Abort(_('no [keyword] patterns configured'))
311 raise util.Abort(_('no [keyword] patterns configured'))
306
312
307 def _kwfwrite(ui, repo, expand, *pats, **opts):
313 def _kwfwrite(ui, repo, expand, *pats, **opts):
308 '''Selects files and passes them to kwtemplater.overwrite.'''
314 '''Selects files and passes them to kwtemplater.overwrite.'''
309 kwt = kwtools['templater']
315 kwt = kwtools['templater']
310 status = _status(ui, repo, kwt, *pats, **opts)
316 status = _status(ui, repo, kwt, *pats, **opts)
311 modified, added, removed, deleted, unknown, ignored, clean = status
317 modified, added, removed, deleted, unknown, ignored, clean = status
312 if modified or added or removed or deleted:
318 if modified or added or removed or deleted:
313 raise util.Abort(_('outstanding uncommitted changes in given files'))
319 raise util.Abort(_('outstanding uncommitted changes in given files'))
314 wlock = lock = None
320 wlock = lock = None
315 try:
321 try:
316 wlock = repo.wlock()
322 wlock = repo.wlock()
317 lock = repo.lock()
323 lock = repo.lock()
318 kwt.overwrite(expand=expand, files=clean)
324 kwt.overwrite(expand=expand, files=clean)
319 finally:
325 finally:
320 del wlock, lock
326 del wlock, lock
321
327
322
328
323 def demo(ui, repo, *args, **opts):
329 def demo(ui, repo, *args, **opts):
324 '''print [keywordmaps] configuration and an expansion example
330 '''print [keywordmaps] configuration and an expansion example
325
331
326 Show current, custom, or default keyword template maps
332 Show current, custom, or default keyword template maps
327 and their expansion.
333 and their expansion.
328
334
329 Extend current configuration by specifying maps as arguments
335 Extend current configuration by specifying maps as arguments
330 and optionally by reading from an additional hgrc file.
336 and optionally by reading from an additional hgrc file.
331
337
332 Override current keyword template maps with "default" option.
338 Override current keyword template maps with "default" option.
333 '''
339 '''
334 def demostatus(stat):
340 def demostatus(stat):
335 ui.status(_('\n\t%s\n') % stat)
341 ui.status(_('\n\t%s\n') % stat)
336
342
337 def demoitems(section, items):
343 def demoitems(section, items):
338 ui.write('[%s]\n' % section)
344 ui.write('[%s]\n' % section)
339 for k, v in items:
345 for k, v in items:
340 ui.write('%s = %s\n' % (k, v))
346 ui.write('%s = %s\n' % (k, v))
341
347
342 msg = 'hg keyword config and expansion example'
348 msg = 'hg keyword config and expansion example'
343 kwstatus = 'current'
349 kwstatus = 'current'
344 fn = 'demo.txt'
350 fn = 'demo.txt'
345 branchname = 'demobranch'
351 branchname = 'demobranch'
346 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
352 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
347 ui.note(_('creating temporary repo at %s\n') % tmpdir)
353 ui.note(_('creating temporary repo at %s\n') % tmpdir)
348 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
354 repo = localrepo.localrepository(ui, path=tmpdir, create=True)
349 ui.setconfig('keyword', fn, '')
355 ui.setconfig('keyword', fn, '')
350 if args or opts.get('rcfile'):
356 if args or opts.get('rcfile'):
351 kwstatus = 'custom'
357 kwstatus = 'custom'
352 if opts.get('rcfile'):
358 if opts.get('rcfile'):
353 ui.readconfig(opts.get('rcfile'))
359 ui.readconfig(opts.get('rcfile'))
354 if opts.get('default'):
360 if opts.get('default'):
355 kwstatus = 'default'
361 kwstatus = 'default'
356 kwmaps = kwtemplater.templates
362 kwmaps = kwtemplater.templates
357 if ui.configitems('keywordmaps'):
363 if ui.configitems('keywordmaps'):
358 # override maps from optional rcfile
364 # override maps from optional rcfile
359 for k, v in kwmaps.iteritems():
365 for k, v in kwmaps.iteritems():
360 ui.setconfig('keywordmaps', k, v)
366 ui.setconfig('keywordmaps', k, v)
361 elif args:
367 elif args:
362 # simulate hgrc parsing
368 # simulate hgrc parsing
363 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
369 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
364 fp = repo.opener('hgrc', 'w')
370 fp = repo.opener('hgrc', 'w')
365 fp.writelines(rcmaps)
371 fp.writelines(rcmaps)
366 fp.close()
372 fp.close()
367 ui.readconfig(repo.join('hgrc'))
373 ui.readconfig(repo.join('hgrc'))
368 if not opts.get('default'):
374 if not opts.get('default'):
369 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
375 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
370 reposetup(ui, repo)
376 reposetup(ui, repo)
371 for k, v in ui.configitems('extensions'):
377 for k, v in ui.configitems('extensions'):
372 if k.endswith('keyword'):
378 if k.endswith('keyword'):
373 extension = '%s = %s' % (k, v)
379 extension = '%s = %s' % (k, v)
374 break
380 break
375 demostatus('config using %s keyword template maps' % kwstatus)
381 demostatus('config using %s keyword template maps' % kwstatus)
376 ui.write('[extensions]\n%s\n' % extension)
382 ui.write('[extensions]\n%s\n' % extension)
377 demoitems('keyword', ui.configitems('keyword'))
383 demoitems('keyword', ui.configitems('keyword'))
378 demoitems('keywordmaps', kwmaps.iteritems())
384 demoitems('keywordmaps', kwmaps.iteritems())
379 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
385 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
380 repo.wopener(fn, 'w').write(keywords)
386 repo.wopener(fn, 'w').write(keywords)
381 repo.add([fn])
387 repo.add([fn])
382 path = repo.wjoin(fn)
388 path = repo.wjoin(fn)
383 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
389 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
384 ui.note(keywords)
390 ui.note(keywords)
385 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
391 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
386 # silence branch command if not verbose
392 # silence branch command if not verbose
387 quiet = ui.quiet
393 quiet = ui.quiet
388 ui.quiet = not ui.verbose
394 ui.quiet = not ui.verbose
389 commands.branch(ui, repo, branchname)
395 commands.branch(ui, repo, branchname)
390 ui.quiet = quiet
396 ui.quiet = quiet
391 for name, cmd in ui.configitems('hooks'):
397 for name, cmd in ui.configitems('hooks'):
392 if name.split('.', 1)[0].find('commit') > -1:
398 if name.split('.', 1)[0].find('commit') > -1:
393 repo.ui.setconfig('hooks', name, '')
399 repo.ui.setconfig('hooks', name, '')
394 ui.note(_('unhooked all commit hooks\n'))
400 ui.note(_('unhooked all commit hooks\n'))
395 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
401 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
396 repo.commit(text=msg)
402 repo.commit(text=msg)
397 format = ui.verbose and ' in %s' % path or ''
403 format = ui.verbose and ' in %s' % path or ''
398 demostatus('%s keywords expanded%s' % (kwstatus, format))
404 demostatus('%s keywords expanded%s' % (kwstatus, format))
399 ui.write(repo.wread(fn))
405 ui.write(repo.wread(fn))
400 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
406 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
401 shutil.rmtree(tmpdir, ignore_errors=True)
407 shutil.rmtree(tmpdir, ignore_errors=True)
402
408
403 def expand(ui, repo, *pats, **opts):
409 def expand(ui, repo, *pats, **opts):
404 '''expand keywords in working directory
410 '''expand keywords in working directory
405
411
406 Run after (re)enabling keyword expansion.
412 Run after (re)enabling keyword expansion.
407
413
408 kwexpand refuses to run if given files contain local changes.
414 kwexpand refuses to run if given files contain local changes.
409 '''
415 '''
410 # 3rd argument sets expansion to True
416 # 3rd argument sets expansion to True
411 _kwfwrite(ui, repo, True, *pats, **opts)
417 _kwfwrite(ui, repo, True, *pats, **opts)
412
418
413 def files(ui, repo, *pats, **opts):
419 def files(ui, repo, *pats, **opts):
414 '''print files currently configured for keyword expansion
420 '''print files currently configured for keyword expansion
415
421
416 Crosscheck which files in working directory are potential targets for
422 Crosscheck which files in working directory are potential targets for
417 keyword expansion.
423 keyword expansion.
418 That is, files matched by [keyword] config patterns but not symlinks.
424 That is, files matched by [keyword] config patterns but not symlinks.
419 '''
425 '''
420 kwt = kwtools['templater']
426 kwt = kwtools['templater']
421 status = _status(ui, repo, kwt, *pats, **opts)
427 status = _status(ui, repo, kwt, *pats, **opts)
422 modified, added, removed, deleted, unknown, ignored, clean = status
428 modified, added, removed, deleted, unknown, ignored, clean = status
423 files = modified + added + clean
429 files = modified + added + clean
424 if opts.get('untracked'):
430 if opts.get('untracked'):
425 files += unknown
431 files += unknown
426 files.sort()
432 files.sort()
427 wctx = repo.workingctx()
433 wctx = repo.workingctx()
428 islink = lambda p: 'l' in wctx.fileflags(p)
434 islink = lambda p: 'l' in wctx.fileflags(p)
429 kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
435 kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
430 cwd = pats and repo.getcwd() or ''
436 cwd = pats and repo.getcwd() or ''
431 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
437 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
432 if opts.get('all') or opts.get('ignore'):
438 if opts.get('all') or opts.get('ignore'):
433 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
439 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
434 for char, filenames in kwfstats:
440 for char, filenames in kwfstats:
435 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
441 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
436 for f in filenames:
442 for f in filenames:
437 ui.write(format % repo.pathto(f, cwd))
443 ui.write(format % repo.pathto(f, cwd))
438
444
439 def shrink(ui, repo, *pats, **opts):
445 def shrink(ui, repo, *pats, **opts):
440 '''revert expanded keywords in working directory
446 '''revert expanded keywords in working directory
441
447
442 Run before changing/disabling active keywords
448 Run before changing/disabling active keywords
443 or if you experience problems with "hg import" or "hg merge".
449 or if you experience problems with "hg import" or "hg merge".
444
450
445 kwshrink refuses to run if given files contain local changes.
451 kwshrink refuses to run if given files contain local changes.
446 '''
452 '''
447 # 3rd argument sets expansion to False
453 # 3rd argument sets expansion to False
448 _kwfwrite(ui, repo, False, *pats, **opts)
454 _kwfwrite(ui, repo, False, *pats, **opts)
449
455
450
456
451 def reposetup(ui, repo):
457 def reposetup(ui, repo):
452 '''Sets up repo as kwrepo for keyword substitution.
458 '''Sets up repo as kwrepo for keyword substitution.
453 Overrides file method to return kwfilelog instead of filelog
459 Overrides file method to return kwfilelog instead of filelog
454 if file matches user configuration.
460 if file matches user configuration.
455 Wraps commit to overwrite configured files with updated
461 Wraps commit to overwrite configured files with updated
456 keyword substitutions.
462 keyword substitutions.
457 This is done for local repos only, and only if there are
463 This is done for local repos only, and only if there are
458 files configured at all for keyword substitution.'''
464 files configured at all for keyword substitution.'''
459
465
460 try:
466 try:
461 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
467 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
462 or '.hg' in util.splitpath(repo.root)
468 or '.hg' in util.splitpath(repo.root)
463 or repo._url.startswith('bundle:')):
469 or repo._url.startswith('bundle:')):
464 return
470 return
465 except AttributeError:
471 except AttributeError:
466 pass
472 pass
467
473
468 inc, exc = [], ['.hg*']
474 inc, exc = [], ['.hg*']
469 for pat, opt in ui.configitems('keyword'):
475 for pat, opt in ui.configitems('keyword'):
470 if opt != 'ignore':
476 if opt != 'ignore':
471 inc.append(pat)
477 inc.append(pat)
472 else:
478 else:
473 exc.append(pat)
479 exc.append(pat)
474 if not inc:
480 if not inc:
475 return
481 return
476
482
477 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
483 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
478
484
479 class kwrepo(repo.__class__):
485 class kwrepo(repo.__class__):
480 def file(self, f):
486 def file(self, f):
481 if f[0] == '/':
487 if f[0] == '/':
482 f = f[1:]
488 f = f[1:]
483 return kwfilelog(self.sopener, f)
489 return kwfilelog(self.sopener, f)
484
490
485 def wread(self, filename):
491 def wread(self, filename):
486 data = super(kwrepo, self).wread(filename)
492 data = super(kwrepo, self).wread(filename)
487 return kwt.wread(filename, data)
493 return kwt.wread(filename, data)
488
494
489 def commit(self, files=None, text='', user=None, date=None,
495 def commit(self, files=None, text='', user=None, date=None,
490 match=util.always, force=False, force_editor=False,
496 match=util.always, force=False, force_editor=False,
491 p1=None, p2=None, extra={}, empty_ok=False):
497 p1=None, p2=None, extra={}, empty_ok=False):
492 wlock = lock = None
498 wlock = lock = None
493 _p1 = _p2 = None
499 _p1 = _p2 = None
494 try:
500 try:
495 wlock = self.wlock()
501 wlock = self.wlock()
496 lock = self.lock()
502 lock = self.lock()
497 # store and postpone commit hooks
503 # store and postpone commit hooks
498 commithooks = {}
504 commithooks = {}
499 for name, cmd in ui.configitems('hooks'):
505 for name, cmd in ui.configitems('hooks'):
500 if name.split('.', 1)[0] == 'commit':
506 if name.split('.', 1)[0] == 'commit':
501 commithooks[name] = cmd
507 commithooks[name] = cmd
502 ui.setconfig('hooks', name, None)
508 ui.setconfig('hooks', name, None)
503 if commithooks:
509 if commithooks:
504 # store parents for commit hook environment
510 # store parents for commit hook environment
505 if p1 is None:
511 if p1 is None:
506 _p1, _p2 = repo.dirstate.parents()
512 _p1, _p2 = repo.dirstate.parents()
507 else:
513 else:
508 _p1, _p2 = p1, p2 or nullid
514 _p1, _p2 = p1, p2 or nullid
509 _p1 = hex(_p1)
515 _p1 = hex(_p1)
510 if _p2 == nullid:
516 if _p2 == nullid:
511 _p2 = ''
517 _p2 = ''
512 else:
518 else:
513 _p2 = hex(_p2)
519 _p2 = hex(_p2)
514
520
515 node = super(kwrepo,
521 node = super(kwrepo,
516 self).commit(files=files, text=text, user=user,
522 self).commit(files=files, text=text, user=user,
517 date=date, match=match, force=force,
523 date=date, match=match, force=force,
518 force_editor=force_editor,
524 force_editor=force_editor,
519 p1=p1, p2=p2, extra=extra,
525 p1=p1, p2=p2, extra=extra,
520 empty_ok=empty_ok)
526 empty_ok=empty_ok)
521
527
522 # restore commit hooks
528 # restore commit hooks
523 for name, cmd in commithooks.iteritems():
529 for name, cmd in commithooks.iteritems():
524 ui.setconfig('hooks', name, cmd)
530 ui.setconfig('hooks', name, cmd)
525 if node is not None:
531 if node is not None:
526 kwt.overwrite(node=node)
532 kwt.overwrite(node=node)
527 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
533 repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
528 return node
534 return node
529 finally:
535 finally:
530 del wlock, lock
536 del wlock, lock
531
537
532 repo.__class__ = kwrepo
538 repo.__class__ = kwrepo
533 patch.patchfile.__init__ = _kwpatchfile_init
539 patch.patchfile.__init__ = _kwpatchfile_init
534 patch.diff = _kw_diff
540 patch.diff = _kw_diff
535 webcommands.changeset = webcommands.rev = _kwweb_changeset
541 webcommands.changeset = webcommands.rev = _kwweb_changeset
536 webcommands.filediff = webcommands.diff = _kwweb_filediff
542 webcommands.filediff = webcommands.diff = _kwweb_filediff
537
543
538
544
539 cmdtable = {
545 cmdtable = {
540 'kwdemo':
546 'kwdemo':
541 (demo,
547 (demo,
542 [('d', 'default', None, _('show default keyword template maps')),
548 [('d', 'default', None, _('show default keyword template maps')),
543 ('f', 'rcfile', [], _('read maps from rcfile'))],
549 ('f', 'rcfile', [], _('read maps from rcfile'))],
544 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
550 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
545 'kwexpand': (expand, commands.walkopts,
551 'kwexpand': (expand, commands.walkopts,
546 _('hg kwexpand [OPTION]... [FILE]...')),
552 _('hg kwexpand [OPTION]... [FILE]...')),
547 'kwfiles':
553 'kwfiles':
548 (files,
554 (files,
549 [('a', 'all', None, _('show keyword status flags of all files')),
555 [('a', 'all', None, _('show keyword status flags of all files')),
550 ('i', 'ignore', None, _('show files excluded from expansion')),
556 ('i', 'ignore', None, _('show files excluded from expansion')),
551 ('u', 'untracked', None, _('additionally show untracked files')),
557 ('u', 'untracked', None, _('additionally show untracked files')),
552 ] + commands.walkopts,
558 ] + commands.walkopts,
553 _('hg kwfiles [OPTION]... [FILE]...')),
559 _('hg kwfiles [OPTION]... [FILE]...')),
554 'kwshrink': (shrink, commands.walkopts,
560 'kwshrink': (shrink, commands.walkopts,
555 _('hg kwshrink [OPTION]... [FILE]...')),
561 _('hg kwshrink [OPTION]... [FILE]...')),
556 }
562 }
This diff has been collapsed as it changes many lines, (521 lines changed) Show them Hide them
@@ -1,892 +1,379
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes, re
9 import os, mimetypes
10 from mercurial.node import hex, nullid, short
10 from mercurial.node import hex, nullid
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import mdiff, ui, hg, util, archival, patch, hook
12 from mercurial import mdiff, ui, hg, util, patch, hook
13 from mercurial import revlog, templater, templatefilters, changegroup
13 from mercurial import revlog, templater, templatefilters, changegroup
14 from common import get_mtime, style_map, paritygen, countgen, get_contact
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
15 from common import ErrorResponse
16 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 from request import wsgirequest
16 from request import wsgirequest
18 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
19
18
20 shortcuts = {
19 shortcuts = {
21 'cl': [('cmd', ['changelog']), ('rev', None)],
20 'cl': [('cmd', ['changelog']), ('rev', None)],
22 'sl': [('cmd', ['shortlog']), ('rev', None)],
21 'sl': [('cmd', ['shortlog']), ('rev', None)],
23 'cs': [('cmd', ['changeset']), ('node', None)],
22 'cs': [('cmd', ['changeset']), ('node', None)],
24 'f': [('cmd', ['file']), ('filenode', None)],
23 'f': [('cmd', ['file']), ('filenode', None)],
25 'fl': [('cmd', ['filelog']), ('filenode', None)],
24 'fl': [('cmd', ['filelog']), ('filenode', None)],
26 'fd': [('cmd', ['filediff']), ('node', None)],
25 'fd': [('cmd', ['filediff']), ('node', None)],
27 'fa': [('cmd', ['annotate']), ('filenode', None)],
26 'fa': [('cmd', ['annotate']), ('filenode', None)],
28 'mf': [('cmd', ['manifest']), ('manifest', None)],
27 'mf': [('cmd', ['manifest']), ('manifest', None)],
29 'ca': [('cmd', ['archive']), ('node', None)],
28 'ca': [('cmd', ['archive']), ('node', None)],
30 'tags': [('cmd', ['tags'])],
29 'tags': [('cmd', ['tags'])],
31 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
30 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
32 'static': [('cmd', ['static']), ('file', None)]
31 'static': [('cmd', ['static']), ('file', None)]
33 }
32 }
34
33
35 def _up(p):
36 if p[0] != "/":
37 p = "/" + p
38 if p[-1] == "/":
39 p = p[:-1]
40 up = os.path.dirname(p)
41 if up == "/":
42 return "/"
43 return up + "/"
44
45 def revnavgen(pos, pagelen, limit, nodefunc):
46 def seq(factor, limit=None):
47 if limit:
48 yield limit
49 if limit >= 20 and limit <= 40:
50 yield 50
51 else:
52 yield 1 * factor
53 yield 3 * factor
54 for f in seq(factor * 10):
55 yield f
56
57 def nav(**map):
58 l = []
59 last = 0
60 for f in seq(1, pagelen):
61 if f < pagelen or f <= last:
62 continue
63 if f > limit:
64 break
65 last = f
66 if pos + f < limit:
67 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
68 if pos - f >= 0:
69 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
70
71 try:
72 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
73
74 for label, node in l:
75 yield {"label": label, "node": node}
76
77 yield {"label": "tip", "node": "tip"}
78 except RepoError:
79 pass
80
81 return nav
82
83 class hgweb(object):
34 class hgweb(object):
84 def __init__(self, repo, name=None):
35 def __init__(self, repo, name=None):
85 if isinstance(repo, str):
36 if isinstance(repo, str):
86 parentui = ui.ui(report_untrusted=False, interactive=False)
37 parentui = ui.ui(report_untrusted=False, interactive=False)
87 self.repo = hg.repository(parentui, repo)
38 self.repo = hg.repository(parentui, repo)
88 else:
39 else:
89 self.repo = repo
40 self.repo = repo
90
41
91 hook.redirect(True)
42 hook.redirect(True)
92 self.mtime = -1
43 self.mtime = -1
93 self.reponame = name
44 self.reponame = name
94 self.archives = 'zip', 'gz', 'bz2'
45 self.archives = 'zip', 'gz', 'bz2'
95 self.stripecount = 1
46 self.stripecount = 1
96 self._capabilities = None
47 self._capabilities = None
97 # a repo owner may set web.templates in .hg/hgrc to get any file
48 # a repo owner may set web.templates in .hg/hgrc to get any file
98 # readable by the user running the CGI script
49 # readable by the user running the CGI script
99 self.templatepath = self.config("web", "templates",
50 self.templatepath = self.config("web", "templates",
100 templater.templatepath(),
51 templater.templatepath(),
101 untrusted=False)
52 untrusted=False)
102
53
103 # The CGI scripts are often run by a user different from the repo owner.
54 # The CGI scripts are often run by a user different from the repo owner.
104 # Trust the settings from the .hg/hgrc files by default.
55 # Trust the settings from the .hg/hgrc files by default.
105 def config(self, section, name, default=None, untrusted=True):
56 def config(self, section, name, default=None, untrusted=True):
106 return self.repo.ui.config(section, name, default,
57 return self.repo.ui.config(section, name, default,
107 untrusted=untrusted)
58 untrusted=untrusted)
108
59
109 def configbool(self, section, name, default=False, untrusted=True):
60 def configbool(self, section, name, default=False, untrusted=True):
110 return self.repo.ui.configbool(section, name, default,
61 return self.repo.ui.configbool(section, name, default,
111 untrusted=untrusted)
62 untrusted=untrusted)
112
63
113 def configlist(self, section, name, default=None, untrusted=True):
64 def configlist(self, section, name, default=None, untrusted=True):
114 return self.repo.ui.configlist(section, name, default,
65 return self.repo.ui.configlist(section, name, default,
115 untrusted=untrusted)
66 untrusted=untrusted)
116
67
117 def refresh(self):
68 def refresh(self):
118 mtime = get_mtime(self.repo.root)
69 mtime = get_mtime(self.repo.root)
119 if mtime != self.mtime:
70 if mtime != self.mtime:
120 self.mtime = mtime
71 self.mtime = mtime
121 self.repo = hg.repository(self.repo.ui, self.repo.root)
72 self.repo = hg.repository(self.repo.ui, self.repo.root)
122 self.maxchanges = int(self.config("web", "maxchanges", 10))
73 self.maxchanges = int(self.config("web", "maxchanges", 10))
123 self.stripecount = int(self.config("web", "stripes", 1))
74 self.stripecount = int(self.config("web", "stripes", 1))
124 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
75 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
125 self.maxfiles = int(self.config("web", "maxfiles", 10))
76 self.maxfiles = int(self.config("web", "maxfiles", 10))
126 self.allowpull = self.configbool("web", "allowpull", True)
77 self.allowpull = self.configbool("web", "allowpull", True)
127 self.encoding = self.config("web", "encoding", util._encoding)
78 self.encoding = self.config("web", "encoding", util._encoding)
128 self._capabilities = None
79 self._capabilities = None
129
80
130 def capabilities(self):
81 def capabilities(self):
131 if self._capabilities is not None:
82 if self._capabilities is not None:
132 return self._capabilities
83 return self._capabilities
133 caps = ['lookup', 'changegroupsubset']
84 caps = ['lookup', 'changegroupsubset']
134 if self.configbool('server', 'uncompressed'):
85 if self.configbool('server', 'uncompressed'):
135 caps.append('stream=%d' % self.repo.changelog.version)
86 caps.append('stream=%d' % self.repo.changelog.version)
136 if changegroup.bundlepriority:
87 if changegroup.bundlepriority:
137 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
88 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
138 self._capabilities = caps
89 self._capabilities = caps
139 return caps
90 return caps
140
91
141 def run(self):
92 def run(self):
142 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
93 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
143 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
94 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
144 import mercurial.hgweb.wsgicgi as wsgicgi
95 import mercurial.hgweb.wsgicgi as wsgicgi
145 wsgicgi.launch(self)
96 wsgicgi.launch(self)
146
97
147 def __call__(self, env, respond):
98 def __call__(self, env, respond):
148 req = wsgirequest(env, respond)
99 req = wsgirequest(env, respond)
149 self.run_wsgi(req)
100 self.run_wsgi(req)
150 return req
101 return req
151
102
152 def run_wsgi(self, req):
103 def run_wsgi(self, req):
153
104
154 self.refresh()
105 self.refresh()
155
106
156 # expand form shortcuts
107 # expand form shortcuts
157
108
158 for k in shortcuts.iterkeys():
109 for k in shortcuts.iterkeys():
159 if k in req.form:
110 if k in req.form:
160 for name, value in shortcuts[k]:
111 for name, value in shortcuts[k]:
161 if value is None:
112 if value is None:
162 value = req.form[k]
113 value = req.form[k]
163 req.form[name] = value
114 req.form[name] = value
164 del req.form[k]
115 del req.form[k]
165
116
166 # work with CGI variables to create coherent structure
117 # work with CGI variables to create coherent structure
167 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
118 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
168
119
169 req.url = req.env['SCRIPT_NAME']
120 req.url = req.env['SCRIPT_NAME']
170 if not req.url.endswith('/'):
121 if not req.url.endswith('/'):
171 req.url += '/'
122 req.url += '/'
172 if 'REPO_NAME' in req.env:
123 if 'REPO_NAME' in req.env:
173 req.url += req.env['REPO_NAME'] + '/'
124 req.url += req.env['REPO_NAME'] + '/'
174
125
175 if req.env.get('PATH_INFO'):
126 if req.env.get('PATH_INFO'):
176 parts = req.env.get('PATH_INFO').strip('/').split('/')
127 parts = req.env.get('PATH_INFO').strip('/').split('/')
177 repo_parts = req.env.get('REPO_NAME', '').split('/')
128 repo_parts = req.env.get('REPO_NAME', '').split('/')
178 if parts[:len(repo_parts)] == repo_parts:
129 if parts[:len(repo_parts)] == repo_parts:
179 parts = parts[len(repo_parts):]
130 parts = parts[len(repo_parts):]
180 query = '/'.join(parts)
131 query = '/'.join(parts)
181 else:
132 else:
182 query = req.env['QUERY_STRING'].split('&', 1)[0]
133 query = req.env['QUERY_STRING'].split('&', 1)[0]
183 query = query.split(';', 1)[0]
134 query = query.split(';', 1)[0]
184
135
185 # translate user-visible url structure to internal structure
136 # translate user-visible url structure to internal structure
186
137
187 args = query.split('/', 2)
138 args = query.split('/', 2)
188 if 'cmd' not in req.form and args and args[0]:
139 if 'cmd' not in req.form and args and args[0]:
189
140
190 cmd = args.pop(0)
141 cmd = args.pop(0)
191 style = cmd.rfind('-')
142 style = cmd.rfind('-')
192 if style != -1:
143 if style != -1:
193 req.form['style'] = [cmd[:style]]
144 req.form['style'] = [cmd[:style]]
194 cmd = cmd[style+1:]
145 cmd = cmd[style+1:]
195
146
196 # avoid accepting e.g. style parameter as command
147 # avoid accepting e.g. style parameter as command
197 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
148 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
198 req.form['cmd'] = [cmd]
149 req.form['cmd'] = [cmd]
199
150
200 if args and args[0]:
151 if args and args[0]:
201 node = args.pop(0)
152 node = args.pop(0)
202 req.form['node'] = [node]
153 req.form['node'] = [node]
203 if args:
154 if args:
204 req.form['file'] = args
155 req.form['file'] = args
205
156
206 if cmd == 'static':
157 if cmd == 'static':
207 req.form['file'] = req.form['node']
158 req.form['file'] = req.form['node']
208 elif cmd == 'archive':
159 elif cmd == 'archive':
209 fn = req.form['node'][0]
160 fn = req.form['node'][0]
210 for type_, spec in self.archive_specs.iteritems():
161 for type_, spec in self.archive_specs.iteritems():
211 ext = spec[2]
162 ext = spec[2]
212 if fn.endswith(ext):
163 if fn.endswith(ext):
213 req.form['node'] = [fn[:-len(ext)]]
164 req.form['node'] = [fn[:-len(ext)]]
214 req.form['type'] = [type_]
165 req.form['type'] = [type_]
215
166
216 # process this if it's a protocol request
167 # process this if it's a protocol request
217
168
218 cmd = req.form.get('cmd', [''])[0]
169 cmd = req.form.get('cmd', [''])[0]
219 if cmd in protocol.__all__:
170 if cmd in protocol.__all__:
220 method = getattr(protocol, cmd)
171 method = getattr(protocol, cmd)
221 method(self, req)
172 method(self, req)
222 return
173 return
223
174
224 # process the web interface request
175 # process the web interface request
225
176
226 try:
177 try:
227
178
228 tmpl = self.templater(req)
179 tmpl = self.templater(req)
229 ctype = tmpl('mimetype', encoding=self.encoding)
180 ctype = tmpl('mimetype', encoding=self.encoding)
230 ctype = templater.stringify(ctype)
181 ctype = templater.stringify(ctype)
231
182
232 if cmd == '':
183 if cmd == '':
233 req.form['cmd'] = [tmpl.cache['default']]
184 req.form['cmd'] = [tmpl.cache['default']]
234 cmd = req.form['cmd'][0]
185 cmd = req.form['cmd'][0]
235
186
236 if cmd not in webcommands.__all__:
187 if cmd not in webcommands.__all__:
237 msg = 'no such method: %s' % cmd
188 msg = 'no such method: %s' % cmd
238 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
189 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
239 elif cmd == 'file' and 'raw' in req.form.get('style', []):
190 elif cmd == 'file' and 'raw' in req.form.get('style', []):
240 self.ctype = ctype
191 self.ctype = ctype
241 content = webcommands.rawfile(self, req, tmpl)
192 content = webcommands.rawfile(self, req, tmpl)
242 else:
193 else:
243 content = getattr(webcommands, cmd)(self, req, tmpl)
194 content = getattr(webcommands, cmd)(self, req, tmpl)
244 req.respond(HTTP_OK, ctype)
195 req.respond(HTTP_OK, ctype)
245
196
246 req.write(content)
197 req.write(content)
247 del tmpl
198 del tmpl
248
199
249 except revlog.LookupError, err:
200 except revlog.LookupError, err:
250 req.respond(HTTP_NOT_FOUND, ctype)
201 req.respond(HTTP_NOT_FOUND, ctype)
251 msg = str(err)
202 msg = str(err)
252 if 'manifest' not in msg:
203 if 'manifest' not in msg:
253 msg = 'revision not found: %s' % err.name
204 msg = 'revision not found: %s' % err.name
254 req.write(tmpl('error', error=msg))
205 req.write(tmpl('error', error=msg))
255 except (RepoError, revlog.RevlogError), inst:
206 except (RepoError, revlog.RevlogError), inst:
256 req.respond(HTTP_SERVER_ERROR, ctype)
207 req.respond(HTTP_SERVER_ERROR, ctype)
257 req.write(tmpl('error', error=str(inst)))
208 req.write(tmpl('error', error=str(inst)))
258 except ErrorResponse, inst:
209 except ErrorResponse, inst:
259 req.respond(inst.code, ctype)
210 req.respond(inst.code, ctype)
260 req.write(tmpl('error', error=inst.message))
211 req.write(tmpl('error', error=inst.message))
261
212
262 def templater(self, req):
213 def templater(self, req):
263
214
264 # determine scheme, port and server name
215 # determine scheme, port and server name
265 # this is needed to create absolute urls
216 # this is needed to create absolute urls
266
217
267 proto = req.env.get('wsgi.url_scheme')
218 proto = req.env.get('wsgi.url_scheme')
268 if proto == 'https':
219 if proto == 'https':
269 proto = 'https'
220 proto = 'https'
270 default_port = "443"
221 default_port = "443"
271 else:
222 else:
272 proto = 'http'
223 proto = 'http'
273 default_port = "80"
224 default_port = "80"
274
225
275 port = req.env["SERVER_PORT"]
226 port = req.env["SERVER_PORT"]
276 port = port != default_port and (":" + port) or ""
227 port = port != default_port and (":" + port) or ""
277 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
228 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
278 staticurl = self.config("web", "staticurl") or req.url + 'static/'
229 staticurl = self.config("web", "staticurl") or req.url + 'static/'
279 if not staticurl.endswith('/'):
230 if not staticurl.endswith('/'):
280 staticurl += '/'
231 staticurl += '/'
281
232
282 # some functions for the templater
233 # some functions for the templater
283
234
284 def header(**map):
235 def header(**map):
285 yield tmpl('header', encoding=self.encoding, **map)
236 yield tmpl('header', encoding=self.encoding, **map)
286
237
287 def footer(**map):
238 def footer(**map):
288 yield tmpl("footer", **map)
239 yield tmpl("footer", **map)
289
240
290 def motd(**map):
241 def motd(**map):
291 yield self.config("web", "motd", "")
242 yield self.config("web", "motd", "")
292
243
293 def sessionvars(**map):
244 def sessionvars(**map):
294 fields = []
245 fields = []
295 if 'style' in req.form:
246 if 'style' in req.form:
296 style = req.form['style'][0]
247 style = req.form['style'][0]
297 if style != self.config('web', 'style', ''):
248 if style != self.config('web', 'style', ''):
298 fields.append(('style', style))
249 fields.append(('style', style))
299
250
300 separator = req.url[-1] == '?' and ';' or '?'
251 separator = req.url[-1] == '?' and ';' or '?'
301 for name, value in fields:
252 for name, value in fields:
302 yield dict(name=name, value=value, separator=separator)
253 yield dict(name=name, value=value, separator=separator)
303 separator = ';'
254 separator = ';'
304
255
305 # figure out which style to use
256 # figure out which style to use
306
257
307 style = self.config("web", "style", "")
258 style = self.config("web", "style", "")
308 if 'style' in req.form:
259 if 'style' in req.form:
309 style = req.form['style'][0]
260 style = req.form['style'][0]
310 mapfile = style_map(self.templatepath, style)
261 mapfile = style_map(self.templatepath, style)
311
262
312 if not self.reponame:
263 if not self.reponame:
313 self.reponame = (self.config("web", "name")
264 self.reponame = (self.config("web", "name")
314 or req.env.get('REPO_NAME')
265 or req.env.get('REPO_NAME')
315 or req.url.strip('/') or self.repo.root)
266 or req.url.strip('/') or self.repo.root)
316
267
317 # create the templater
268 # create the templater
318
269
319 tmpl = templater.templater(mapfile, templatefilters.filters,
270 tmpl = templater.templater(mapfile, templatefilters.filters,
320 defaults={"url": req.url,
271 defaults={"url": req.url,
321 "staticurl": staticurl,
272 "staticurl": staticurl,
322 "urlbase": urlbase,
273 "urlbase": urlbase,
323 "repo": self.reponame,
274 "repo": self.reponame,
324 "header": header,
275 "header": header,
325 "footer": footer,
276 "footer": footer,
326 "motd": motd,
277 "motd": motd,
327 "sessionvars": sessionvars
278 "sessionvars": sessionvars
328 })
279 })
329 return tmpl
280 return tmpl
330
281
331 def archivelist(self, nodeid):
282 def archivelist(self, nodeid):
332 allowed = self.configlist("web", "allow_archive")
283 allowed = self.configlist("web", "allow_archive")
333 for i, spec in self.archive_specs.iteritems():
284 for i, spec in self.archive_specs.iteritems():
334 if i in allowed or self.configbool("web", "allow" + i):
285 if i in allowed or self.configbool("web", "allow" + i):
335 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
286 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
336
287
337 def listfilediffs(self, tmpl, files, changeset):
288 def listfilediffs(self, tmpl, files, changeset):
338 for f in files[:self.maxfiles]:
289 for f in files[:self.maxfiles]:
339 yield tmpl("filedifflink", node=hex(changeset), file=f)
290 yield tmpl("filedifflink", node=hex(changeset), file=f)
340 if len(files) > self.maxfiles:
291 if len(files) > self.maxfiles:
341 yield tmpl("fileellipses")
292 yield tmpl("fileellipses")
342
293
343 def diff(self, tmpl, node1, node2, files):
294 def diff(self, tmpl, node1, node2, files):
344 def filterfiles(filters, files):
295 def filterfiles(filters, files):
345 l = [x for x in files if x in filters]
296 l = [x for x in files if x in filters]
346
297
347 for t in filters:
298 for t in filters:
348 if t and t[-1] != os.sep:
299 if t and t[-1] != os.sep:
349 t += os.sep
300 t += os.sep
350 l += [x for x in files if x.startswith(t)]
301 l += [x for x in files if x.startswith(t)]
351 return l
302 return l
352
303
353 parity = paritygen(self.stripecount)
304 parity = paritygen(self.stripecount)
354 def diffblock(diff, f, fn):
305 def diffblock(diff, f, fn):
355 yield tmpl("diffblock",
306 yield tmpl("diffblock",
356 lines=prettyprintlines(diff),
307 lines=prettyprintlines(diff),
357 parity=parity.next(),
308 parity=parity.next(),
358 file=f,
309 file=f,
359 filenode=hex(fn or nullid))
310 filenode=hex(fn or nullid))
360
311
361 blockcount = countgen()
312 blockcount = countgen()
362 def prettyprintlines(diff):
313 def prettyprintlines(diff):
363 blockno = blockcount.next()
314 blockno = blockcount.next()
364 for lineno, l in enumerate(diff.splitlines(1)):
315 for lineno, l in enumerate(diff.splitlines(1)):
365 if blockno == 0:
316 if blockno == 0:
366 lineno = lineno + 1
317 lineno = lineno + 1
367 else:
318 else:
368 lineno = "%d.%d" % (blockno, lineno + 1)
319 lineno = "%d.%d" % (blockno, lineno + 1)
369 if l.startswith('+'):
320 if l.startswith('+'):
370 ltype = "difflineplus"
321 ltype = "difflineplus"
371 elif l.startswith('-'):
322 elif l.startswith('-'):
372 ltype = "difflineminus"
323 ltype = "difflineminus"
373 elif l.startswith('@'):
324 elif l.startswith('@'):
374 ltype = "difflineat"
325 ltype = "difflineat"
375 else:
326 else:
376 ltype = "diffline"
327 ltype = "diffline"
377 yield tmpl(ltype,
328 yield tmpl(ltype,
378 line=l,
329 line=l,
379 lineid="l%s" % lineno,
330 lineid="l%s" % lineno,
380 linenumber="% 8s" % lineno)
331 linenumber="% 8s" % lineno)
381
332
382 r = self.repo
333 r = self.repo
383 c1 = r.changectx(node1)
334 c1 = r.changectx(node1)
384 c2 = r.changectx(node2)
335 c2 = r.changectx(node2)
385 date1 = util.datestr(c1.date())
336 date1 = util.datestr(c1.date())
386 date2 = util.datestr(c2.date())
337 date2 = util.datestr(c2.date())
387
338
388 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
339 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
389 if files:
340 if files:
390 modified, added, removed = map(lambda x: filterfiles(files, x),
341 modified, added, removed = map(lambda x: filterfiles(files, x),
391 (modified, added, removed))
342 (modified, added, removed))
392
343
393 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
344 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
394 for f in modified:
345 for f in modified:
395 to = c1.filectx(f).data()
346 to = c1.filectx(f).data()
396 tn = c2.filectx(f).data()
347 tn = c2.filectx(f).data()
397 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
348 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
398 opts=diffopts), f, tn)
349 opts=diffopts), f, tn)
399 for f in added:
350 for f in added:
400 to = None
351 to = None
401 tn = c2.filectx(f).data()
352 tn = c2.filectx(f).data()
402 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
353 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
403 opts=diffopts), f, tn)
354 opts=diffopts), f, tn)
404 for f in removed:
355 for f in removed:
405 to = c1.filectx(f).data()
356 to = c1.filectx(f).data()
406 tn = None
357 tn = None
407 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
358 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
408 opts=diffopts), f, tn)
359 opts=diffopts), f, tn)
409
360
410 def changelog(self, tmpl, ctx, shortlog=False):
411 def changelist(limit=0,**map):
412 cl = self.repo.changelog
413 l = [] # build a list in forward order for efficiency
414 for i in xrange(start, end):
415 ctx = self.repo.changectx(i)
416 n = ctx.node()
417 showtags = webutil.showtag(self.repo, tmpl, 'changelogtag', n)
418
419 l.insert(0, {"parity": parity.next(),
420 "author": ctx.user(),
421 "parent": webutil.siblings(ctx.parents(), i - 1),
422 "child": webutil.siblings(ctx.children(), i + 1),
423 "changelogtag": showtags,
424 "desc": ctx.description(),
425 "date": ctx.date(),
426 "files": self.listfilediffs(tmpl, ctx.files(), n),
427 "rev": i,
428 "node": hex(n),
429 "tags": webutil.nodetagsdict(self.repo, n),
430 "inbranch": webutil.nodeinbranch(self.repo, ctx),
431 "branches": webutil.nodebranchdict(self.repo, ctx)
432 })
433
434 if limit > 0:
435 l = l[:limit]
436
437 for e in l:
438 yield e
439
440 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
441 cl = self.repo.changelog
442 count = cl.count()
443 pos = ctx.rev()
444 start = max(0, pos - maxchanges + 1)
445 end = min(count, start + maxchanges)
446 pos = end - 1
447 parity = paritygen(self.stripecount, offset=start-end)
448
449 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
450
451 return tmpl(shortlog and 'shortlog' or 'changelog',
452 changenav=changenav,
453 node=hex(cl.tip()),
454 rev=pos, changesets=count,
455 entries=lambda **x: changelist(limit=0,**x),
456 latestentry=lambda **x: changelist(limit=1,**x),
457 archives=self.archivelist("tip"))
458
459 def search(self, tmpl, query):
460
461 def changelist(**map):
462 cl = self.repo.changelog
463 count = 0
464 qw = query.lower().split()
465
466 def revgen():
467 for i in xrange(cl.count() - 1, 0, -100):
468 l = []
469 for j in xrange(max(0, i - 100), i + 1):
470 ctx = self.repo.changectx(j)
471 l.append(ctx)
472 l.reverse()
473 for e in l:
474 yield e
475
476 for ctx in revgen():
477 miss = 0
478 for q in qw:
479 if not (q in ctx.user().lower() or
480 q in ctx.description().lower() or
481 q in " ".join(ctx.files()).lower()):
482 miss = 1
483 break
484 if miss:
485 continue
486
487 count += 1
488 n = ctx.node()
489 showtags = webutil.showtag(self.repo, tmpl, 'changelogtag', n)
490
491 yield tmpl('searchentry',
492 parity=parity.next(),
493 author=ctx.user(),
494 parent=webutil.siblings(ctx.parents()),
495 child=webutil.siblings(ctx.children()),
496 changelogtag=showtags,
497 desc=ctx.description(),
498 date=ctx.date(),
499 files=self.listfilediffs(tmpl, ctx.files(), n),
500 rev=ctx.rev(),
501 node=hex(n),
502 tags=webutil.nodetagsdict(self.repo, n),
503 inbranch=webutil.nodeinbranch(self.repo, ctx),
504 branches=webutil.nodebranchdict(self.repo, ctx))
505
506 if count >= self.maxchanges:
507 break
508
509 cl = self.repo.changelog
510 parity = paritygen(self.stripecount)
511
512 return tmpl('search',
513 query=query,
514 node=hex(cl.tip()),
515 entries=changelist,
516 archives=self.archivelist("tip"))
517
518 def changeset(self, tmpl, ctx):
519 n = ctx.node()
520 showtags = webutil.showtag(self.repo, tmpl, 'changesettag', n)
521 parents = ctx.parents()
522 p1 = parents[0].node()
523
524 files = []
525 parity = paritygen(self.stripecount)
526 for f in ctx.files():
527 files.append(tmpl("filenodelink",
528 node=hex(n), file=f,
529 parity=parity.next()))
530
531 def diff(**map):
532 yield self.diff(tmpl, p1, n, None)
533
534 return tmpl('changeset',
535 diff=diff,
536 rev=ctx.rev(),
537 node=hex(n),
538 parent=webutil.siblings(parents),
539 child=webutil.siblings(ctx.children()),
540 changesettag=showtags,
541 author=ctx.user(),
542 desc=ctx.description(),
543 date=ctx.date(),
544 files=files,
545 archives=self.archivelist(hex(n)),
546 tags=webutil.nodetagsdict(self.repo, n),
547 branch=webutil.nodebranchnodefault(ctx),
548 inbranch=webutil.nodeinbranch(self.repo, ctx),
549 branches=webutil.nodebranchdict(self.repo, ctx))
550
551 def filelog(self, tmpl, fctx):
552 f = fctx.path()
553 fl = fctx.filelog()
554 count = fl.count()
555 pagelen = self.maxshortchanges
556 pos = fctx.filerev()
557 start = max(0, pos - pagelen + 1)
558 end = min(count, start + pagelen)
559 pos = end - 1
560 parity = paritygen(self.stripecount, offset=start-end)
561
562 def entries(limit=0, **map):
563 l = []
564
565 for i in xrange(start, end):
566 ctx = fctx.filectx(i)
567 n = fl.node(i)
568
569 l.insert(0, {"parity": parity.next(),
570 "filerev": i,
571 "file": f,
572 "node": hex(ctx.node()),
573 "author": ctx.user(),
574 "date": ctx.date(),
575 "rename": webutil.renamelink(fl, n),
576 "parent": webutil.siblings(fctx.parents()),
577 "child": webutil.siblings(fctx.children()),
578 "desc": ctx.description()})
579
580 if limit > 0:
581 l = l[:limit]
582
583 for e in l:
584 yield e
585
586 nodefunc = lambda x: fctx.filectx(fileid=x)
587 nav = revnavgen(pos, pagelen, count, nodefunc)
588 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
589 entries=lambda **x: entries(limit=0, **x),
590 latestentry=lambda **x: entries(limit=1, **x))
591
592 def filerevision(self, tmpl, fctx):
593 f = fctx.path()
594 text = fctx.data()
595 fl = fctx.filelog()
596 n = fctx.filenode()
597 parity = paritygen(self.stripecount)
598
599 if util.binary(text):
600 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
601 text = '(binary:%s)' % mt
602
603 def lines():
604 for lineno, t in enumerate(text.splitlines(1)):
605 yield {"line": t,
606 "lineid": "l%d" % (lineno + 1),
607 "linenumber": "% 6d" % (lineno + 1),
608 "parity": parity.next()}
609
610 return tmpl("filerevision",
611 file=f,
612 path=_up(f),
613 text=lines(),
614 rev=fctx.rev(),
615 node=hex(fctx.node()),
616 author=fctx.user(),
617 date=fctx.date(),
618 desc=fctx.description(),
619 branch=webutil.nodebranchnodefault(fctx),
620 parent=webutil.siblings(fctx.parents()),
621 child=webutil.siblings(fctx.children()),
622 rename=webutil.renamelink(fl, n),
623 permissions=fctx.manifest().flags(f))
624
625 def fileannotate(self, tmpl, fctx):
626 f = fctx.path()
627 n = fctx.filenode()
628 fl = fctx.filelog()
629 parity = paritygen(self.stripecount)
630
631 def annotate(**map):
632 last = None
633 if util.binary(fctx.data()):
634 mt = (mimetypes.guess_type(fctx.path())[0]
635 or 'application/octet-stream')
636 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
637 '(binary:%s)' % mt)])
638 else:
639 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
640 for lineno, ((f, targetline), l) in lines:
641 fnode = f.filenode()
642 name = self.repo.ui.shortuser(f.user())
643
644 if last != fnode:
645 last = fnode
646
647 yield {"parity": parity.next(),
648 "node": hex(f.node()),
649 "rev": f.rev(),
650 "author": name,
651 "file": f.path(),
652 "targetline": targetline,
653 "line": l,
654 "lineid": "l%d" % (lineno + 1),
655 "linenumber": "% 6d" % (lineno + 1)}
656
657 return tmpl("fileannotate",
658 file=f,
659 annotate=annotate,
660 path=_up(f),
661 rev=fctx.rev(),
662 node=hex(fctx.node()),
663 author=fctx.user(),
664 date=fctx.date(),
665 desc=fctx.description(),
666 rename=webutil.renamelink(fl, n),
667 branch=webutil.nodebranchnodefault(fctx),
668 parent=webutil.siblings(fctx.parents()),
669 child=webutil.siblings(fctx.children()),
670 permissions=fctx.manifest().flags(f))
671
672 def manifest(self, tmpl, ctx, path):
673 mf = ctx.manifest()
674 node = ctx.node()
675
676 files = {}
677 parity = paritygen(self.stripecount)
678
679 if path and path[-1] != "/":
680 path += "/"
681 l = len(path)
682 abspath = "/" + path
683
684 for f, n in mf.items():
685 if f[:l] != path:
686 continue
687 remain = f[l:]
688 if "/" in remain:
689 short = remain[:remain.index("/") + 1] # bleah
690 files[short] = (f, None)
691 else:
692 short = os.path.basename(remain)
693 files[short] = (f, n)
694
695 if not files:
696 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
697
698 def filelist(**map):
699 fl = files.keys()
700 fl.sort()
701 for f in fl:
702 full, fnode = files[f]
703 if not fnode:
704 continue
705
706 fctx = ctx.filectx(full)
707 yield {"file": full,
708 "parity": parity.next(),
709 "basename": f,
710 "date": fctx.changectx().date(),
711 "size": fctx.size(),
712 "permissions": mf.flags(full)}
713
714 def dirlist(**map):
715 fl = files.keys()
716 fl.sort()
717 for f in fl:
718 full, fnode = files[f]
719 if fnode:
720 continue
721
722 yield {"parity": parity.next(),
723 "path": "%s%s" % (abspath, f),
724 "basename": f[:-1]}
725
726 return tmpl("manifest",
727 rev=ctx.rev(),
728 node=hex(node),
729 path=abspath,
730 up=_up(abspath),
731 upparity=parity.next(),
732 fentries=filelist,
733 dentries=dirlist,
734 archives=self.archivelist(hex(node)),
735 tags=webutil.nodetagsdict(self.repo, node),
736 inbranch=webutil.nodeinbranch(self.repo, ctx),
737 branches=webutil.nodebranchdict(self.repo, ctx))
738
739 def tags(self, tmpl):
740 i = self.repo.tagslist()
741 i.reverse()
742 parity = paritygen(self.stripecount)
743
744 def entries(notip=False,limit=0, **map):
745 count = 0
746 for k, n in i:
747 if notip and k == "tip":
748 continue
749 if limit > 0 and count >= limit:
750 continue
751 count = count + 1
752 yield {"parity": parity.next(),
753 "tag": k,
754 "date": self.repo.changectx(n).date(),
755 "node": hex(n)}
756
757 return tmpl("tags",
758 node=hex(self.repo.changelog.tip()),
759 entries=lambda **x: entries(False,0, **x),
760 entriesnotip=lambda **x: entries(True,0, **x),
761 latestentry=lambda **x: entries(True,1, **x))
762
763 def summary(self, tmpl):
764 i = self.repo.tagslist()
765 i.reverse()
766
767 def tagentries(**map):
768 parity = paritygen(self.stripecount)
769 count = 0
770 for k, n in i:
771 if k == "tip": # skip tip
772 continue;
773
774 count += 1
775 if count > 10: # limit to 10 tags
776 break;
777
778 yield tmpl("tagentry",
779 parity=parity.next(),
780 tag=k,
781 node=hex(n),
782 date=self.repo.changectx(n).date())
783
784
785 def branches(**map):
786 parity = paritygen(self.stripecount)
787
788 b = self.repo.branchtags()
789 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
790 l.sort()
791
792 for r,n,t in l:
793 ctx = self.repo.changectx(n)
794
795 yield {'parity': parity.next(),
796 'branch': t,
797 'node': hex(n),
798 'date': ctx.date()}
799
800 def changelist(**map):
801 parity = paritygen(self.stripecount, offset=start-end)
802 l = [] # build a list in forward order for efficiency
803 for i in xrange(start, end):
804 ctx = self.repo.changectx(i)
805 n = ctx.node()
806 hn = hex(n)
807
808 l.insert(0, tmpl(
809 'shortlogentry',
810 parity=parity.next(),
811 author=ctx.user(),
812 desc=ctx.description(),
813 date=ctx.date(),
814 rev=i,
815 node=hn,
816 tags=webutil.nodetagsdict(self.repo, n),
817 inbranch=webutil.nodeinbranch(self.repo, ctx),
818 branches=webutil.nodebranchdict(self.repo, ctx)))
819
820 yield l
821
822 cl = self.repo.changelog
823 count = cl.count()
824 start = max(0, count - self.maxchanges)
825 end = min(count, start + self.maxchanges)
826
827 return tmpl("summary",
828 desc=self.config("web", "description", "unknown"),
829 owner=get_contact(self.config) or "unknown",
830 lastchange=cl.read(cl.tip())[2],
831 tags=tagentries,
832 branches=branches,
833 shortlog=changelist,
834 node=hex(cl.tip()),
835 archives=self.archivelist("tip"))
836
837 def filediff(self, tmpl, fctx):
838 n = fctx.node()
839 path = fctx.path()
840 parents = fctx.parents()
841 p1 = parents and parents[0].node() or nullid
842
843 def diff(**map):
844 yield self.diff(tmpl, p1, n, [path])
845
846 return tmpl("filediff",
847 file=path,
848 node=hex(n),
849 rev=fctx.rev(),
850 branch=webutil.nodebranchnodefault(fctx),
851 parent=webutil.siblings(parents),
852 child=webutil.siblings(fctx.children()),
853 diff=diff)
854
855 archive_specs = {
361 archive_specs = {
856 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
362 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
857 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
363 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
858 'zip': ('application/zip', 'zip', '.zip', None),
364 'zip': ('application/zip', 'zip', '.zip', None),
859 }
365 }
860
366
861 def archive(self, tmpl, req, key, type_):
862 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
863 cnode = self.repo.lookup(key)
864 arch_version = key
865 if cnode == key or key == 'tip':
866 arch_version = short(cnode)
867 name = "%s-%s" % (reponame, arch_version)
868 mimetype, artype, extension, encoding = self.archive_specs[type_]
869 headers = [
870 ('Content-Type', mimetype),
871 ('Content-Disposition', 'attachment; filename=%s%s' %
872 (name, extension))
873 ]
874 if encoding:
875 headers.append(('Content-Encoding', encoding))
876 req.header(headers)
877 req.respond(HTTP_OK)
878 archival.archive(self.repo, req, cnode, artype, prefix=name)
879
880 def check_perm(self, req, op, default):
367 def check_perm(self, req, op, default):
881 '''check permission for operation based on user auth.
368 '''check permission for operation based on user auth.
882 return true if op allowed, else false.
369 return true if op allowed, else false.
883 default is policy to use if no config given.'''
370 default is policy to use if no config given.'''
884
371
885 user = req.env.get('REMOTE_USER')
372 user = req.env.get('REMOTE_USER')
886
373
887 deny = self.configlist('web', 'deny_' + op)
374 deny = self.configlist('web', 'deny_' + op)
888 if deny and (not user or deny == ['*'] or user in deny):
375 if deny and (not user or deny == ['*'] or user in deny):
889 return False
376 return False
890
377
891 allow = self.configlist('web', 'allow_' + op)
378 allow = self.configlist('web', 'allow_' + op)
892 return (allow and (allow == ['*'] or user in allow)) or default
379 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,129 +1,572
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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 import os, mimetypes
8 import os, mimetypes, re
9 import webutil
9 import webutil
10 from mercurial import revlog
10 from mercurial import revlog, archival
11 from mercurial.node import hex, nullid
11 from mercurial.util import binary
12 from mercurial.util import binary
12 from mercurial.repo import RepoError
13 from mercurial.repo import RepoError
13 from common import staticfile, ErrorResponse, HTTP_OK, HTTP_NOT_FOUND
14 from common import paritygen, staticfile, get_contact, ErrorResponse
15 from common import HTTP_OK, HTTP_NOT_FOUND
14
16
15 # __all__ is populated with the allowed commands. Be sure to add to it if
17 # __all__ is populated with the allowed commands. Be sure to add to it if
16 # you're adding a new command, or the new command won't work.
18 # you're adding a new command, or the new command won't work.
17
19
18 __all__ = [
20 __all__ = [
19 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
21 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
20 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
22 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
21 'archive', 'static',
23 'archive', 'static',
22 ]
24 ]
23
25
24 def log(web, req, tmpl):
26 def log(web, req, tmpl):
25 if 'file' in req.form and req.form['file'][0]:
27 if 'file' in req.form and req.form['file'][0]:
26 return filelog(web, req, tmpl)
28 return filelog(web, req, tmpl)
27 else:
29 else:
28 return changelog(web, req, tmpl)
30 return changelog(web, req, tmpl)
29
31
30 def rawfile(web, req, tmpl):
32 def rawfile(web, req, tmpl):
31 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
33 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
32 if not path:
34 if not path:
33 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path)
35 content = manifest(web, req, tmpl)
34 req.respond(HTTP_OK, web.ctype)
36 req.respond(HTTP_OK, web.ctype)
35 return content
37 return content
36
38
37 try:
39 try:
38 fctx = webutil.filectx(web.repo, req)
40 fctx = webutil.filectx(web.repo, req)
39 except revlog.LookupError, inst:
41 except revlog.LookupError, inst:
40 try:
42 try:
41 content = web.manifest(tmpl, webutil.changectx(web.repo, req), path)
43 content = manifest(web, req, tmpl)
42 req.respond(HTTP_OK, web.ctype)
44 req.respond(HTTP_OK, web.ctype)
43 return content
45 return content
44 except ErrorResponse:
46 except ErrorResponse:
45 raise inst
47 raise inst
46
48
47 path = fctx.path()
49 path = fctx.path()
48 text = fctx.data()
50 text = fctx.data()
49 mt = mimetypes.guess_type(path)[0]
51 mt = mimetypes.guess_type(path)[0]
50 if mt is None or binary(text):
52 if mt is None or binary(text):
51 mt = mt or 'application/octet-stream'
53 mt = mt or 'application/octet-stream'
52
54
53 req.respond(HTTP_OK, mt, path, len(text))
55 req.respond(HTTP_OK, mt, path, len(text))
54 return [text]
56 return [text]
55
57
58 def _filerevision(web, tmpl, fctx):
59 f = fctx.path()
60 text = fctx.data()
61 fl = fctx.filelog()
62 n = fctx.filenode()
63 parity = paritygen(web.stripecount)
64
65 if binary(text):
66 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
67 text = '(binary:%s)' % mt
68
69 def lines():
70 for lineno, t in enumerate(text.splitlines(1)):
71 yield {"line": t,
72 "lineid": "l%d" % (lineno + 1),
73 "linenumber": "% 6d" % (lineno + 1),
74 "parity": parity.next()}
75
76 return tmpl("filerevision",
77 file=f,
78 path=webutil.up(f),
79 text=lines(),
80 rev=fctx.rev(),
81 node=hex(fctx.node()),
82 author=fctx.user(),
83 date=fctx.date(),
84 desc=fctx.description(),
85 branch=webutil.nodebranchnodefault(fctx),
86 parent=webutil.siblings(fctx.parents()),
87 child=webutil.siblings(fctx.children()),
88 rename=webutil.renamelink(fl, n),
89 permissions=fctx.manifest().flags(f))
90
56 def file(web, req, tmpl):
91 def file(web, req, tmpl):
57 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
92 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
58 if path:
93 if path:
59 try:
94 try:
60 return web.filerevision(tmpl, webutil.filectx(web.repo, req))
95 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
61 except revlog.LookupError, inst:
96 except revlog.LookupError, inst:
62 pass
97 pass
63
98
64 try:
99 try:
65 return web.manifest(tmpl, webutil.changectx(web.repo, req), path)
100 return manifest(web, req, tmpl)
66 except ErrorResponse:
101 except ErrorResponse:
67 raise inst
102 raise inst
68
103
104 def _search(web, tmpl, query):
105
106 def changelist(**map):
107 cl = web.repo.changelog
108 count = 0
109 qw = query.lower().split()
110
111 def revgen():
112 for i in xrange(cl.count() - 1, 0, -100):
113 l = []
114 for j in xrange(max(0, i - 100), i + 1):
115 ctx = web.repo.changectx(j)
116 l.append(ctx)
117 l.reverse()
118 for e in l:
119 yield e
120
121 for ctx in revgen():
122 miss = 0
123 for q in qw:
124 if not (q in ctx.user().lower() or
125 q in ctx.description().lower() or
126 q in " ".join(ctx.files()).lower()):
127 miss = 1
128 break
129 if miss:
130 continue
131
132 count = 1
133 n = ctx.node()
134 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
135
136 yield tmpl('searchentry',
137 parity=parity.next(),
138 author=ctx.user(),
139 parent=webutil.siblings(ctx.parents()),
140 child=webutil.siblings(ctx.children()),
141 changelogtag=showtags,
142 desc=ctx.description(),
143 date=ctx.date(),
144 files=web.listfilediffs(tmpl, ctx.files(), n),
145 rev=ctx.rev(),
146 node=hex(n),
147 tags=webutil.nodetagsdict(web.repo, n),
148 inbranch=webutil.nodeinbranch(web.repo, ctx),
149 branches=webutil.nodebranchdict(web.repo, ctx))
150
151 if count >= web.maxchanges:
152 break
153
154 cl = web.repo.changelog
155 parity = paritygen(web.stripecount)
156
157 return tmpl('search',
158 query=query,
159 node=hex(cl.tip()),
160 entries=changelist,
161 archives=web.archivelist("tip"))
162
69 def changelog(web, req, tmpl, shortlog = False):
163 def changelog(web, req, tmpl, shortlog = False):
70 if 'node' in req.form:
164 if 'node' in req.form:
71 ctx = webutil.changectx(web.repo, req)
165 ctx = webutil.changectx(web.repo, req)
72 else:
166 else:
73 if 'rev' in req.form:
167 if 'rev' in req.form:
74 hi = req.form['rev'][0]
168 hi = req.form['rev'][0]
75 else:
169 else:
76 hi = web.repo.changelog.count() - 1
170 hi = web.repo.changelog.count() - 1
77 try:
171 try:
78 ctx = web.repo.changectx(hi)
172 ctx = web.repo.changectx(hi)
79 except RepoError:
173 except RepoError:
80 return web.search(tmpl, hi) # XXX redirect to 404 page?
174 return _search(web, tmpl, hi) # XXX redirect to 404 page?
175
176 def changelist(limit=0, **map):
177 cl = web.repo.changelog
178 l = [] # build a list in forward order for efficiency
179 for i in xrange(start, end):
180 ctx = web.repo.changectx(i)
181 n = ctx.node()
182 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
183
184 l.insert(0, {"parity": parity.next(),
185 "author": ctx.user(),
186 "parent": webutil.siblings(ctx.parents(), i - 1),
187 "child": webutil.siblings(ctx.children(), i + 1),
188 "changelogtag": showtags,
189 "desc": ctx.description(),
190 "date": ctx.date(),
191 "files": web.listfilediffs(tmpl, ctx.files(), n),
192 "rev": i,
193 "node": hex(n),
194 "tags": webutil.nodetagsdict(web.repo, n),
195 "inbranch": webutil.nodeinbranch(web.repo, ctx),
196 "branches": webutil.nodebranchdict(web.repo, ctx)
197 })
81
198
82 return web.changelog(tmpl, ctx, shortlog = shortlog)
199 if limit > 0:
200 l = l[:limit]
201
202 for e in l:
203 yield e
204
205 maxchanges = shortlog and web.maxshortchanges or web.maxchanges
206 cl = web.repo.changelog
207 count = cl.count()
208 pos = ctx.rev()
209 start = max(0, pos - maxchanges + 1)
210 end = min(count, start + maxchanges)
211 pos = end - 1
212 parity = paritygen(web.stripecount, offset=start-end)
213
214 changenav = webutil.revnavgen(pos, maxchanges, count, web.repo.changectx)
215
216 return tmpl(shortlog and 'shortlog' or 'changelog',
217 changenav=changenav,
218 node=hex(cl.tip()),
219 rev=pos, changesets=count,
220 entries=lambda **x: changelist(limit=0,**x),
221 latestentry=lambda **x: changelist(limit=1,**x),
222 archives=web.archivelist("tip"))
83
223
84 def shortlog(web, req, tmpl):
224 def shortlog(web, req, tmpl):
85 return changelog(web, req, tmpl, shortlog = True)
225 return changelog(web, req, tmpl, shortlog = True)
86
226
87 def changeset(web, req, tmpl):
227 def changeset(web, req, tmpl):
88 return web.changeset(tmpl, webutil.changectx(web.repo, req))
228 ctx = webutil.changectx(web.repo, req)
229 n = ctx.node()
230 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', n)
231 parents = ctx.parents()
232 p1 = parents[0].node()
233
234 files = []
235 parity = paritygen(web.stripecount)
236 for f in ctx.files():
237 files.append(tmpl("filenodelink",
238 node=hex(n), file=f,
239 parity=parity.next()))
240
241 diffs = web.diff(tmpl, p1, n, None)
242 return tmpl('changeset',
243 diff=diffs,
244 rev=ctx.rev(),
245 node=hex(n),
246 parent=webutil.siblings(parents),
247 child=webutil.siblings(ctx.children()),
248 changesettag=showtags,
249 author=ctx.user(),
250 desc=ctx.description(),
251 date=ctx.date(),
252 files=files,
253 archives=web.archivelist(hex(n)),
254 tags=webutil.nodetagsdict(web.repo, n),
255 branch=webutil.nodebranchnodefault(ctx),
256 inbranch=webutil.nodeinbranch(web.repo, ctx),
257 branches=webutil.nodebranchdict(web.repo, ctx))
89
258
90 rev = changeset
259 rev = changeset
91
260
92 def manifest(web, req, tmpl):
261 def manifest(web, req, tmpl):
93 return web.manifest(tmpl, webutil.changectx(web.repo, req),
262 ctx = webutil.changectx(web.repo, req)
94 webutil.cleanpath(web.repo, req.form['path'][0]))
263 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
264 mf = ctx.manifest()
265 node = ctx.node()
266
267 files = {}
268 parity = paritygen(web.stripecount)
269
270 if path and path[-1] != "/":
271 path += "/"
272 l = len(path)
273 abspath = "/" + path
274
275 for f, n in mf.items():
276 if f[:l] != path:
277 continue
278 remain = f[l:]
279 if "/" in remain:
280 short = remain[:remain.index("/") + 1] # bleah
281 files[short] = (f, None)
282 else:
283 short = os.path.basename(remain)
284 files[short] = (f, n)
285
286 if not files:
287 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
288
289 def filelist(**map):
290 fl = files.keys()
291 fl.sort()
292 for f in fl:
293 full, fnode = files[f]
294 if not fnode:
295 continue
296
297 fctx = ctx.filectx(full)
298 yield {"file": full,
299 "parity": parity.next(),
300 "basename": f,
301 "date": fctx.changectx().date(),
302 "size": fctx.size(),
303 "permissions": mf.flags(full)}
304
305 def dirlist(**map):
306 fl = files.keys()
307 fl.sort()
308 for f in fl:
309 full, fnode = files[f]
310 if fnode:
311 continue
312
313 yield {"parity": parity.next(),
314 "path": "%s%s" % (abspath, f),
315 "basename": f[:-1]}
316
317 return tmpl("manifest",
318 rev=ctx.rev(),
319 node=hex(node),
320 path=abspath,
321 up=webutil.up(abspath),
322 upparity=parity.next(),
323 fentries=filelist,
324 dentries=dirlist,
325 archives=web.archivelist(hex(node)),
326 tags=webutil.nodetagsdict(web.repo, node),
327 inbranch=webutil.nodeinbranch(web.repo, ctx),
328 branches=webutil.nodebranchdict(web.repo, ctx))
95
329
96 def tags(web, req, tmpl):
330 def tags(web, req, tmpl):
97 return web.tags(tmpl)
331 i = web.repo.tagslist()
332 i.reverse()
333 parity = paritygen(web.stripecount)
334
335 def entries(notip=False,limit=0, **map):
336 count = 0
337 for k, n in i:
338 if notip and k == "tip":
339 continue
340 if limit > 0 and count >= limit:
341 continue
342 count = count + 1
343 yield {"parity": parity.next(),
344 "tag": k,
345 "date": web.repo.changectx(n).date(),
346 "node": hex(n)}
347
348 return tmpl("tags",
349 node=hex(web.repo.changelog.tip()),
350 entries=lambda **x: entries(False,0, **x),
351 entriesnotip=lambda **x: entries(True,0, **x),
352 latestentry=lambda **x: entries(True,1, **x))
98
353
99 def summary(web, req, tmpl):
354 def summary(web, req, tmpl):
100 return web.summary(tmpl)
355 i = web.repo.tagslist()
356 i.reverse()
357
358 def tagentries(**map):
359 parity = paritygen(web.stripecount)
360 count = 0
361 for k, n in i:
362 if k == "tip": # skip tip
363 continue
364
365 count = 1
366 if count > 10: # limit to 10 tags
367 break
368
369 yield tmpl("tagentry",
370 parity=parity.next(),
371 tag=k,
372 node=hex(n),
373 date=web.repo.changectx(n).date())
374
375 def branches(**map):
376 parity = paritygen(web.stripecount)
377
378 b = web.repo.branchtags()
379 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.items()]
380 l.sort()
381
382 for r,n,t in l:
383 ctx = web.repo.changectx(n)
384 yield {'parity': parity.next(),
385 'branch': t,
386 'node': hex(n),
387 'date': ctx.date()}
388
389 def changelist(**map):
390 parity = paritygen(web.stripecount, offset=start-end)
391 l = [] # build a list in forward order for efficiency
392 for i in xrange(start, end):
393 ctx = web.repo.changectx(i)
394 n = ctx.node()
395 hn = hex(n)
396
397 l.insert(0, tmpl(
398 'shortlogentry',
399 parity=parity.next(),
400 author=ctx.user(),
401 desc=ctx.description(),
402 date=ctx.date(),
403 rev=i,
404 node=hn,
405 tags=webutil.nodetagsdict(web.repo, n),
406 inbranch=webutil.nodeinbranch(web.repo, ctx),
407 branches=webutil.nodebranchdict(web.repo, ctx)))
408
409 yield l
410
411 cl = web.repo.changelog
412 count = cl.count()
413 start = max(0, count - web.maxchanges)
414 end = min(count, start + web.maxchanges)
415
416 return tmpl("summary",
417 desc=web.config("web", "description", "unknown"),
418 owner=get_contact(web.config) or "unknown",
419 lastchange=cl.read(cl.tip())[2],
420 tags=tagentries,
421 branches=branches,
422 shortlog=changelist,
423 node=hex(cl.tip()),
424 archives=web.archivelist("tip"))
101
425
102 def filediff(web, req, tmpl):
426 def filediff(web, req, tmpl):
103 return web.filediff(tmpl, webutil.filectx(web.repo, req))
427 fctx = webutil.filectx(web.repo, req)
428 n = fctx.node()
429 path = fctx.path()
430 parents = fctx.parents()
431 p1 = parents and parents[0].node() or nullid
432
433 diffs = web.diff(tmpl, p1, n, [path])
434 return tmpl("filediff",
435 file=path,
436 node=hex(n),
437 rev=fctx.rev(),
438 branch=webutil.nodebranchnodefault(fctx),
439 parent=webutil.siblings(parents),
440 child=webutil.siblings(fctx.children()),
441 diff=diffs)
104
442
105 diff = filediff
443 diff = filediff
106
444
107 def annotate(web, req, tmpl):
445 def annotate(web, req, tmpl):
108 return web.fileannotate(tmpl, webutil.filectx(web.repo, req))
446 fctx = webutil.filectx(web.repo, req)
447 f = fctx.path()
448 n = fctx.filenode()
449 fl = fctx.filelog()
450 parity = paritygen(web.stripecount)
451
452 def annotate(**map):
453 last = None
454 if binary(fctx.data()):
455 mt = (mimetypes.guess_type(fctx.path())[0]
456 or 'application/octet-stream')
457 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
458 '(binary:%s)' % mt)])
459 else:
460 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
461 for lineno, ((f, targetline), l) in lines:
462 fnode = f.filenode()
463 name = web.repo.ui.shortuser(f.user())
464
465 if last != fnode:
466 last = fnode
467
468 yield {"parity": parity.next(),
469 "node": hex(f.node()),
470 "rev": f.rev(),
471 "author": name,
472 "file": f.path(),
473 "targetline": targetline,
474 "line": l,
475 "lineid": "l%d" % (lineno + 1),
476 "linenumber": "% 6d" % (lineno + 1)}
477
478 return tmpl("fileannotate",
479 file=f,
480 annotate=annotate,
481 path=webutil.up(f),
482 rev=fctx.rev(),
483 node=hex(fctx.node()),
484 author=fctx.user(),
485 date=fctx.date(),
486 desc=fctx.description(),
487 rename=webutil.renamelink(fl, n),
488 branch=webutil.nodebranchnodefault(fctx),
489 parent=webutil.siblings(fctx.parents()),
490 child=webutil.siblings(fctx.children()),
491 permissions=fctx.manifest().flags(f))
109
492
110 def filelog(web, req, tmpl):
493 def filelog(web, req, tmpl):
111 return web.filelog(tmpl, webutil.filectx(web.repo, req))
494 fctx = webutil.filectx(web.repo, req)
495 f = fctx.path()
496 fl = fctx.filelog()
497 count = fl.count()
498 pagelen = web.maxshortchanges
499 pos = fctx.filerev()
500 start = max(0, pos - pagelen + 1)
501 end = min(count, start + pagelen)
502 pos = end - 1
503 parity = paritygen(web.stripecount, offset=start-end)
504
505 def entries(limit=0, **map):
506 l = []
507
508 for i in xrange(start, end):
509 ctx = fctx.filectx(i)
510 n = fl.node(i)
511
512 l.insert(0, {"parity": parity.next(),
513 "filerev": i,
514 "file": f,
515 "node": hex(ctx.node()),
516 "author": ctx.user(),
517 "date": ctx.date(),
518 "rename": webutil.renamelink(fl, n),
519 "parent": webutil.siblings(fctx.parents()),
520 "child": webutil.siblings(fctx.children()),
521 "desc": ctx.description()})
522
523 if limit > 0:
524 l = l[:limit]
525
526 for e in l:
527 yield e
528
529 nodefunc = lambda x: fctx.filectx(fileid=x)
530 nav = webutil.revnavgen(pos, pagelen, count, nodefunc)
531 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
532 entries=lambda **x: entries(limit=0, **x),
533 latestentry=lambda **x: entries(limit=1, **x))
534
112
535
113 def archive(web, req, tmpl):
536 def archive(web, req, tmpl):
114 type_ = req.form['type'][0]
537 type_ = req.form['type'][0]
115 allowed = web.configlist("web", "allow_archive")
538 allowed = web.configlist("web", "allow_archive")
116 if (type_ in web.archives and (type_ in allowed or
539 key = req.form['node'][0]
540
541 if not (type_ in web.archives and (type_ in allowed or
117 web.configbool("web", "allow" + type_, False))):
542 web.configbool("web", "allow" + type_, False))):
118 web.archive(tmpl, req, req.form['node'][0], type_)
543 msg = 'Unsupported archive type: %s' % type_
544 raise ErrorResponse(HTTP_NOT_FOUND, msg)
545
546 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
547 cnode = web.repo.lookup(key)
548 arch_version = key
549 if cnode == key or key == 'tip':
550 arch_version = short(cnode)
551 name = "%s-%s" % (reponame, arch_version)
552 mimetype, artype, extension, encoding = web.archive_specs[type_]
553 headers = [
554 ('Content-Type', mimetype),
555 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
556 ]
557 if encoding:
558 headers.append(('Content-Encoding', encoding))
559 req.header(headers)
560 req.respond(HTTP_OK)
561 archival.archive(web.repo, req, cnode, artype, prefix=name)
119 return []
562 return []
120 raise ErrorResponse(HTTP_NOT_FOUND, 'unsupported archive type: %s' % type_)
563
121
564
122 def static(web, req, tmpl):
565 def static(web, req, tmpl):
123 fname = req.form['file'][0]
566 fname = req.form['file'][0]
124 # a repo owner may set web.static in .hg/hgrc to get any file
567 # a repo owner may set web.static in .hg/hgrc to get any file
125 # readable by the user running the CGI script
568 # readable by the user running the CGI script
126 static = web.config("web", "static",
569 static = web.config("web", "static",
127 os.path.join(web.templatepath, "static"),
570 os.path.join(web.templatepath, "static"),
128 untrusted=False)
571 untrusted=False)
129 return [staticfile(static, fname, req)]
572 return [staticfile(static, fname, req)]
@@ -1,94 +1,143
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os
9 from mercurial.node import hex, nullid
10 from mercurial.node import hex, nullid
10 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
11 from mercurial import util
12 from mercurial import util
12
13
14 def up(p):
15 if p[0] != "/":
16 p = "/" + p
17 if p[-1] == "/":
18 p = p[:-1]
19 up = os.path.dirname(p)
20 if up == "/":
21 return "/"
22 return up + "/"
23
24 def revnavgen(pos, pagelen, limit, nodefunc):
25 def seq(factor, limit=None):
26 if limit:
27 yield limit
28 if limit >= 20 and limit <= 40:
29 yield 50
30 else:
31 yield 1 * factor
32 yield 3 * factor
33 for f in seq(factor * 10):
34 yield f
35
36 def nav(**map):
37 l = []
38 last = 0
39 for f in seq(1, pagelen):
40 if f < pagelen or f <= last:
41 continue
42 if f > limit:
43 break
44 last = f
45 if pos + f < limit:
46 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
47 if pos - f >= 0:
48 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
49
50 try:
51 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
52
53 for label, node in l:
54 yield {"label": label, "node": node}
55
56 yield {"label": "tip", "node": "tip"}
57 except RepoError:
58 pass
59
60 return nav
61
13 def siblings(siblings=[], hiderev=None, **args):
62 def siblings(siblings=[], hiderev=None, **args):
14 siblings = [s for s in siblings if s.node() != nullid]
63 siblings = [s for s in siblings if s.node() != nullid]
15 if len(siblings) == 1 and siblings[0].rev() == hiderev:
64 if len(siblings) == 1 and siblings[0].rev() == hiderev:
16 return
65 return
17 for s in siblings:
66 for s in siblings:
18 d = {'node': hex(s.node()), 'rev': s.rev()}
67 d = {'node': hex(s.node()), 'rev': s.rev()}
19 if hasattr(s, 'path'):
68 if hasattr(s, 'path'):
20 d['file'] = s.path()
69 d['file'] = s.path()
21 d.update(args)
70 d.update(args)
22 yield d
71 yield d
23
72
24 def renamelink(fl, node):
73 def renamelink(fl, node):
25 r = fl.renamed(node)
74 r = fl.renamed(node)
26 if r:
75 if r:
27 return [dict(file=r[0], node=hex(r[1]))]
76 return [dict(file=r[0], node=hex(r[1]))]
28 return []
77 return []
29
78
30 def nodetagsdict(repo, node):
79 def nodetagsdict(repo, node):
31 return [{"name": i} for i in repo.nodetags(node)]
80 return [{"name": i} for i in repo.nodetags(node)]
32
81
33 def nodebranchdict(repo, ctx):
82 def nodebranchdict(repo, ctx):
34 branches = []
83 branches = []
35 branch = ctx.branch()
84 branch = ctx.branch()
36 # If this is an empty repo, ctx.node() == nullid,
85 # If this is an empty repo, ctx.node() == nullid,
37 # ctx.branch() == 'default', but branchtags() is
86 # ctx.branch() == 'default', but branchtags() is
38 # an empty dict. Using dict.get avoids a traceback.
87 # an empty dict. Using dict.get avoids a traceback.
39 if repo.branchtags().get(branch) == ctx.node():
88 if repo.branchtags().get(branch) == ctx.node():
40 branches.append({"name": branch})
89 branches.append({"name": branch})
41 return branches
90 return branches
42
91
43 def nodeinbranch(repo, ctx):
92 def nodeinbranch(repo, ctx):
44 branches = []
93 branches = []
45 branch = ctx.branch()
94 branch = ctx.branch()
46 if branch != 'default' and repo.branchtags().get(branch) != ctx.node():
95 if branch != 'default' and repo.branchtags().get(branch) != ctx.node():
47 branches.append({"name": branch})
96 branches.append({"name": branch})
48 return branches
97 return branches
49
98
50 def nodebranchnodefault(ctx):
99 def nodebranchnodefault(ctx):
51 branches = []
100 branches = []
52 branch = ctx.branch()
101 branch = ctx.branch()
53 if branch != 'default':
102 if branch != 'default':
54 branches.append({"name": branch})
103 branches.append({"name": branch})
55 return branches
104 return branches
56
105
57 def showtag(repo, tmpl, t1, node=nullid, **args):
106 def showtag(repo, tmpl, t1, node=nullid, **args):
58 for t in repo.nodetags(node):
107 for t in repo.nodetags(node):
59 yield tmpl(t1, tag=t, **args)
108 yield tmpl(t1, tag=t, **args)
60
109
61 def cleanpath(repo, path):
110 def cleanpath(repo, path):
62 path = path.lstrip('/')
111 path = path.lstrip('/')
63 return util.canonpath(repo.root, '', path)
112 return util.canonpath(repo.root, '', path)
64
113
65 def changectx(repo, req):
114 def changectx(repo, req):
66 if 'node' in req.form:
115 if 'node' in req.form:
67 changeid = req.form['node'][0]
116 changeid = req.form['node'][0]
68 elif 'manifest' in req.form:
117 elif 'manifest' in req.form:
69 changeid = req.form['manifest'][0]
118 changeid = req.form['manifest'][0]
70 else:
119 else:
71 changeid = self.repo.changelog.count() - 1
120 changeid = self.repo.changelog.count() - 1
72
121
73 try:
122 try:
74 ctx = repo.changectx(changeid)
123 ctx = repo.changectx(changeid)
75 except RepoError:
124 except RepoError:
76 man = repo.manifest
125 man = repo.manifest
77 mn = man.lookup(changeid)
126 mn = man.lookup(changeid)
78 ctx = repo.changectx(man.linkrev(mn))
127 ctx = repo.changectx(man.linkrev(mn))
79
128
80 return ctx
129 return ctx
81
130
82 def filectx(repo, req):
131 def filectx(repo, req):
83 path = cleanpath(repo, req.form['file'][0])
132 path = cleanpath(repo, req.form['file'][0])
84 if 'node' in req.form:
133 if 'node' in req.form:
85 changeid = req.form['node'][0]
134 changeid = req.form['node'][0]
86 else:
135 else:
87 changeid = req.form['filenode'][0]
136 changeid = req.form['filenode'][0]
88 try:
137 try:
89 ctx = repo.changectx(changeid)
138 ctx = repo.changectx(changeid)
90 fctx = ctx.filectx(path)
139 fctx = ctx.filectx(path)
91 except RepoError:
140 except RepoError:
92 fctx = repo.filectx(path, fileid=changeid)
141 fctx = repo.filectx(path, fileid=changeid)
93
142
94 return fctx
143 return fctx
General Comments 0
You need to be logged in to leave comments. Login now