##// END OF EJS Templates
help: improve hgweb help...
Mads Kiilerich -
r17104:5a9acb0b default
parent child Browse files
Show More
@@ -1,47 +1,50 b''
1 Mercurial's internal web server, hgweb, can serve either a single
1 Mercurial's internal web server, hgweb, can serve either a single
2 repository, or a collection of them. In the latter case, a special
2 repository, or a tree of repositories. In the second case, repository
3 configuration file can be used to specify the repository paths to use
3 paths and global options can be defined using a dedicated
4 and global web configuration options.
4 configuration file common to :hg:`serve`, ``hgweb.wsgi``,
5 ``hgweb.cgi`` and ``hgweb.fcgi``.
5
6
6 This file uses the same syntax as other Mercurial configuration files,
7 This file uses the same syntax as other Mercurial configuration files
7 but only the following sections are recognized:
8 but recognizes only the following sections:
8
9
9 - web
10 - web
10 - paths
11 - paths
11 - collections
12 - collections
12
13
13 The ``web`` section can specify all the settings described in the web
14 The ``web`` options are thorougly described in :hg:`help config`.
14 section of the hgrc(5) documentation. See :hg:`help config` for
15
15 information on where to find the manual page.
16 The ``paths`` section maps URL paths to paths of repositories in the
17 filesystem. hgweb will not expose the filesystem directly - only
18 Mercurial repositories can be published and only according to the
19 configuration.
16
20
17 The ``paths`` section provides mappings of physical repository
21 The left hand side is the path in the URL. Note that hgweb reserves
18 paths to virtual ones. For instance::
22 subpaths like ``rev`` or ``file``, try using different names for
23 nested repositories to avoid confusing effects.
24
25 The right hand side is the path in the filesystem. If the specified
26 path ends with ``*`` or ``**`` the filesystem will be searched
27 recursively for repositories below that point.
28 With ``*`` it will not recurse into the repositories it finds (except for
29 ``.hg/patches``).
30 With ``**`` it will also search inside repository working directories
31 and possibly find subrepositories.
32
33 In this example::
19
34
20 [paths]
35 [paths]
21 projects/a = /foo/bar
36 /projects/a = /srv/tmprepos/a
22 projects/b = /baz/quux
37 /projects/b = c:/repos/b
23 web/root = /real/root/*
38 / = /srv/repos/*
24 / = /real/root2/*
39 /user/bob = /home/bob/repos/**
25 virtual/root2 = /real/root2/**
26
40
27 - The first two entries make two repositories in different directories
41 - The first two entries make two repositories in different directories
28 appear under the same directory in the web interface
42 appear under the same directory in the web interface
29 - The third entry maps every Mercurial repository found in '/real/root'
43 - The third entry will publish every Mercurial repository found in
30 into 'web/root'. This format is preferred over the [collections] one,
44 ``/srv/repos/``, for instance the repository ``/srv/repos/quux/``
31 since using absolute paths as configuration keys is not supported on every
45 will appear as ``http://server/quux/``
32 platform (especially on Windows).
46 - The fourth entry will publish both ``http://server/user/bob/quux/``
33 - The fourth entry is a special case mapping all repositories in
47 and ``http://server/user/bob/quux/testsubrepo/``
34 '/real/root2' in the root of the virtual directory.
35 - The fifth entry recursively finds all repositories under the real
36 root, and maps their relative paths under the virtual root.
37
48
38 The ``collections`` section provides mappings of trees of physical
49 The ``collections`` section is deprecated and has been superseeded by
39 repositories paths to virtual ones, though the paths syntax is generally
50 ``paths``.
40 preferred. For instance::
41
42 [collections]
43 /foo = /foo
44
45 Here, the left side will be stripped off all repositories found in the
46 right side. Thus ``/foo/bar`` and ``foo/quux/baz`` will be listed as
47 ``bar`` and ``quux/baz`` respectively.
@@ -1,449 +1,449 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re, time
9 import os, re, time
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, scmutil, util, templater
11 from mercurial import ui, hg, scmutil, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 # mounted as foo/subrepo
27 # /bar/ be served as as foo/N .
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 # remember to use it without working dir.
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(os.path.abspath(roothead))
35 roothead = os.path.normpath(os.path.abspath(roothead))
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 repos.extend(urlrepos(prefix, roothead, paths))
37 repos.extend(urlrepos(prefix, roothead, paths))
38 return repos
38 return repos
39
39
40 def urlrepos(prefix, roothead, paths):
40 def urlrepos(prefix, roothead, paths):
41 """yield url paths and filesystem paths from a list of repo paths
41 """yield url paths and filesystem paths from a list of repo paths
42
42
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 """
48 """
49 for path in paths:
49 for path in paths:
50 path = os.path.normpath(path)
50 path = os.path.normpath(path)
51 yield (prefix + '/' +
51 yield (prefix + '/' +
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53
53
54 def geturlcgivars(baseurl, port):
54 def geturlcgivars(baseurl, port):
55 """
55 """
56 Extract CGI variables from baseurl
56 Extract CGI variables from baseurl
57
57
58 >>> geturlcgivars("http://host.org/base", "80")
58 >>> geturlcgivars("http://host.org/base", "80")
59 ('host.org', '80', '/base')
59 ('host.org', '80', '/base')
60 >>> geturlcgivars("http://host.org:8000/base", "80")
60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 ('host.org', '8000', '/base')
61 ('host.org', '8000', '/base')
62 >>> geturlcgivars('/base', 8000)
62 >>> geturlcgivars('/base', 8000)
63 ('', '8000', '/base')
63 ('', '8000', '/base')
64 >>> geturlcgivars("base", '8000')
64 >>> geturlcgivars("base", '8000')
65 ('', '8000', '/base')
65 ('', '8000', '/base')
66 >>> geturlcgivars("http://host", '8000')
66 >>> geturlcgivars("http://host", '8000')
67 ('host', '8000', '/')
67 ('host', '8000', '/')
68 >>> geturlcgivars("http://host/", '8000')
68 >>> geturlcgivars("http://host/", '8000')
69 ('host', '8000', '/')
69 ('host', '8000', '/')
70 """
70 """
71 u = util.url(baseurl)
71 u = util.url(baseurl)
72 name = u.host or ''
72 name = u.host or ''
73 if u.port:
73 if u.port:
74 port = u.port
74 port = u.port
75 path = u.path or ""
75 path = u.path or ""
76 if not path.startswith('/'):
76 if not path.startswith('/'):
77 path = '/' + path
77 path = '/' + path
78
78
79 return name, str(port), path
79 return name, str(port), path
80
80
81 class hgwebdir(object):
81 class hgwebdir(object):
82 refreshinterval = 20
82 refreshinterval = 20
83
83
84 def __init__(self, conf, baseui=None):
84 def __init__(self, conf, baseui=None):
85 self.conf = conf
85 self.conf = conf
86 self.baseui = baseui
86 self.baseui = baseui
87 self.lastrefresh = 0
87 self.lastrefresh = 0
88 self.motd = None
88 self.motd = None
89 self.refresh()
89 self.refresh()
90
90
91 def refresh(self):
91 def refresh(self):
92 if self.lastrefresh + self.refreshinterval > time.time():
92 if self.lastrefresh + self.refreshinterval > time.time():
93 return
93 return
94
94
95 if self.baseui:
95 if self.baseui:
96 u = self.baseui.copy()
96 u = self.baseui.copy()
97 else:
97 else:
98 u = ui.ui()
98 u = ui.ui()
99 u.setconfig('ui', 'report_untrusted', 'off')
99 u.setconfig('ui', 'report_untrusted', 'off')
100 u.setconfig('ui', 'nontty', 'true')
100 u.setconfig('ui', 'nontty', 'true')
101
101
102 if not isinstance(self.conf, (dict, list, tuple)):
102 if not isinstance(self.conf, (dict, list, tuple)):
103 map = {'paths': 'hgweb-paths'}
103 map = {'paths': 'hgweb-paths'}
104 if not os.path.exists(self.conf):
104 if not os.path.exists(self.conf):
105 raise util.Abort(_('config file %s not found!') % self.conf)
105 raise util.Abort(_('config file %s not found!') % self.conf)
106 u.readconfig(self.conf, remap=map, trust=True)
106 u.readconfig(self.conf, remap=map, trust=True)
107 paths = []
107 paths = []
108 for name, ignored in u.configitems('hgweb-paths'):
108 for name, ignored in u.configitems('hgweb-paths'):
109 for path in u.configlist('hgweb-paths', name):
109 for path in u.configlist('hgweb-paths', name):
110 paths.append((name, path))
110 paths.append((name, path))
111 elif isinstance(self.conf, (list, tuple)):
111 elif isinstance(self.conf, (list, tuple)):
112 paths = self.conf
112 paths = self.conf
113 elif isinstance(self.conf, dict):
113 elif isinstance(self.conf, dict):
114 paths = self.conf.items()
114 paths = self.conf.items()
115
115
116 repos = findrepos(paths)
116 repos = findrepos(paths)
117 for prefix, root in u.configitems('collections'):
117 for prefix, root in u.configitems('collections'):
118 prefix = util.pconvert(prefix)
118 prefix = util.pconvert(prefix)
119 for path in scmutil.walkrepos(root, followsym=True):
119 for path in scmutil.walkrepos(root, followsym=True):
120 repo = os.path.normpath(path)
120 repo = os.path.normpath(path)
121 name = util.pconvert(repo)
121 name = util.pconvert(repo)
122 if name.startswith(prefix):
122 if name.startswith(prefix):
123 name = name[len(prefix):]
123 name = name[len(prefix):]
124 repos.append((name.lstrip('/'), repo))
124 repos.append((name.lstrip('/'), repo))
125
125
126 self.repos = repos
126 self.repos = repos
127 self.ui = u
127 self.ui = u
128 encoding.encoding = self.ui.config('web', 'encoding',
128 encoding.encoding = self.ui.config('web', 'encoding',
129 encoding.encoding)
129 encoding.encoding)
130 self.style = self.ui.config('web', 'style', 'paper')
130 self.style = self.ui.config('web', 'style', 'paper')
131 self.templatepath = self.ui.config('web', 'templates', None)
131 self.templatepath = self.ui.config('web', 'templates', None)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 if self.stripecount:
133 if self.stripecount:
134 self.stripecount = int(self.stripecount)
134 self.stripecount = int(self.stripecount)
135 self._baseurl = self.ui.config('web', 'baseurl')
135 self._baseurl = self.ui.config('web', 'baseurl')
136 self.lastrefresh = time.time()
136 self.lastrefresh = time.time()
137
137
138 def run(self):
138 def run(self):
139 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
139 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
140 raise RuntimeError("This function is only intended to be "
140 raise RuntimeError("This function is only intended to be "
141 "called while running as a CGI script.")
141 "called while running as a CGI script.")
142 import mercurial.hgweb.wsgicgi as wsgicgi
142 import mercurial.hgweb.wsgicgi as wsgicgi
143 wsgicgi.launch(self)
143 wsgicgi.launch(self)
144
144
145 def __call__(self, env, respond):
145 def __call__(self, env, respond):
146 req = wsgirequest(env, respond)
146 req = wsgirequest(env, respond)
147 return self.run_wsgi(req)
147 return self.run_wsgi(req)
148
148
149 def read_allowed(self, ui, req):
149 def read_allowed(self, ui, req):
150 """Check allow_read and deny_read config options of a repo's ui object
150 """Check allow_read and deny_read config options of a repo's ui object
151 to determine user permissions. By default, with neither option set (or
151 to determine user permissions. By default, with neither option set (or
152 both empty), allow all users to read the repo. There are two ways a
152 both empty), allow all users to read the repo. There are two ways a
153 user can be denied read access: (1) deny_read is not empty, and the
153 user can be denied read access: (1) deny_read is not empty, and the
154 user is unauthenticated or deny_read contains user (or *), and (2)
154 user is unauthenticated or deny_read contains user (or *), and (2)
155 allow_read is not empty and the user is not in allow_read. Return True
155 allow_read is not empty and the user is not in allow_read. Return True
156 if user is allowed to read the repo, else return False."""
156 if user is allowed to read the repo, else return False."""
157
157
158 user = req.env.get('REMOTE_USER')
158 user = req.env.get('REMOTE_USER')
159
159
160 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
160 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
161 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
161 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
162 return False
162 return False
163
163
164 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
164 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
165 # by default, allow reading if no allow_read option has been set
165 # by default, allow reading if no allow_read option has been set
166 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
166 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
167 return True
167 return True
168
168
169 return False
169 return False
170
170
171 def run_wsgi(self, req):
171 def run_wsgi(self, req):
172 try:
172 try:
173 try:
173 try:
174 self.refresh()
174 self.refresh()
175
175
176 virtual = req.env.get("PATH_INFO", "").strip('/')
176 virtual = req.env.get("PATH_INFO", "").strip('/')
177 tmpl = self.templater(req)
177 tmpl = self.templater(req)
178 ctype = tmpl('mimetype', encoding=encoding.encoding)
178 ctype = tmpl('mimetype', encoding=encoding.encoding)
179 ctype = templater.stringify(ctype)
179 ctype = templater.stringify(ctype)
180
180
181 # a static file
181 # a static file
182 if virtual.startswith('static/') or 'static' in req.form:
182 if virtual.startswith('static/') or 'static' in req.form:
183 if virtual.startswith('static/'):
183 if virtual.startswith('static/'):
184 fname = virtual[7:]
184 fname = virtual[7:]
185 else:
185 else:
186 fname = req.form['static'][0]
186 fname = req.form['static'][0]
187 static = templater.templatepath('static')
187 static = templater.templatepath('static')
188 return (staticfile(static, fname, req),)
188 return (staticfile(static, fname, req),)
189
189
190 # top-level index
190 # top-level index
191 elif not virtual:
191 elif not virtual:
192 req.respond(HTTP_OK, ctype)
192 req.respond(HTTP_OK, ctype)
193 return self.makeindex(req, tmpl)
193 return self.makeindex(req, tmpl)
194
194
195 # nested indexes and hgwebs
195 # nested indexes and hgwebs
196
196
197 repos = dict(self.repos)
197 repos = dict(self.repos)
198 virtualrepo = virtual
198 virtualrepo = virtual
199 while virtualrepo:
199 while virtualrepo:
200 real = repos.get(virtualrepo)
200 real = repos.get(virtualrepo)
201 if real:
201 if real:
202 req.env['REPO_NAME'] = virtualrepo
202 req.env['REPO_NAME'] = virtualrepo
203 try:
203 try:
204 repo = hg.repository(self.ui, real)
204 repo = hg.repository(self.ui, real)
205 return hgweb(repo).run_wsgi(req)
205 return hgweb(repo).run_wsgi(req)
206 except IOError, inst:
206 except IOError, inst:
207 msg = inst.strerror
207 msg = inst.strerror
208 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
208 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
209 except error.RepoError, inst:
209 except error.RepoError, inst:
210 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
210 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
211
211
212 up = virtualrepo.rfind('/')
212 up = virtualrepo.rfind('/')
213 if up < 0:
213 if up < 0:
214 break
214 break
215 virtualrepo = virtualrepo[:up]
215 virtualrepo = virtualrepo[:up]
216
216
217 # browse subdirectories
217 # browse subdirectories
218 subdir = virtual + '/'
218 subdir = virtual + '/'
219 if [r for r in repos if r.startswith(subdir)]:
219 if [r for r in repos if r.startswith(subdir)]:
220 req.respond(HTTP_OK, ctype)
220 req.respond(HTTP_OK, ctype)
221 return self.makeindex(req, tmpl, subdir)
221 return self.makeindex(req, tmpl, subdir)
222
222
223 # prefixes not found
223 # prefixes not found
224 req.respond(HTTP_NOT_FOUND, ctype)
224 req.respond(HTTP_NOT_FOUND, ctype)
225 return tmpl("notfound", repo=virtual)
225 return tmpl("notfound", repo=virtual)
226
226
227 except ErrorResponse, err:
227 except ErrorResponse, err:
228 req.respond(err, ctype)
228 req.respond(err, ctype)
229 return tmpl('error', error=err.message or '')
229 return tmpl('error', error=err.message or '')
230 finally:
230 finally:
231 tmpl = None
231 tmpl = None
232
232
233 def makeindex(self, req, tmpl, subdir=""):
233 def makeindex(self, req, tmpl, subdir=""):
234
234
235 def archivelist(ui, nodeid, url):
235 def archivelist(ui, nodeid, url):
236 allowed = ui.configlist("web", "allow_archive", untrusted=True)
236 allowed = ui.configlist("web", "allow_archive", untrusted=True)
237 archives = []
237 archives = []
238 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
238 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
239 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
239 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
240 untrusted=True):
240 untrusted=True):
241 archives.append({"type" : i[0], "extension": i[1],
241 archives.append({"type" : i[0], "extension": i[1],
242 "node": nodeid, "url": url})
242 "node": nodeid, "url": url})
243 return archives
243 return archives
244
244
245 def rawentries(subdir="", **map):
245 def rawentries(subdir="", **map):
246
246
247 descend = self.ui.configbool('web', 'descend', True)
247 descend = self.ui.configbool('web', 'descend', True)
248 collapse = self.ui.configbool('web', 'collapse', False)
248 collapse = self.ui.configbool('web', 'collapse', False)
249 seenrepos = set()
249 seenrepos = set()
250 seendirs = set()
250 seendirs = set()
251 for name, path in self.repos:
251 for name, path in self.repos:
252
252
253 if not name.startswith(subdir):
253 if not name.startswith(subdir):
254 continue
254 continue
255 name = name[len(subdir):]
255 name = name[len(subdir):]
256 directory = False
256 directory = False
257
257
258 if '/' in name:
258 if '/' in name:
259 if not descend:
259 if not descend:
260 continue
260 continue
261
261
262 nameparts = name.split('/')
262 nameparts = name.split('/')
263 rootname = nameparts[0]
263 rootname = nameparts[0]
264
264
265 if not collapse:
265 if not collapse:
266 pass
266 pass
267 elif rootname in seendirs:
267 elif rootname in seendirs:
268 continue
268 continue
269 elif rootname in seenrepos:
269 elif rootname in seenrepos:
270 pass
270 pass
271 else:
271 else:
272 directory = True
272 directory = True
273 name = rootname
273 name = rootname
274
274
275 # redefine the path to refer to the directory
275 # redefine the path to refer to the directory
276 discarded = '/'.join(nameparts[1:])
276 discarded = '/'.join(nameparts[1:])
277
277
278 # remove name parts plus accompanying slash
278 # remove name parts plus accompanying slash
279 path = path[:-len(discarded) - 1]
279 path = path[:-len(discarded) - 1]
280
280
281 parts = [name]
281 parts = [name]
282 if 'PATH_INFO' in req.env:
282 if 'PATH_INFO' in req.env:
283 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
283 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
284 if req.env['SCRIPT_NAME']:
284 if req.env['SCRIPT_NAME']:
285 parts.insert(0, req.env['SCRIPT_NAME'])
285 parts.insert(0, req.env['SCRIPT_NAME'])
286 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
286 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
287
287
288 # show either a directory entry or a repository
288 # show either a directory entry or a repository
289 if directory:
289 if directory:
290 # get the directory's time information
290 # get the directory's time information
291 try:
291 try:
292 d = (get_mtime(path), util.makedate()[1])
292 d = (get_mtime(path), util.makedate()[1])
293 except OSError:
293 except OSError:
294 continue
294 continue
295
295
296 row = dict(contact="",
296 row = dict(contact="",
297 contact_sort="",
297 contact_sort="",
298 name=name,
298 name=name,
299 name_sort=name,
299 name_sort=name,
300 url=url,
300 url=url,
301 description="",
301 description="",
302 description_sort="",
302 description_sort="",
303 lastchange=d,
303 lastchange=d,
304 lastchange_sort=d[1]-d[0],
304 lastchange_sort=d[1]-d[0],
305 archives=[])
305 archives=[])
306
306
307 seendirs.add(name)
307 seendirs.add(name)
308 yield row
308 yield row
309 continue
309 continue
310
310
311 u = self.ui.copy()
311 u = self.ui.copy()
312 try:
312 try:
313 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
313 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
314 except Exception, e:
314 except Exception, e:
315 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
315 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
316 continue
316 continue
317 def get(section, name, default=None):
317 def get(section, name, default=None):
318 return u.config(section, name, default, untrusted=True)
318 return u.config(section, name, default, untrusted=True)
319
319
320 if u.configbool("web", "hidden", untrusted=True):
320 if u.configbool("web", "hidden", untrusted=True):
321 continue
321 continue
322
322
323 if not self.read_allowed(u, req):
323 if not self.read_allowed(u, req):
324 continue
324 continue
325
325
326 # update time with local timezone
326 # update time with local timezone
327 try:
327 try:
328 r = hg.repository(self.ui, path)
328 r = hg.repository(self.ui, path)
329 except IOError:
329 except IOError:
330 u.warn(_('error accessing repository at %s\n') % path)
330 u.warn(_('error accessing repository at %s\n') % path)
331 continue
331 continue
332 except error.RepoError:
332 except error.RepoError:
333 u.warn(_('error accessing repository at %s\n') % path)
333 u.warn(_('error accessing repository at %s\n') % path)
334 continue
334 continue
335 try:
335 try:
336 d = (get_mtime(r.spath), util.makedate()[1])
336 d = (get_mtime(r.spath), util.makedate()[1])
337 except OSError:
337 except OSError:
338 continue
338 continue
339
339
340 contact = get_contact(get)
340 contact = get_contact(get)
341 description = get("web", "description", "")
341 description = get("web", "description", "")
342 name = get("web", "name", name)
342 name = get("web", "name", name)
343 row = dict(contact=contact or "unknown",
343 row = dict(contact=contact or "unknown",
344 contact_sort=contact.upper() or "unknown",
344 contact_sort=contact.upper() or "unknown",
345 name=name,
345 name=name,
346 name_sort=name,
346 name_sort=name,
347 url=url,
347 url=url,
348 description=description or "unknown",
348 description=description or "unknown",
349 description_sort=description.upper() or "unknown",
349 description_sort=description.upper() or "unknown",
350 lastchange=d,
350 lastchange=d,
351 lastchange_sort=d[1]-d[0],
351 lastchange_sort=d[1]-d[0],
352 archives=archivelist(u, "tip", url))
352 archives=archivelist(u, "tip", url))
353
353
354 seenrepos.add(name)
354 seenrepos.add(name)
355 yield row
355 yield row
356
356
357 sortdefault = None, False
357 sortdefault = None, False
358 def entries(sortcolumn="", descending=False, subdir="", **map):
358 def entries(sortcolumn="", descending=False, subdir="", **map):
359 rows = rawentries(subdir=subdir, **map)
359 rows = rawentries(subdir=subdir, **map)
360
360
361 if sortcolumn and sortdefault != (sortcolumn, descending):
361 if sortcolumn and sortdefault != (sortcolumn, descending):
362 sortkey = '%s_sort' % sortcolumn
362 sortkey = '%s_sort' % sortcolumn
363 rows = sorted(rows, key=lambda x: x[sortkey],
363 rows = sorted(rows, key=lambda x: x[sortkey],
364 reverse=descending)
364 reverse=descending)
365 for row, parity in zip(rows, paritygen(self.stripecount)):
365 for row, parity in zip(rows, paritygen(self.stripecount)):
366 row['parity'] = parity
366 row['parity'] = parity
367 yield row
367 yield row
368
368
369 self.refresh()
369 self.refresh()
370 sortable = ["name", "description", "contact", "lastchange"]
370 sortable = ["name", "description", "contact", "lastchange"]
371 sortcolumn, descending = sortdefault
371 sortcolumn, descending = sortdefault
372 if 'sort' in req.form:
372 if 'sort' in req.form:
373 sortcolumn = req.form['sort'][0]
373 sortcolumn = req.form['sort'][0]
374 descending = sortcolumn.startswith('-')
374 descending = sortcolumn.startswith('-')
375 if descending:
375 if descending:
376 sortcolumn = sortcolumn[1:]
376 sortcolumn = sortcolumn[1:]
377 if sortcolumn not in sortable:
377 if sortcolumn not in sortable:
378 sortcolumn = ""
378 sortcolumn = ""
379
379
380 sort = [("sort_%s" % column,
380 sort = [("sort_%s" % column,
381 "%s%s" % ((not descending and column == sortcolumn)
381 "%s%s" % ((not descending and column == sortcolumn)
382 and "-" or "", column))
382 and "-" or "", column))
383 for column in sortable]
383 for column in sortable]
384
384
385 self.refresh()
385 self.refresh()
386 self.updatereqenv(req.env)
386 self.updatereqenv(req.env)
387
387
388 return tmpl("index", entries=entries, subdir=subdir,
388 return tmpl("index", entries=entries, subdir=subdir,
389 sortcolumn=sortcolumn, descending=descending,
389 sortcolumn=sortcolumn, descending=descending,
390 **dict(sort))
390 **dict(sort))
391
391
392 def templater(self, req):
392 def templater(self, req):
393
393
394 def header(**map):
394 def header(**map):
395 yield tmpl('header', encoding=encoding.encoding, **map)
395 yield tmpl('header', encoding=encoding.encoding, **map)
396
396
397 def footer(**map):
397 def footer(**map):
398 yield tmpl("footer", **map)
398 yield tmpl("footer", **map)
399
399
400 def motd(**map):
400 def motd(**map):
401 if self.motd is not None:
401 if self.motd is not None:
402 yield self.motd
402 yield self.motd
403 else:
403 else:
404 yield config('web', 'motd', '')
404 yield config('web', 'motd', '')
405
405
406 def config(section, name, default=None, untrusted=True):
406 def config(section, name, default=None, untrusted=True):
407 return self.ui.config(section, name, default, untrusted)
407 return self.ui.config(section, name, default, untrusted)
408
408
409 self.updatereqenv(req.env)
409 self.updatereqenv(req.env)
410
410
411 url = req.env.get('SCRIPT_NAME', '')
411 url = req.env.get('SCRIPT_NAME', '')
412 if not url.endswith('/'):
412 if not url.endswith('/'):
413 url += '/'
413 url += '/'
414
414
415 vars = {}
415 vars = {}
416 styles = (
416 styles = (
417 req.form.get('style', [None])[0],
417 req.form.get('style', [None])[0],
418 config('web', 'style'),
418 config('web', 'style'),
419 'paper'
419 'paper'
420 )
420 )
421 style, mapfile = templater.stylemap(styles, self.templatepath)
421 style, mapfile = templater.stylemap(styles, self.templatepath)
422 if style == styles[0]:
422 if style == styles[0]:
423 vars['style'] = style
423 vars['style'] = style
424
424
425 start = url[-1] == '?' and '&' or '?'
425 start = url[-1] == '?' and '&' or '?'
426 sessionvars = webutil.sessionvars(vars, start)
426 sessionvars = webutil.sessionvars(vars, start)
427 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
427 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
428 logoimg = config('web', 'logoimg', 'hglogo.png')
428 logoimg = config('web', 'logoimg', 'hglogo.png')
429 staticurl = config('web', 'staticurl') or url + 'static/'
429 staticurl = config('web', 'staticurl') or url + 'static/'
430 if not staticurl.endswith('/'):
430 if not staticurl.endswith('/'):
431 staticurl += '/'
431 staticurl += '/'
432
432
433 tmpl = templater.templater(mapfile,
433 tmpl = templater.templater(mapfile,
434 defaults={"header": header,
434 defaults={"header": header,
435 "footer": footer,
435 "footer": footer,
436 "motd": motd,
436 "motd": motd,
437 "url": url,
437 "url": url,
438 "logourl": logourl,
438 "logourl": logourl,
439 "logoimg": logoimg,
439 "logoimg": logoimg,
440 "staticurl": staticurl,
440 "staticurl": staticurl,
441 "sessionvars": sessionvars})
441 "sessionvars": sessionvars})
442 return tmpl
442 return tmpl
443
443
444 def updatereqenv(self, env):
444 def updatereqenv(self, env):
445 if self._baseurl is not None:
445 if self._baseurl is not None:
446 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
446 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
447 env['SERVER_NAME'] = name
447 env['SERVER_NAME'] = name
448 env['SERVER_PORT'] = port
448 env['SERVER_PORT'] = port
449 env['SCRIPT_NAME'] = path
449 env['SCRIPT_NAME'] = path
@@ -1,886 +1,887 b''
1 # scmutil.py - Mercurial core utility functions
1 # scmutil.py - Mercurial core utility functions
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import util, error, osutil, revset, similar, encoding
9 import util, error, osutil, revset, similar, encoding
10 import match as matchmod
10 import match as matchmod
11 import os, errno, re, stat, sys, glob
11 import os, errno, re, stat, sys, glob
12
12
13 def nochangesfound(ui, secretlist=None):
13 def nochangesfound(ui, secretlist=None):
14 '''report no changes for push/pull'''
14 '''report no changes for push/pull'''
15 if secretlist:
15 if secretlist:
16 ui.status(_("no changes found (ignored %d secret changesets)\n")
16 ui.status(_("no changes found (ignored %d secret changesets)\n")
17 % len(secretlist))
17 % len(secretlist))
18 else:
18 else:
19 ui.status(_("no changes found\n"))
19 ui.status(_("no changes found\n"))
20
20
21 def checkfilename(f):
21 def checkfilename(f):
22 '''Check that the filename f is an acceptable filename for a tracked file'''
22 '''Check that the filename f is an acceptable filename for a tracked file'''
23 if '\r' in f or '\n' in f:
23 if '\r' in f or '\n' in f:
24 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
24 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
25
25
26 def checkportable(ui, f):
26 def checkportable(ui, f):
27 '''Check if filename f is portable and warn or abort depending on config'''
27 '''Check if filename f is portable and warn or abort depending on config'''
28 checkfilename(f)
28 checkfilename(f)
29 abort, warn = checkportabilityalert(ui)
29 abort, warn = checkportabilityalert(ui)
30 if abort or warn:
30 if abort or warn:
31 msg = util.checkwinfilename(f)
31 msg = util.checkwinfilename(f)
32 if msg:
32 if msg:
33 msg = "%s: %r" % (msg, f)
33 msg = "%s: %r" % (msg, f)
34 if abort:
34 if abort:
35 raise util.Abort(msg)
35 raise util.Abort(msg)
36 ui.warn(_("warning: %s\n") % msg)
36 ui.warn(_("warning: %s\n") % msg)
37
37
38 def checkportabilityalert(ui):
38 def checkportabilityalert(ui):
39 '''check if the user's config requests nothing, a warning, or abort for
39 '''check if the user's config requests nothing, a warning, or abort for
40 non-portable filenames'''
40 non-portable filenames'''
41 val = ui.config('ui', 'portablefilenames', 'warn')
41 val = ui.config('ui', 'portablefilenames', 'warn')
42 lval = val.lower()
42 lval = val.lower()
43 bval = util.parsebool(val)
43 bval = util.parsebool(val)
44 abort = os.name == 'nt' or lval == 'abort'
44 abort = os.name == 'nt' or lval == 'abort'
45 warn = bval or lval == 'warn'
45 warn = bval or lval == 'warn'
46 if bval is None and not (warn or abort or lval == 'ignore'):
46 if bval is None and not (warn or abort or lval == 'ignore'):
47 raise error.ConfigError(
47 raise error.ConfigError(
48 _("ui.portablefilenames value is invalid ('%s')") % val)
48 _("ui.portablefilenames value is invalid ('%s')") % val)
49 return abort, warn
49 return abort, warn
50
50
51 class casecollisionauditor(object):
51 class casecollisionauditor(object):
52 def __init__(self, ui, abort, existingiter):
52 def __init__(self, ui, abort, existingiter):
53 self._ui = ui
53 self._ui = ui
54 self._abort = abort
54 self._abort = abort
55 self._map = {}
55 self._map = {}
56 for f in existingiter:
56 for f in existingiter:
57 self._map[encoding.lower(f)] = f
57 self._map[encoding.lower(f)] = f
58
58
59 def __call__(self, f):
59 def __call__(self, f):
60 fl = encoding.lower(f)
60 fl = encoding.lower(f)
61 map = self._map
61 map = self._map
62 if fl in map and map[fl] != f:
62 if fl in map and map[fl] != f:
63 msg = _('possible case-folding collision for %s') % f
63 msg = _('possible case-folding collision for %s') % f
64 if self._abort:
64 if self._abort:
65 raise util.Abort(msg)
65 raise util.Abort(msg)
66 self._ui.warn(_("warning: %s\n") % msg)
66 self._ui.warn(_("warning: %s\n") % msg)
67 map[fl] = f
67 map[fl] = f
68
68
69 class pathauditor(object):
69 class pathauditor(object):
70 '''ensure that a filesystem path contains no banned components.
70 '''ensure that a filesystem path contains no banned components.
71 the following properties of a path are checked:
71 the following properties of a path are checked:
72
72
73 - ends with a directory separator
73 - ends with a directory separator
74 - under top-level .hg
74 - under top-level .hg
75 - starts at the root of a windows drive
75 - starts at the root of a windows drive
76 - contains ".."
76 - contains ".."
77 - traverses a symlink (e.g. a/symlink_here/b)
77 - traverses a symlink (e.g. a/symlink_here/b)
78 - inside a nested repository (a callback can be used to approve
78 - inside a nested repository (a callback can be used to approve
79 some nested repositories, e.g., subrepositories)
79 some nested repositories, e.g., subrepositories)
80 '''
80 '''
81
81
82 def __init__(self, root, callback=None):
82 def __init__(self, root, callback=None):
83 self.audited = set()
83 self.audited = set()
84 self.auditeddir = set()
84 self.auditeddir = set()
85 self.root = root
85 self.root = root
86 self.callback = callback
86 self.callback = callback
87 if os.path.lexists(root) and not util.checkcase(root):
87 if os.path.lexists(root) and not util.checkcase(root):
88 self.normcase = util.normcase
88 self.normcase = util.normcase
89 else:
89 else:
90 self.normcase = lambda x: x
90 self.normcase = lambda x: x
91
91
92 def __call__(self, path):
92 def __call__(self, path):
93 '''Check the relative path.
93 '''Check the relative path.
94 path may contain a pattern (e.g. foodir/**.txt)'''
94 path may contain a pattern (e.g. foodir/**.txt)'''
95
95
96 path = util.localpath(path)
96 path = util.localpath(path)
97 normpath = self.normcase(path)
97 normpath = self.normcase(path)
98 if normpath in self.audited:
98 if normpath in self.audited:
99 return
99 return
100 # AIX ignores "/" at end of path, others raise EISDIR.
100 # AIX ignores "/" at end of path, others raise EISDIR.
101 if util.endswithsep(path):
101 if util.endswithsep(path):
102 raise util.Abort(_("path ends in directory separator: %s") % path)
102 raise util.Abort(_("path ends in directory separator: %s") % path)
103 parts = util.splitpath(path)
103 parts = util.splitpath(path)
104 if (os.path.splitdrive(path)[0]
104 if (os.path.splitdrive(path)[0]
105 or parts[0].lower() in ('.hg', '.hg.', '')
105 or parts[0].lower() in ('.hg', '.hg.', '')
106 or os.pardir in parts):
106 or os.pardir in parts):
107 raise util.Abort(_("path contains illegal component: %s") % path)
107 raise util.Abort(_("path contains illegal component: %s") % path)
108 if '.hg' in path.lower():
108 if '.hg' in path.lower():
109 lparts = [p.lower() for p in parts]
109 lparts = [p.lower() for p in parts]
110 for p in '.hg', '.hg.':
110 for p in '.hg', '.hg.':
111 if p in lparts[1:]:
111 if p in lparts[1:]:
112 pos = lparts.index(p)
112 pos = lparts.index(p)
113 base = os.path.join(*parts[:pos])
113 base = os.path.join(*parts[:pos])
114 raise util.Abort(_("path '%s' is inside nested repo %r")
114 raise util.Abort(_("path '%s' is inside nested repo %r")
115 % (path, base))
115 % (path, base))
116
116
117 normparts = util.splitpath(normpath)
117 normparts = util.splitpath(normpath)
118 assert len(parts) == len(normparts)
118 assert len(parts) == len(normparts)
119
119
120 parts.pop()
120 parts.pop()
121 normparts.pop()
121 normparts.pop()
122 prefixes = []
122 prefixes = []
123 while parts:
123 while parts:
124 prefix = os.sep.join(parts)
124 prefix = os.sep.join(parts)
125 normprefix = os.sep.join(normparts)
125 normprefix = os.sep.join(normparts)
126 if normprefix in self.auditeddir:
126 if normprefix in self.auditeddir:
127 break
127 break
128 curpath = os.path.join(self.root, prefix)
128 curpath = os.path.join(self.root, prefix)
129 try:
129 try:
130 st = os.lstat(curpath)
130 st = os.lstat(curpath)
131 except OSError, err:
131 except OSError, err:
132 # EINVAL can be raised as invalid path syntax under win32.
132 # EINVAL can be raised as invalid path syntax under win32.
133 # They must be ignored for patterns can be checked too.
133 # They must be ignored for patterns can be checked too.
134 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
134 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
135 raise
135 raise
136 else:
136 else:
137 if stat.S_ISLNK(st.st_mode):
137 if stat.S_ISLNK(st.st_mode):
138 raise util.Abort(
138 raise util.Abort(
139 _('path %r traverses symbolic link %r')
139 _('path %r traverses symbolic link %r')
140 % (path, prefix))
140 % (path, prefix))
141 elif (stat.S_ISDIR(st.st_mode) and
141 elif (stat.S_ISDIR(st.st_mode) and
142 os.path.isdir(os.path.join(curpath, '.hg'))):
142 os.path.isdir(os.path.join(curpath, '.hg'))):
143 if not self.callback or not self.callback(curpath):
143 if not self.callback or not self.callback(curpath):
144 raise util.Abort(_("path '%s' is inside nested "
144 raise util.Abort(_("path '%s' is inside nested "
145 "repo %r")
145 "repo %r")
146 % (path, prefix))
146 % (path, prefix))
147 prefixes.append(normprefix)
147 prefixes.append(normprefix)
148 parts.pop()
148 parts.pop()
149 normparts.pop()
149 normparts.pop()
150
150
151 self.audited.add(normpath)
151 self.audited.add(normpath)
152 # only add prefixes to the cache after checking everything: we don't
152 # only add prefixes to the cache after checking everything: we don't
153 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
153 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
154 self.auditeddir.update(prefixes)
154 self.auditeddir.update(prefixes)
155
155
156 class abstractopener(object):
156 class abstractopener(object):
157 """Abstract base class; cannot be instantiated"""
157 """Abstract base class; cannot be instantiated"""
158
158
159 def __init__(self, *args, **kwargs):
159 def __init__(self, *args, **kwargs):
160 '''Prevent instantiation; don't call this from subclasses.'''
160 '''Prevent instantiation; don't call this from subclasses.'''
161 raise NotImplementedError('attempted instantiating ' + str(type(self)))
161 raise NotImplementedError('attempted instantiating ' + str(type(self)))
162
162
163 def tryread(self, path):
163 def tryread(self, path):
164 '''gracefully return an empty string for missing files'''
164 '''gracefully return an empty string for missing files'''
165 try:
165 try:
166 return self.read(path)
166 return self.read(path)
167 except IOError, inst:
167 except IOError, inst:
168 if inst.errno != errno.ENOENT:
168 if inst.errno != errno.ENOENT:
169 raise
169 raise
170 return ""
170 return ""
171
171
172 def read(self, path):
172 def read(self, path):
173 fp = self(path, 'rb')
173 fp = self(path, 'rb')
174 try:
174 try:
175 return fp.read()
175 return fp.read()
176 finally:
176 finally:
177 fp.close()
177 fp.close()
178
178
179 def write(self, path, data):
179 def write(self, path, data):
180 fp = self(path, 'wb')
180 fp = self(path, 'wb')
181 try:
181 try:
182 return fp.write(data)
182 return fp.write(data)
183 finally:
183 finally:
184 fp.close()
184 fp.close()
185
185
186 def append(self, path, data):
186 def append(self, path, data):
187 fp = self(path, 'ab')
187 fp = self(path, 'ab')
188 try:
188 try:
189 return fp.write(data)
189 return fp.write(data)
190 finally:
190 finally:
191 fp.close()
191 fp.close()
192
192
193 class opener(abstractopener):
193 class opener(abstractopener):
194 '''Open files relative to a base directory
194 '''Open files relative to a base directory
195
195
196 This class is used to hide the details of COW semantics and
196 This class is used to hide the details of COW semantics and
197 remote file access from higher level code.
197 remote file access from higher level code.
198 '''
198 '''
199 def __init__(self, base, audit=True):
199 def __init__(self, base, audit=True):
200 self.base = base
200 self.base = base
201 self._audit = audit
201 self._audit = audit
202 if audit:
202 if audit:
203 self.auditor = pathauditor(base)
203 self.auditor = pathauditor(base)
204 else:
204 else:
205 self.auditor = util.always
205 self.auditor = util.always
206 self.createmode = None
206 self.createmode = None
207 self._trustnlink = None
207 self._trustnlink = None
208
208
209 @util.propertycache
209 @util.propertycache
210 def _cansymlink(self):
210 def _cansymlink(self):
211 return util.checklink(self.base)
211 return util.checklink(self.base)
212
212
213 def _fixfilemode(self, name):
213 def _fixfilemode(self, name):
214 if self.createmode is None:
214 if self.createmode is None:
215 return
215 return
216 os.chmod(name, self.createmode & 0666)
216 os.chmod(name, self.createmode & 0666)
217
217
218 def __call__(self, path, mode="r", text=False, atomictemp=False):
218 def __call__(self, path, mode="r", text=False, atomictemp=False):
219 if self._audit:
219 if self._audit:
220 r = util.checkosfilename(path)
220 r = util.checkosfilename(path)
221 if r:
221 if r:
222 raise util.Abort("%s: %r" % (r, path))
222 raise util.Abort("%s: %r" % (r, path))
223 self.auditor(path)
223 self.auditor(path)
224 f = self.join(path)
224 f = self.join(path)
225
225
226 if not text and "b" not in mode:
226 if not text and "b" not in mode:
227 mode += "b" # for that other OS
227 mode += "b" # for that other OS
228
228
229 nlink = -1
229 nlink = -1
230 dirname, basename = os.path.split(f)
230 dirname, basename = os.path.split(f)
231 # If basename is empty, then the path is malformed because it points
231 # If basename is empty, then the path is malformed because it points
232 # to a directory. Let the posixfile() call below raise IOError.
232 # to a directory. Let the posixfile() call below raise IOError.
233 if basename and mode not in ('r', 'rb'):
233 if basename and mode not in ('r', 'rb'):
234 if atomictemp:
234 if atomictemp:
235 if not os.path.isdir(dirname):
235 if not os.path.isdir(dirname):
236 util.makedirs(dirname, self.createmode)
236 util.makedirs(dirname, self.createmode)
237 return util.atomictempfile(f, mode, self.createmode)
237 return util.atomictempfile(f, mode, self.createmode)
238 try:
238 try:
239 if 'w' in mode:
239 if 'w' in mode:
240 util.unlink(f)
240 util.unlink(f)
241 nlink = 0
241 nlink = 0
242 else:
242 else:
243 # nlinks() may behave differently for files on Windows
243 # nlinks() may behave differently for files on Windows
244 # shares if the file is open.
244 # shares if the file is open.
245 fd = util.posixfile(f)
245 fd = util.posixfile(f)
246 nlink = util.nlinks(f)
246 nlink = util.nlinks(f)
247 if nlink < 1:
247 if nlink < 1:
248 nlink = 2 # force mktempcopy (issue1922)
248 nlink = 2 # force mktempcopy (issue1922)
249 fd.close()
249 fd.close()
250 except (OSError, IOError), e:
250 except (OSError, IOError), e:
251 if e.errno != errno.ENOENT:
251 if e.errno != errno.ENOENT:
252 raise
252 raise
253 nlink = 0
253 nlink = 0
254 if not os.path.isdir(dirname):
254 if not os.path.isdir(dirname):
255 util.makedirs(dirname, self.createmode)
255 util.makedirs(dirname, self.createmode)
256 if nlink > 0:
256 if nlink > 0:
257 if self._trustnlink is None:
257 if self._trustnlink is None:
258 self._trustnlink = nlink > 1 or util.checknlink(f)
258 self._trustnlink = nlink > 1 or util.checknlink(f)
259 if nlink > 1 or not self._trustnlink:
259 if nlink > 1 or not self._trustnlink:
260 util.rename(util.mktempcopy(f), f)
260 util.rename(util.mktempcopy(f), f)
261 fp = util.posixfile(f, mode)
261 fp = util.posixfile(f, mode)
262 if nlink == 0:
262 if nlink == 0:
263 self._fixfilemode(f)
263 self._fixfilemode(f)
264 return fp
264 return fp
265
265
266 def symlink(self, src, dst):
266 def symlink(self, src, dst):
267 self.auditor(dst)
267 self.auditor(dst)
268 linkname = self.join(dst)
268 linkname = self.join(dst)
269 try:
269 try:
270 os.unlink(linkname)
270 os.unlink(linkname)
271 except OSError:
271 except OSError:
272 pass
272 pass
273
273
274 dirname = os.path.dirname(linkname)
274 dirname = os.path.dirname(linkname)
275 if not os.path.exists(dirname):
275 if not os.path.exists(dirname):
276 util.makedirs(dirname, self.createmode)
276 util.makedirs(dirname, self.createmode)
277
277
278 if self._cansymlink:
278 if self._cansymlink:
279 try:
279 try:
280 os.symlink(src, linkname)
280 os.symlink(src, linkname)
281 except OSError, err:
281 except OSError, err:
282 raise OSError(err.errno, _('could not symlink to %r: %s') %
282 raise OSError(err.errno, _('could not symlink to %r: %s') %
283 (src, err.strerror), linkname)
283 (src, err.strerror), linkname)
284 else:
284 else:
285 f = self(dst, "w")
285 f = self(dst, "w")
286 f.write(src)
286 f.write(src)
287 f.close()
287 f.close()
288 self._fixfilemode(dst)
288 self._fixfilemode(dst)
289
289
290 def audit(self, path):
290 def audit(self, path):
291 self.auditor(path)
291 self.auditor(path)
292
292
293 def join(self, path):
293 def join(self, path):
294 return os.path.join(self.base, path)
294 return os.path.join(self.base, path)
295
295
296 class filteropener(abstractopener):
296 class filteropener(abstractopener):
297 '''Wrapper opener for filtering filenames with a function.'''
297 '''Wrapper opener for filtering filenames with a function.'''
298
298
299 def __init__(self, opener, filter):
299 def __init__(self, opener, filter):
300 self._filter = filter
300 self._filter = filter
301 self._orig = opener
301 self._orig = opener
302
302
303 def __call__(self, path, *args, **kwargs):
303 def __call__(self, path, *args, **kwargs):
304 return self._orig(self._filter(path), *args, **kwargs)
304 return self._orig(self._filter(path), *args, **kwargs)
305
305
306 def canonpath(root, cwd, myname, auditor=None):
306 def canonpath(root, cwd, myname, auditor=None):
307 '''return the canonical path of myname, given cwd and root'''
307 '''return the canonical path of myname, given cwd and root'''
308 if util.endswithsep(root):
308 if util.endswithsep(root):
309 rootsep = root
309 rootsep = root
310 else:
310 else:
311 rootsep = root + os.sep
311 rootsep = root + os.sep
312 name = myname
312 name = myname
313 if not os.path.isabs(name):
313 if not os.path.isabs(name):
314 name = os.path.join(root, cwd, name)
314 name = os.path.join(root, cwd, name)
315 name = os.path.normpath(name)
315 name = os.path.normpath(name)
316 if auditor is None:
316 if auditor is None:
317 auditor = pathauditor(root)
317 auditor = pathauditor(root)
318 if name != rootsep and name.startswith(rootsep):
318 if name != rootsep and name.startswith(rootsep):
319 name = name[len(rootsep):]
319 name = name[len(rootsep):]
320 auditor(name)
320 auditor(name)
321 return util.pconvert(name)
321 return util.pconvert(name)
322 elif name == root:
322 elif name == root:
323 return ''
323 return ''
324 else:
324 else:
325 # Determine whether `name' is in the hierarchy at or beneath `root',
325 # Determine whether `name' is in the hierarchy at or beneath `root',
326 # by iterating name=dirname(name) until that causes no change (can't
326 # by iterating name=dirname(name) until that causes no change (can't
327 # check name == '/', because that doesn't work on windows). The list
327 # check name == '/', because that doesn't work on windows). The list
328 # `rel' holds the reversed list of components making up the relative
328 # `rel' holds the reversed list of components making up the relative
329 # file name we want.
329 # file name we want.
330 rel = []
330 rel = []
331 while True:
331 while True:
332 try:
332 try:
333 s = util.samefile(name, root)
333 s = util.samefile(name, root)
334 except OSError:
334 except OSError:
335 s = False
335 s = False
336 if s:
336 if s:
337 if not rel:
337 if not rel:
338 # name was actually the same as root (maybe a symlink)
338 # name was actually the same as root (maybe a symlink)
339 return ''
339 return ''
340 rel.reverse()
340 rel.reverse()
341 name = os.path.join(*rel)
341 name = os.path.join(*rel)
342 auditor(name)
342 auditor(name)
343 return util.pconvert(name)
343 return util.pconvert(name)
344 dirname, basename = os.path.split(name)
344 dirname, basename = os.path.split(name)
345 rel.append(basename)
345 rel.append(basename)
346 if dirname == name:
346 if dirname == name:
347 break
347 break
348 name = dirname
348 name = dirname
349
349
350 raise util.Abort('%s not under root' % myname)
350 raise util.Abort('%s not under root' % myname)
351
351
352 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
352 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
353 '''yield every hg repository under path, recursively.'''
353 '''yield every hg repository under path, always recursively.
354 The recurse flag will only control recursion into repo working dirs'''
354 def errhandler(err):
355 def errhandler(err):
355 if err.filename == path:
356 if err.filename == path:
356 raise err
357 raise err
357 samestat = getattr(os.path, 'samestat', None)
358 samestat = getattr(os.path, 'samestat', None)
358 if followsym and samestat is not None:
359 if followsym and samestat is not None:
359 def adddir(dirlst, dirname):
360 def adddir(dirlst, dirname):
360 match = False
361 match = False
361 dirstat = os.stat(dirname)
362 dirstat = os.stat(dirname)
362 for lstdirstat in dirlst:
363 for lstdirstat in dirlst:
363 if samestat(dirstat, lstdirstat):
364 if samestat(dirstat, lstdirstat):
364 match = True
365 match = True
365 break
366 break
366 if not match:
367 if not match:
367 dirlst.append(dirstat)
368 dirlst.append(dirstat)
368 return not match
369 return not match
369 else:
370 else:
370 followsym = False
371 followsym = False
371
372
372 if (seen_dirs is None) and followsym:
373 if (seen_dirs is None) and followsym:
373 seen_dirs = []
374 seen_dirs = []
374 adddir(seen_dirs, path)
375 adddir(seen_dirs, path)
375 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
376 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
376 dirs.sort()
377 dirs.sort()
377 if '.hg' in dirs:
378 if '.hg' in dirs:
378 yield root # found a repository
379 yield root # found a repository
379 qroot = os.path.join(root, '.hg', 'patches')
380 qroot = os.path.join(root, '.hg', 'patches')
380 if os.path.isdir(os.path.join(qroot, '.hg')):
381 if os.path.isdir(os.path.join(qroot, '.hg')):
381 yield qroot # we have a patch queue repo here
382 yield qroot # we have a patch queue repo here
382 if recurse:
383 if recurse:
383 # avoid recursing inside the .hg directory
384 # avoid recursing inside the .hg directory
384 dirs.remove('.hg')
385 dirs.remove('.hg')
385 else:
386 else:
386 dirs[:] = [] # don't descend further
387 dirs[:] = [] # don't descend further
387 elif followsym:
388 elif followsym:
388 newdirs = []
389 newdirs = []
389 for d in dirs:
390 for d in dirs:
390 fname = os.path.join(root, d)
391 fname = os.path.join(root, d)
391 if adddir(seen_dirs, fname):
392 if adddir(seen_dirs, fname):
392 if os.path.islink(fname):
393 if os.path.islink(fname):
393 for hgname in walkrepos(fname, True, seen_dirs):
394 for hgname in walkrepos(fname, True, seen_dirs):
394 yield hgname
395 yield hgname
395 else:
396 else:
396 newdirs.append(d)
397 newdirs.append(d)
397 dirs[:] = newdirs
398 dirs[:] = newdirs
398
399
399 def osrcpath():
400 def osrcpath():
400 '''return default os-specific hgrc search path'''
401 '''return default os-specific hgrc search path'''
401 path = systemrcpath()
402 path = systemrcpath()
402 path.extend(userrcpath())
403 path.extend(userrcpath())
403 path = [os.path.normpath(f) for f in path]
404 path = [os.path.normpath(f) for f in path]
404 return path
405 return path
405
406
406 _rcpath = None
407 _rcpath = None
407
408
408 def rcpath():
409 def rcpath():
409 '''return hgrc search path. if env var HGRCPATH is set, use it.
410 '''return hgrc search path. if env var HGRCPATH is set, use it.
410 for each item in path, if directory, use files ending in .rc,
411 for each item in path, if directory, use files ending in .rc,
411 else use item.
412 else use item.
412 make HGRCPATH empty to only look in .hg/hgrc of current repo.
413 make HGRCPATH empty to only look in .hg/hgrc of current repo.
413 if no HGRCPATH, use default os-specific path.'''
414 if no HGRCPATH, use default os-specific path.'''
414 global _rcpath
415 global _rcpath
415 if _rcpath is None:
416 if _rcpath is None:
416 if 'HGRCPATH' in os.environ:
417 if 'HGRCPATH' in os.environ:
417 _rcpath = []
418 _rcpath = []
418 for p in os.environ['HGRCPATH'].split(os.pathsep):
419 for p in os.environ['HGRCPATH'].split(os.pathsep):
419 if not p:
420 if not p:
420 continue
421 continue
421 p = util.expandpath(p)
422 p = util.expandpath(p)
422 if os.path.isdir(p):
423 if os.path.isdir(p):
423 for f, kind in osutil.listdir(p):
424 for f, kind in osutil.listdir(p):
424 if f.endswith('.rc'):
425 if f.endswith('.rc'):
425 _rcpath.append(os.path.join(p, f))
426 _rcpath.append(os.path.join(p, f))
426 else:
427 else:
427 _rcpath.append(p)
428 _rcpath.append(p)
428 else:
429 else:
429 _rcpath = osrcpath()
430 _rcpath = osrcpath()
430 return _rcpath
431 return _rcpath
431
432
432 if os.name != 'nt':
433 if os.name != 'nt':
433
434
434 def rcfiles(path):
435 def rcfiles(path):
435 rcs = [os.path.join(path, 'hgrc')]
436 rcs = [os.path.join(path, 'hgrc')]
436 rcdir = os.path.join(path, 'hgrc.d')
437 rcdir = os.path.join(path, 'hgrc.d')
437 try:
438 try:
438 rcs.extend([os.path.join(rcdir, f)
439 rcs.extend([os.path.join(rcdir, f)
439 for f, kind in osutil.listdir(rcdir)
440 for f, kind in osutil.listdir(rcdir)
440 if f.endswith(".rc")])
441 if f.endswith(".rc")])
441 except OSError:
442 except OSError:
442 pass
443 pass
443 return rcs
444 return rcs
444
445
445 def systemrcpath():
446 def systemrcpath():
446 path = []
447 path = []
447 if sys.platform == 'plan9':
448 if sys.platform == 'plan9':
448 root = 'lib/mercurial'
449 root = 'lib/mercurial'
449 else:
450 else:
450 root = 'etc/mercurial'
451 root = 'etc/mercurial'
451 # old mod_python does not set sys.argv
452 # old mod_python does not set sys.argv
452 if len(getattr(sys, 'argv', [])) > 0:
453 if len(getattr(sys, 'argv', [])) > 0:
453 p = os.path.dirname(os.path.dirname(sys.argv[0]))
454 p = os.path.dirname(os.path.dirname(sys.argv[0]))
454 path.extend(rcfiles(os.path.join(p, root)))
455 path.extend(rcfiles(os.path.join(p, root)))
455 path.extend(rcfiles('/' + root))
456 path.extend(rcfiles('/' + root))
456 return path
457 return path
457
458
458 def userrcpath():
459 def userrcpath():
459 if sys.platform == 'plan9':
460 if sys.platform == 'plan9':
460 return [os.environ['home'] + '/lib/hgrc']
461 return [os.environ['home'] + '/lib/hgrc']
461 else:
462 else:
462 return [os.path.expanduser('~/.hgrc')]
463 return [os.path.expanduser('~/.hgrc')]
463
464
464 else:
465 else:
465
466
466 import _winreg
467 import _winreg
467
468
468 def systemrcpath():
469 def systemrcpath():
469 '''return default os-specific hgrc search path'''
470 '''return default os-specific hgrc search path'''
470 rcpath = []
471 rcpath = []
471 filename = util.executablepath()
472 filename = util.executablepath()
472 # Use mercurial.ini found in directory with hg.exe
473 # Use mercurial.ini found in directory with hg.exe
473 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
474 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
474 if os.path.isfile(progrc):
475 if os.path.isfile(progrc):
475 rcpath.append(progrc)
476 rcpath.append(progrc)
476 return rcpath
477 return rcpath
477 # Use hgrc.d found in directory with hg.exe
478 # Use hgrc.d found in directory with hg.exe
478 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
479 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
479 if os.path.isdir(progrcd):
480 if os.path.isdir(progrcd):
480 for f, kind in osutil.listdir(progrcd):
481 for f, kind in osutil.listdir(progrcd):
481 if f.endswith('.rc'):
482 if f.endswith('.rc'):
482 rcpath.append(os.path.join(progrcd, f))
483 rcpath.append(os.path.join(progrcd, f))
483 return rcpath
484 return rcpath
484 # else look for a system rcpath in the registry
485 # else look for a system rcpath in the registry
485 value = util.lookupreg('SOFTWARE\\Mercurial', None,
486 value = util.lookupreg('SOFTWARE\\Mercurial', None,
486 _winreg.HKEY_LOCAL_MACHINE)
487 _winreg.HKEY_LOCAL_MACHINE)
487 if not isinstance(value, str) or not value:
488 if not isinstance(value, str) or not value:
488 return rcpath
489 return rcpath
489 value = util.localpath(value)
490 value = util.localpath(value)
490 for p in value.split(os.pathsep):
491 for p in value.split(os.pathsep):
491 if p.lower().endswith('mercurial.ini'):
492 if p.lower().endswith('mercurial.ini'):
492 rcpath.append(p)
493 rcpath.append(p)
493 elif os.path.isdir(p):
494 elif os.path.isdir(p):
494 for f, kind in osutil.listdir(p):
495 for f, kind in osutil.listdir(p):
495 if f.endswith('.rc'):
496 if f.endswith('.rc'):
496 rcpath.append(os.path.join(p, f))
497 rcpath.append(os.path.join(p, f))
497 return rcpath
498 return rcpath
498
499
499 def userrcpath():
500 def userrcpath():
500 '''return os-specific hgrc search path to the user dir'''
501 '''return os-specific hgrc search path to the user dir'''
501 home = os.path.expanduser('~')
502 home = os.path.expanduser('~')
502 path = [os.path.join(home, 'mercurial.ini'),
503 path = [os.path.join(home, 'mercurial.ini'),
503 os.path.join(home, '.hgrc')]
504 os.path.join(home, '.hgrc')]
504 userprofile = os.environ.get('USERPROFILE')
505 userprofile = os.environ.get('USERPROFILE')
505 if userprofile:
506 if userprofile:
506 path.append(os.path.join(userprofile, 'mercurial.ini'))
507 path.append(os.path.join(userprofile, 'mercurial.ini'))
507 path.append(os.path.join(userprofile, '.hgrc'))
508 path.append(os.path.join(userprofile, '.hgrc'))
508 return path
509 return path
509
510
510 def revsingle(repo, revspec, default='.'):
511 def revsingle(repo, revspec, default='.'):
511 if not revspec:
512 if not revspec:
512 return repo[default]
513 return repo[default]
513
514
514 l = revrange(repo, [revspec])
515 l = revrange(repo, [revspec])
515 if len(l) < 1:
516 if len(l) < 1:
516 raise util.Abort(_('empty revision set'))
517 raise util.Abort(_('empty revision set'))
517 return repo[l[-1]]
518 return repo[l[-1]]
518
519
519 def revpair(repo, revs):
520 def revpair(repo, revs):
520 if not revs:
521 if not revs:
521 return repo.dirstate.p1(), None
522 return repo.dirstate.p1(), None
522
523
523 l = revrange(repo, revs)
524 l = revrange(repo, revs)
524
525
525 if len(l) == 0:
526 if len(l) == 0:
526 if revs:
527 if revs:
527 raise util.Abort(_('empty revision range'))
528 raise util.Abort(_('empty revision range'))
528 return repo.dirstate.p1(), None
529 return repo.dirstate.p1(), None
529
530
530 if len(l) == 1 and len(revs) == 1 and _revrangesep not in revs[0]:
531 if len(l) == 1 and len(revs) == 1 and _revrangesep not in revs[0]:
531 return repo.lookup(l[0]), None
532 return repo.lookup(l[0]), None
532
533
533 return repo.lookup(l[0]), repo.lookup(l[-1])
534 return repo.lookup(l[0]), repo.lookup(l[-1])
534
535
535 _revrangesep = ':'
536 _revrangesep = ':'
536
537
537 def revrange(repo, revs):
538 def revrange(repo, revs):
538 """Yield revision as strings from a list of revision specifications."""
539 """Yield revision as strings from a list of revision specifications."""
539
540
540 def revfix(repo, val, defval):
541 def revfix(repo, val, defval):
541 if not val and val != 0 and defval is not None:
542 if not val and val != 0 and defval is not None:
542 return defval
543 return defval
543 return repo[val].rev()
544 return repo[val].rev()
544
545
545 seen, l = set(), []
546 seen, l = set(), []
546 for spec in revs:
547 for spec in revs:
547 if l and not seen:
548 if l and not seen:
548 seen = set(l)
549 seen = set(l)
549 # attempt to parse old-style ranges first to deal with
550 # attempt to parse old-style ranges first to deal with
550 # things like old-tag which contain query metacharacters
551 # things like old-tag which contain query metacharacters
551 try:
552 try:
552 if isinstance(spec, int):
553 if isinstance(spec, int):
553 seen.add(spec)
554 seen.add(spec)
554 l.append(spec)
555 l.append(spec)
555 continue
556 continue
556
557
557 if _revrangesep in spec:
558 if _revrangesep in spec:
558 start, end = spec.split(_revrangesep, 1)
559 start, end = spec.split(_revrangesep, 1)
559 start = revfix(repo, start, 0)
560 start = revfix(repo, start, 0)
560 end = revfix(repo, end, len(repo) - 1)
561 end = revfix(repo, end, len(repo) - 1)
561 step = start > end and -1 or 1
562 step = start > end and -1 or 1
562 if not seen and not l:
563 if not seen and not l:
563 # by far the most common case: revs = ["-1:0"]
564 # by far the most common case: revs = ["-1:0"]
564 l = range(start, end + step, step)
565 l = range(start, end + step, step)
565 # defer syncing seen until next iteration
566 # defer syncing seen until next iteration
566 continue
567 continue
567 newrevs = set(xrange(start, end + step, step))
568 newrevs = set(xrange(start, end + step, step))
568 if seen:
569 if seen:
569 newrevs.difference_update(seen)
570 newrevs.difference_update(seen)
570 seen.update(newrevs)
571 seen.update(newrevs)
571 else:
572 else:
572 seen = newrevs
573 seen = newrevs
573 l.extend(sorted(newrevs, reverse=start > end))
574 l.extend(sorted(newrevs, reverse=start > end))
574 continue
575 continue
575 elif spec and spec in repo: # single unquoted rev
576 elif spec and spec in repo: # single unquoted rev
576 rev = revfix(repo, spec, None)
577 rev = revfix(repo, spec, None)
577 if rev in seen:
578 if rev in seen:
578 continue
579 continue
579 seen.add(rev)
580 seen.add(rev)
580 l.append(rev)
581 l.append(rev)
581 continue
582 continue
582 except error.RepoLookupError:
583 except error.RepoLookupError:
583 pass
584 pass
584
585
585 # fall through to new-style queries if old-style fails
586 # fall through to new-style queries if old-style fails
586 m = revset.match(repo.ui, spec)
587 m = revset.match(repo.ui, spec)
587 dl = [r for r in m(repo, xrange(len(repo))) if r not in seen]
588 dl = [r for r in m(repo, xrange(len(repo))) if r not in seen]
588 l.extend(dl)
589 l.extend(dl)
589 seen.update(dl)
590 seen.update(dl)
590
591
591 return l
592 return l
592
593
593 def expandpats(pats):
594 def expandpats(pats):
594 if not util.expandglobs:
595 if not util.expandglobs:
595 return list(pats)
596 return list(pats)
596 ret = []
597 ret = []
597 for p in pats:
598 for p in pats:
598 kind, name = matchmod._patsplit(p, None)
599 kind, name = matchmod._patsplit(p, None)
599 if kind is None:
600 if kind is None:
600 try:
601 try:
601 globbed = glob.glob(name)
602 globbed = glob.glob(name)
602 except re.error:
603 except re.error:
603 globbed = [name]
604 globbed = [name]
604 if globbed:
605 if globbed:
605 ret.extend(globbed)
606 ret.extend(globbed)
606 continue
607 continue
607 ret.append(p)
608 ret.append(p)
608 return ret
609 return ret
609
610
610 def matchandpats(ctx, pats=[], opts={}, globbed=False, default='relpath'):
611 def matchandpats(ctx, pats=[], opts={}, globbed=False, default='relpath'):
611 if pats == ("",):
612 if pats == ("",):
612 pats = []
613 pats = []
613 if not globbed and default == 'relpath':
614 if not globbed and default == 'relpath':
614 pats = expandpats(pats or [])
615 pats = expandpats(pats or [])
615
616
616 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
617 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
617 default)
618 default)
618 def badfn(f, msg):
619 def badfn(f, msg):
619 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
620 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
620 m.bad = badfn
621 m.bad = badfn
621 return m, pats
622 return m, pats
622
623
623 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
624 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
624 return matchandpats(ctx, pats, opts, globbed, default)[0]
625 return matchandpats(ctx, pats, opts, globbed, default)[0]
625
626
626 def matchall(repo):
627 def matchall(repo):
627 return matchmod.always(repo.root, repo.getcwd())
628 return matchmod.always(repo.root, repo.getcwd())
628
629
629 def matchfiles(repo, files):
630 def matchfiles(repo, files):
630 return matchmod.exact(repo.root, repo.getcwd(), files)
631 return matchmod.exact(repo.root, repo.getcwd(), files)
631
632
632 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
633 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
633 if dry_run is None:
634 if dry_run is None:
634 dry_run = opts.get('dry_run')
635 dry_run = opts.get('dry_run')
635 if similarity is None:
636 if similarity is None:
636 similarity = float(opts.get('similarity') or 0)
637 similarity = float(opts.get('similarity') or 0)
637 # we'd use status here, except handling of symlinks and ignore is tricky
638 # we'd use status here, except handling of symlinks and ignore is tricky
638 added, unknown, deleted, removed = [], [], [], []
639 added, unknown, deleted, removed = [], [], [], []
639 audit_path = pathauditor(repo.root)
640 audit_path = pathauditor(repo.root)
640 m = match(repo[None], pats, opts)
641 m = match(repo[None], pats, opts)
641 rejected = []
642 rejected = []
642 m.bad = lambda x, y: rejected.append(x)
643 m.bad = lambda x, y: rejected.append(x)
643
644
644 for abs in repo.walk(m):
645 for abs in repo.walk(m):
645 target = repo.wjoin(abs)
646 target = repo.wjoin(abs)
646 good = True
647 good = True
647 try:
648 try:
648 audit_path(abs)
649 audit_path(abs)
649 except (OSError, util.Abort):
650 except (OSError, util.Abort):
650 good = False
651 good = False
651 rel = m.rel(abs)
652 rel = m.rel(abs)
652 exact = m.exact(abs)
653 exact = m.exact(abs)
653 if good and abs not in repo.dirstate:
654 if good and abs not in repo.dirstate:
654 unknown.append(abs)
655 unknown.append(abs)
655 if repo.ui.verbose or not exact:
656 if repo.ui.verbose or not exact:
656 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
657 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
657 elif (repo.dirstate[abs] != 'r' and
658 elif (repo.dirstate[abs] != 'r' and
658 (not good or not os.path.lexists(target) or
659 (not good or not os.path.lexists(target) or
659 (os.path.isdir(target) and not os.path.islink(target)))):
660 (os.path.isdir(target) and not os.path.islink(target)))):
660 deleted.append(abs)
661 deleted.append(abs)
661 if repo.ui.verbose or not exact:
662 if repo.ui.verbose or not exact:
662 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
663 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
663 # for finding renames
664 # for finding renames
664 elif repo.dirstate[abs] == 'r':
665 elif repo.dirstate[abs] == 'r':
665 removed.append(abs)
666 removed.append(abs)
666 elif repo.dirstate[abs] == 'a':
667 elif repo.dirstate[abs] == 'a':
667 added.append(abs)
668 added.append(abs)
668 copies = {}
669 copies = {}
669 if similarity > 0:
670 if similarity > 0:
670 for old, new, score in similar.findrenames(repo,
671 for old, new, score in similar.findrenames(repo,
671 added + unknown, removed + deleted, similarity):
672 added + unknown, removed + deleted, similarity):
672 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
673 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
673 repo.ui.status(_('recording removal of %s as rename to %s '
674 repo.ui.status(_('recording removal of %s as rename to %s '
674 '(%d%% similar)\n') %
675 '(%d%% similar)\n') %
675 (m.rel(old), m.rel(new), score * 100))
676 (m.rel(old), m.rel(new), score * 100))
676 copies[new] = old
677 copies[new] = old
677
678
678 if not dry_run:
679 if not dry_run:
679 wctx = repo[None]
680 wctx = repo[None]
680 wlock = repo.wlock()
681 wlock = repo.wlock()
681 try:
682 try:
682 wctx.forget(deleted)
683 wctx.forget(deleted)
683 wctx.add(unknown)
684 wctx.add(unknown)
684 for new, old in copies.iteritems():
685 for new, old in copies.iteritems():
685 wctx.copy(old, new)
686 wctx.copy(old, new)
686 finally:
687 finally:
687 wlock.release()
688 wlock.release()
688
689
689 for f in rejected:
690 for f in rejected:
690 if f in m.files():
691 if f in m.files():
691 return 1
692 return 1
692 return 0
693 return 0
693
694
694 def updatedir(ui, repo, patches, similarity=0):
695 def updatedir(ui, repo, patches, similarity=0):
695 '''Update dirstate after patch application according to metadata'''
696 '''Update dirstate after patch application according to metadata'''
696 if not patches:
697 if not patches:
697 return []
698 return []
698 copies = []
699 copies = []
699 removes = set()
700 removes = set()
700 cfiles = patches.keys()
701 cfiles = patches.keys()
701 cwd = repo.getcwd()
702 cwd = repo.getcwd()
702 if cwd:
703 if cwd:
703 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
704 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
704 for f in patches:
705 for f in patches:
705 gp = patches[f]
706 gp = patches[f]
706 if not gp:
707 if not gp:
707 continue
708 continue
708 if gp.op == 'RENAME':
709 if gp.op == 'RENAME':
709 copies.append((gp.oldpath, gp.path))
710 copies.append((gp.oldpath, gp.path))
710 removes.add(gp.oldpath)
711 removes.add(gp.oldpath)
711 elif gp.op == 'COPY':
712 elif gp.op == 'COPY':
712 copies.append((gp.oldpath, gp.path))
713 copies.append((gp.oldpath, gp.path))
713 elif gp.op == 'DELETE':
714 elif gp.op == 'DELETE':
714 removes.add(gp.path)
715 removes.add(gp.path)
715
716
716 wctx = repo[None]
717 wctx = repo[None]
717 for src, dst in copies:
718 for src, dst in copies:
718 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
719 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
719 if (not similarity) and removes:
720 if (not similarity) and removes:
720 wctx.remove(sorted(removes), True)
721 wctx.remove(sorted(removes), True)
721
722
722 for f in patches:
723 for f in patches:
723 gp = patches[f]
724 gp = patches[f]
724 if gp and gp.mode:
725 if gp and gp.mode:
725 islink, isexec = gp.mode
726 islink, isexec = gp.mode
726 dst = repo.wjoin(gp.path)
727 dst = repo.wjoin(gp.path)
727 # patch won't create empty files
728 # patch won't create empty files
728 if gp.op == 'ADD' and not os.path.lexists(dst):
729 if gp.op == 'ADD' and not os.path.lexists(dst):
729 flags = (isexec and 'x' or '') + (islink and 'l' or '')
730 flags = (isexec and 'x' or '') + (islink and 'l' or '')
730 repo.wwrite(gp.path, '', flags)
731 repo.wwrite(gp.path, '', flags)
731 util.setflags(dst, islink, isexec)
732 util.setflags(dst, islink, isexec)
732 addremove(repo, cfiles, similarity=similarity)
733 addremove(repo, cfiles, similarity=similarity)
733 files = patches.keys()
734 files = patches.keys()
734 files.extend([r for r in removes if r not in files])
735 files.extend([r for r in removes if r not in files])
735 return sorted(files)
736 return sorted(files)
736
737
737 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
738 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
738 """Update the dirstate to reflect the intent of copying src to dst. For
739 """Update the dirstate to reflect the intent of copying src to dst. For
739 different reasons it might not end with dst being marked as copied from src.
740 different reasons it might not end with dst being marked as copied from src.
740 """
741 """
741 origsrc = repo.dirstate.copied(src) or src
742 origsrc = repo.dirstate.copied(src) or src
742 if dst == origsrc: # copying back a copy?
743 if dst == origsrc: # copying back a copy?
743 if repo.dirstate[dst] not in 'mn' and not dryrun:
744 if repo.dirstate[dst] not in 'mn' and not dryrun:
744 repo.dirstate.normallookup(dst)
745 repo.dirstate.normallookup(dst)
745 else:
746 else:
746 if repo.dirstate[origsrc] == 'a' and origsrc == src:
747 if repo.dirstate[origsrc] == 'a' and origsrc == src:
747 if not ui.quiet:
748 if not ui.quiet:
748 ui.warn(_("%s has not been committed yet, so no copy "
749 ui.warn(_("%s has not been committed yet, so no copy "
749 "data will be stored for %s.\n")
750 "data will be stored for %s.\n")
750 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
751 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
751 if repo.dirstate[dst] in '?r' and not dryrun:
752 if repo.dirstate[dst] in '?r' and not dryrun:
752 wctx.add([dst])
753 wctx.add([dst])
753 elif not dryrun:
754 elif not dryrun:
754 wctx.copy(origsrc, dst)
755 wctx.copy(origsrc, dst)
755
756
756 def readrequires(opener, supported):
757 def readrequires(opener, supported):
757 '''Reads and parses .hg/requires and checks if all entries found
758 '''Reads and parses .hg/requires and checks if all entries found
758 are in the list of supported features.'''
759 are in the list of supported features.'''
759 requirements = set(opener.read("requires").splitlines())
760 requirements = set(opener.read("requires").splitlines())
760 missings = []
761 missings = []
761 for r in requirements:
762 for r in requirements:
762 if r not in supported:
763 if r not in supported:
763 if not r or not r[0].isalnum():
764 if not r or not r[0].isalnum():
764 raise error.RequirementError(_(".hg/requires file is corrupt"))
765 raise error.RequirementError(_(".hg/requires file is corrupt"))
765 missings.append(r)
766 missings.append(r)
766 missings.sort()
767 missings.sort()
767 if missings:
768 if missings:
768 raise error.RequirementError(
769 raise error.RequirementError(
769 _("unknown repository format: requires features '%s' (upgrade "
770 _("unknown repository format: requires features '%s' (upgrade "
770 "Mercurial)") % "', '".join(missings))
771 "Mercurial)") % "', '".join(missings))
771 return requirements
772 return requirements
772
773
773 class filecacheentry(object):
774 class filecacheentry(object):
774 def __init__(self, path):
775 def __init__(self, path):
775 self.path = path
776 self.path = path
776 self.cachestat = filecacheentry.stat(self.path)
777 self.cachestat = filecacheentry.stat(self.path)
777
778
778 if self.cachestat:
779 if self.cachestat:
779 self._cacheable = self.cachestat.cacheable()
780 self._cacheable = self.cachestat.cacheable()
780 else:
781 else:
781 # None means we don't know yet
782 # None means we don't know yet
782 self._cacheable = None
783 self._cacheable = None
783
784
784 def refresh(self):
785 def refresh(self):
785 if self.cacheable():
786 if self.cacheable():
786 self.cachestat = filecacheentry.stat(self.path)
787 self.cachestat = filecacheentry.stat(self.path)
787
788
788 def cacheable(self):
789 def cacheable(self):
789 if self._cacheable is not None:
790 if self._cacheable is not None:
790 return self._cacheable
791 return self._cacheable
791
792
792 # we don't know yet, assume it is for now
793 # we don't know yet, assume it is for now
793 return True
794 return True
794
795
795 def changed(self):
796 def changed(self):
796 # no point in going further if we can't cache it
797 # no point in going further if we can't cache it
797 if not self.cacheable():
798 if not self.cacheable():
798 return True
799 return True
799
800
800 newstat = filecacheentry.stat(self.path)
801 newstat = filecacheentry.stat(self.path)
801
802
802 # we may not know if it's cacheable yet, check again now
803 # we may not know if it's cacheable yet, check again now
803 if newstat and self._cacheable is None:
804 if newstat and self._cacheable is None:
804 self._cacheable = newstat.cacheable()
805 self._cacheable = newstat.cacheable()
805
806
806 # check again
807 # check again
807 if not self._cacheable:
808 if not self._cacheable:
808 return True
809 return True
809
810
810 if self.cachestat != newstat:
811 if self.cachestat != newstat:
811 self.cachestat = newstat
812 self.cachestat = newstat
812 return True
813 return True
813 else:
814 else:
814 return False
815 return False
815
816
816 @staticmethod
817 @staticmethod
817 def stat(path):
818 def stat(path):
818 try:
819 try:
819 return util.cachestat(path)
820 return util.cachestat(path)
820 except OSError, e:
821 except OSError, e:
821 if e.errno != errno.ENOENT:
822 if e.errno != errno.ENOENT:
822 raise
823 raise
823
824
824 class filecache(object):
825 class filecache(object):
825 '''A property like decorator that tracks a file under .hg/ for updates.
826 '''A property like decorator that tracks a file under .hg/ for updates.
826
827
827 Records stat info when called in _filecache.
828 Records stat info when called in _filecache.
828
829
829 On subsequent calls, compares old stat info with new info, and recreates
830 On subsequent calls, compares old stat info with new info, and recreates
830 the object when needed, updating the new stat info in _filecache.
831 the object when needed, updating the new stat info in _filecache.
831
832
832 Mercurial either atomic renames or appends for files under .hg,
833 Mercurial either atomic renames or appends for files under .hg,
833 so to ensure the cache is reliable we need the filesystem to be able
834 so to ensure the cache is reliable we need the filesystem to be able
834 to tell us if a file has been replaced. If it can't, we fallback to
835 to tell us if a file has been replaced. If it can't, we fallback to
835 recreating the object on every call (essentially the same behaviour as
836 recreating the object on every call (essentially the same behaviour as
836 propertycache).'''
837 propertycache).'''
837 def __init__(self, path):
838 def __init__(self, path):
838 self.path = path
839 self.path = path
839
840
840 def join(self, obj, fname):
841 def join(self, obj, fname):
841 """Used to compute the runtime path of the cached file.
842 """Used to compute the runtime path of the cached file.
842
843
843 Users should subclass filecache and provide their own version of this
844 Users should subclass filecache and provide their own version of this
844 function to call the appropriate join function on 'obj' (an instance
845 function to call the appropriate join function on 'obj' (an instance
845 of the class that its member function was decorated).
846 of the class that its member function was decorated).
846 """
847 """
847 return obj.join(fname)
848 return obj.join(fname)
848
849
849 def __call__(self, func):
850 def __call__(self, func):
850 self.func = func
851 self.func = func
851 self.name = func.__name__
852 self.name = func.__name__
852 return self
853 return self
853
854
854 def __get__(self, obj, type=None):
855 def __get__(self, obj, type=None):
855 # do we need to check if the file changed?
856 # do we need to check if the file changed?
856 if self.name in obj.__dict__:
857 if self.name in obj.__dict__:
857 return obj.__dict__[self.name]
858 return obj.__dict__[self.name]
858
859
859 entry = obj._filecache.get(self.name)
860 entry = obj._filecache.get(self.name)
860
861
861 if entry:
862 if entry:
862 if entry.changed():
863 if entry.changed():
863 entry.obj = self.func(obj)
864 entry.obj = self.func(obj)
864 else:
865 else:
865 path = self.join(obj, self.path)
866 path = self.join(obj, self.path)
866
867
867 # We stat -before- creating the object so our cache doesn't lie if
868 # We stat -before- creating the object so our cache doesn't lie if
868 # a writer modified between the time we read and stat
869 # a writer modified between the time we read and stat
869 entry = filecacheentry(path)
870 entry = filecacheentry(path)
870 entry.obj = self.func(obj)
871 entry.obj = self.func(obj)
871
872
872 obj._filecache[self.name] = entry
873 obj._filecache[self.name] = entry
873
874
874 obj.__dict__[self.name] = entry.obj
875 obj.__dict__[self.name] = entry.obj
875 return entry.obj
876 return entry.obj
876
877
877 def __set__(self, obj, value):
878 def __set__(self, obj, value):
878 if self.name in obj._filecache:
879 if self.name in obj._filecache:
879 obj._filecache[self.name].obj = value # update cached copy
880 obj._filecache[self.name].obj = value # update cached copy
880 obj.__dict__[self.name] = value # update copy returned by obj.x
881 obj.__dict__[self.name] = value # update copy returned by obj.x
881
882
882 def __delete__(self, obj):
883 def __delete__(self, obj):
883 try:
884 try:
884 del obj.__dict__[self.name]
885 del obj.__dict__[self.name]
885 except KeyError:
886 except KeyError:
886 raise AttributeError, self.name
887 raise AttributeError, self.name
General Comments 0
You need to be logged in to leave comments. Login now