##// END OF EJS Templates
hgweb: recurse down collections only if ** in [paths]...
Benoit Allard -
r7523:e60aaae8 default
parent child Browse files
Show More
@@ -1,63 +1,67 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # An example CGI script to export multiple hgweb repos, edit as necessary
3 # An example CGI script to export multiple hgweb repos, edit as necessary
4
4
5 # adjust python path if not a system-wide install:
5 # adjust python path if not a system-wide install:
6 #import sys
6 #import sys
7 #sys.path.insert(0, "/path/to/python/lib")
7 #sys.path.insert(0, "/path/to/python/lib")
8
8
9 # enable importing on demand to reduce startup time
9 # enable importing on demand to reduce startup time
10 from mercurial import demandimport; demandimport.enable()
10 from mercurial import demandimport; demandimport.enable()
11
11
12 # Uncomment to send python tracebacks to the browser if an error occurs:
12 # Uncomment to send python tracebacks to the browser if an error occurs:
13 #import cgitb
13 #import cgitb
14 #cgitb.enable()
14 #cgitb.enable()
15
15
16 # If you'd like to serve pages with UTF-8 instead of your default
16 # If you'd like to serve pages with UTF-8 instead of your default
17 # locale charset, you can do so by uncommenting the following lines.
17 # locale charset, you can do so by uncommenting the following lines.
18 # Note that this will cause your .hgrc files to be interpreted in
18 # Note that this will cause your .hgrc files to be interpreted in
19 # UTF-8 and all your repo files to be displayed using UTF-8.
19 # UTF-8 and all your repo files to be displayed using UTF-8.
20 #
20 #
21 #import os
21 #import os
22 #os.environ["HGENCODING"] = "UTF-8"
22 #os.environ["HGENCODING"] = "UTF-8"
23
23
24 from mercurial.hgweb.hgwebdir_mod import hgwebdir
24 from mercurial.hgweb.hgwebdir_mod import hgwebdir
25 import mercurial.hgweb.wsgicgi as wsgicgi
25 import mercurial.hgweb.wsgicgi as wsgicgi
26
26
27 # The config file looks like this. You can have paths to individual
27 # The config file looks like this. You can have paths to individual
28 # repos, collections of repos in a directory tree, or both.
28 # repos, collections of repos in a directory tree, or both.
29 #
29 #
30 # [paths]
30 # [paths]
31 # virtual/path1 = /real/path1
31 # virtual/path1 = /real/path1
32 # virtual/path2 = /real/path2
32 # virtual/path2 = /real/path2
33 # virtual/root = /real/root/*
33 # virtual/root = /real/root/*
34 # / = /real/root2/*
34 # / = /real/root2/*
35 # virtual/root2 = /real/root2/**
35 #
36 #
36 # [collections]
37 # [collections]
37 # /prefix/to/strip/off = /root/of/tree/full/of/repos
38 # /prefix/to/strip/off = /root/of/tree/full/of/repos
38 #
39 #
39 # paths example:
40 # paths example:
40 #
41 #
41 # * First two lines mount one repository into one virtual path, like
42 # * First two lines mount one repository into one virtual path, like
42 # '/real/path1' into 'virtual/path1'.
43 # '/real/path1' into 'virtual/path1'.
43 #
44 #
44 # * The third entry tells every mercurial repository found in
45 # * The third entry mounts every mercurial repository found in '/real/root'
45 # '/real/root', recursively, should be mounted in 'virtual/root'. This
46 # in 'virtual/root'. This format is preferred over the [collections] one,
46 # format is preferred over the [collections] one, using absolute paths
47 # since using absolute paths as configuration keys is not support on every
47 # as configuration keys is not supported on every platform (including
48 # platform (especially on Windows).
48 # Windows).
49 #
49 #
50 # * The last entry is a special case mounting all repositories in
50 # * The fourth entry is a special case mounting all repositories in
51 # /'real/root2' in the root of the virtual directory.
51 # /'real/root2' in the root of the virtual directory.
52 #
52 #
53 # * The fifth entry recursively finds all repositories under the real root,
54 # and mounts them using their relative path (to given real root) under the
55 # virtual root.
56 #
53 # collections example: say directory tree /foo contains repos /foo/bar,
57 # collections example: say directory tree /foo contains repos /foo/bar,
54 # /foo/quux/baz. Give this config section:
58 # /foo/quux/baz. Give this config section:
55 # [collections]
59 # [collections]
56 # /foo = /foo
60 # /foo = /foo
57 # Then repos will list as bar and quux/baz.
61 # Then repos will list as bar and quux/baz.
58 #
62 #
59 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
63 # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples
60 # or use a dictionary with entries like 'virtual/path': '/real/path'
64 # or use a dictionary with entries like 'virtual/path': '/real/path'
61
65
62 application = hgwebdir('hgweb.config')
66 application = hgwebdir('hgweb.config')
63 wsgicgi.launch(application)
67 wsgicgi.launch(application)
@@ -1,323 +1,328 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
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 import os
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import ui, hg, util, templater, templatefilters
12 from mercurial import ui, hg, util, templater, templatefilters
13 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
13 from common import ErrorResponse, get_mtime, staticfile, style_map, 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
17
18 # This is a stopgap
18 # This is a stopgap
19 class hgwebdir(object):
19 class hgwebdir(object):
20 def __init__(self, config, parentui=None):
20 def __init__(self, config, parentui=None):
21 def cleannames(items):
21 def cleannames(items):
22 return [(util.pconvert(name).strip('/'), path)
22 return [(util.pconvert(name).strip('/'), path)
23 for name, path in items]
23 for name, path in items]
24
24
25 self.parentui = parentui or ui.ui(report_untrusted=False,
25 self.parentui = parentui or ui.ui(report_untrusted=False,
26 interactive = False)
26 interactive = False)
27 self.motd = None
27 self.motd = None
28 self.style = 'paper'
28 self.style = 'paper'
29 self.stripecount = None
29 self.stripecount = None
30 self.repos_sorted = ('name', False)
30 self.repos_sorted = ('name', False)
31 self._baseurl = None
31 self._baseurl = None
32 if isinstance(config, (list, tuple)):
32 if isinstance(config, (list, tuple)):
33 self.repos = cleannames(config)
33 self.repos = cleannames(config)
34 self.repos_sorted = ('', False)
34 self.repos_sorted = ('', False)
35 elif isinstance(config, dict):
35 elif isinstance(config, dict):
36 self.repos = util.sort(cleannames(config.items()))
36 self.repos = util.sort(cleannames(config.items()))
37 else:
37 else:
38 if isinstance(config, util.configparser):
38 if isinstance(config, util.configparser):
39 cp = config
39 cp = config
40 else:
40 else:
41 cp = util.configparser()
41 cp = util.configparser()
42 cp.read(config)
42 cp.read(config)
43 self.repos = []
43 self.repos = []
44 if cp.has_section('web'):
44 if cp.has_section('web'):
45 if cp.has_option('web', 'motd'):
45 if cp.has_option('web', 'motd'):
46 self.motd = cp.get('web', 'motd')
46 self.motd = cp.get('web', 'motd')
47 if cp.has_option('web', 'style'):
47 if cp.has_option('web', 'style'):
48 self.style = cp.get('web', 'style')
48 self.style = cp.get('web', 'style')
49 if cp.has_option('web', 'stripes'):
49 if cp.has_option('web', 'stripes'):
50 self.stripecount = int(cp.get('web', 'stripes'))
50 self.stripecount = int(cp.get('web', 'stripes'))
51 if cp.has_option('web', 'baseurl'):
51 if cp.has_option('web', 'baseurl'):
52 self._baseurl = cp.get('web', 'baseurl')
52 self._baseurl = cp.get('web', 'baseurl')
53 if cp.has_section('paths'):
53 if cp.has_section('paths'):
54 paths = cleannames(cp.items('paths'))
54 paths = cleannames(cp.items('paths'))
55 for prefix, root in paths:
55 for prefix, root in paths:
56 roothead, roottail = os.path.split(root)
56 roothead, roottail = os.path.split(root)
57 if roottail != '*':
57 # "foo = /bar/*" makes every subrepo of /bar/ to be
58 # mounted as foo/subrepo
59 # and "foo = /bar/**" does even recurse inside the
60 # subdirectories, remember to use it without working dir.
61 try:
62 recurse = {'*': False, '**': True}[roottail]
63 except KeyError:
58 self.repos.append((prefix, root))
64 self.repos.append((prefix, root))
59 continue
65 continue
60 # "foo = /bar/*" makes every subrepo of /bar/ to be
61 # mounted as foo/subrepo
62 roothead = os.path.normpath(roothead)
66 roothead = os.path.normpath(roothead)
63 for path in util.walkrepos(roothead, followsym=True):
67 for path in util.walkrepos(roothead, followsym=True,
68 recurse=recurse):
64 path = os.path.normpath(path)
69 path = os.path.normpath(path)
65 name = util.pconvert(path[len(roothead):]).strip('/')
70 name = util.pconvert(path[len(roothead):]).strip('/')
66 if prefix:
71 if prefix:
67 name = prefix + '/' + name
72 name = prefix + '/' + name
68 self.repos.append((name, path))
73 self.repos.append((name, path))
69 if cp.has_section('collections'):
74 if cp.has_section('collections'):
70 for prefix, root in cp.items('collections'):
75 for prefix, root in cp.items('collections'):
71 for path in util.walkrepos(root, followsym=True):
76 for path in util.walkrepos(root, followsym=True):
72 repo = os.path.normpath(path)
77 repo = os.path.normpath(path)
73 name = repo
78 name = repo
74 if name.startswith(prefix):
79 if name.startswith(prefix):
75 name = name[len(prefix):]
80 name = name[len(prefix):]
76 self.repos.append((name.lstrip(os.sep), repo))
81 self.repos.append((name.lstrip(os.sep), repo))
77 self.repos.sort()
82 self.repos.sort()
78
83
79 def run(self):
84 def run(self):
80 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
85 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
81 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
86 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
82 import mercurial.hgweb.wsgicgi as wsgicgi
87 import mercurial.hgweb.wsgicgi as wsgicgi
83 wsgicgi.launch(self)
88 wsgicgi.launch(self)
84
89
85 def __call__(self, env, respond):
90 def __call__(self, env, respond):
86 req = wsgirequest(env, respond)
91 req = wsgirequest(env, respond)
87 return self.run_wsgi(req)
92 return self.run_wsgi(req)
88
93
89 def read_allowed(self, ui, req):
94 def read_allowed(self, ui, req):
90 """Check allow_read and deny_read config options of a repo's ui object
95 """Check allow_read and deny_read config options of a repo's ui object
91 to determine user permissions. By default, with neither option set (or
96 to determine user permissions. By default, with neither option set (or
92 both empty), allow all users to read the repo. There are two ways a
97 both empty), allow all users to read the repo. There are two ways a
93 user can be denied read access: (1) deny_read is not empty, and the
98 user can be denied read access: (1) deny_read is not empty, and the
94 user is unauthenticated or deny_read contains user (or *), and (2)
99 user is unauthenticated or deny_read contains user (or *), and (2)
95 allow_read is not empty and the user is not in allow_read. Return True
100 allow_read is not empty and the user is not in allow_read. Return True
96 if user is allowed to read the repo, else return False."""
101 if user is allowed to read the repo, else return False."""
97
102
98 user = req.env.get('REMOTE_USER')
103 user = req.env.get('REMOTE_USER')
99
104
100 deny_read = ui.configlist('web', 'deny_read', default=None, untrusted=True)
105 deny_read = ui.configlist('web', 'deny_read', default=None, untrusted=True)
101 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
106 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
102 return False
107 return False
103
108
104 allow_read = ui.configlist('web', 'allow_read', default=None, untrusted=True)
109 allow_read = ui.configlist('web', 'allow_read', default=None, untrusted=True)
105 # by default, allow reading if no allow_read option has been set
110 # by default, allow reading if no allow_read option has been set
106 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
111 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
107 return True
112 return True
108
113
109 return False
114 return False
110
115
111 def run_wsgi(self, req):
116 def run_wsgi(self, req):
112
117
113 try:
118 try:
114 try:
119 try:
115
120
116 virtual = req.env.get("PATH_INFO", "").strip('/')
121 virtual = req.env.get("PATH_INFO", "").strip('/')
117 tmpl = self.templater(req)
122 tmpl = self.templater(req)
118 ctype = tmpl('mimetype', encoding=util._encoding)
123 ctype = tmpl('mimetype', encoding=util._encoding)
119 ctype = templater.stringify(ctype)
124 ctype = templater.stringify(ctype)
120
125
121 # a static file
126 # a static file
122 if virtual.startswith('static/') or 'static' in req.form:
127 if virtual.startswith('static/') or 'static' in req.form:
123 if virtual.startswith('static/'):
128 if virtual.startswith('static/'):
124 fname = virtual[7:]
129 fname = virtual[7:]
125 else:
130 else:
126 fname = req.form['static'][0]
131 fname = req.form['static'][0]
127 static = templater.templatepath('static')
132 static = templater.templatepath('static')
128 return staticfile(static, fname, req)
133 return staticfile(static, fname, req)
129
134
130 # top-level index
135 # top-level index
131 elif not virtual:
136 elif not virtual:
132 req.respond(HTTP_OK, ctype)
137 req.respond(HTTP_OK, ctype)
133 return self.makeindex(req, tmpl)
138 return self.makeindex(req, tmpl)
134
139
135 # nested indexes and hgwebs
140 # nested indexes and hgwebs
136
141
137 repos = dict(self.repos)
142 repos = dict(self.repos)
138 while virtual:
143 while virtual:
139 real = repos.get(virtual)
144 real = repos.get(virtual)
140 if real:
145 if real:
141 req.env['REPO_NAME'] = virtual
146 req.env['REPO_NAME'] = virtual
142 try:
147 try:
143 repo = hg.repository(self.parentui, real)
148 repo = hg.repository(self.parentui, real)
144 return hgweb(repo).run_wsgi(req)
149 return hgweb(repo).run_wsgi(req)
145 except IOError, inst:
150 except IOError, inst:
146 msg = inst.strerror
151 msg = inst.strerror
147 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
152 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
148 except RepoError, inst:
153 except RepoError, inst:
149 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
154 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
150
155
151 # browse subdirectories
156 # browse subdirectories
152 subdir = virtual + '/'
157 subdir = virtual + '/'
153 if [r for r in repos if r.startswith(subdir)]:
158 if [r for r in repos if r.startswith(subdir)]:
154 req.respond(HTTP_OK, ctype)
159 req.respond(HTTP_OK, ctype)
155 return self.makeindex(req, tmpl, subdir)
160 return self.makeindex(req, tmpl, subdir)
156
161
157 up = virtual.rfind('/')
162 up = virtual.rfind('/')
158 if up < 0:
163 if up < 0:
159 break
164 break
160 virtual = virtual[:up]
165 virtual = virtual[:up]
161
166
162 # prefixes not found
167 # prefixes not found
163 req.respond(HTTP_NOT_FOUND, ctype)
168 req.respond(HTTP_NOT_FOUND, ctype)
164 return tmpl("notfound", repo=virtual)
169 return tmpl("notfound", repo=virtual)
165
170
166 except ErrorResponse, err:
171 except ErrorResponse, err:
167 req.respond(err.code, ctype)
172 req.respond(err.code, ctype)
168 return tmpl('error', error=err.message or '')
173 return tmpl('error', error=err.message or '')
169 finally:
174 finally:
170 tmpl = None
175 tmpl = None
171
176
172 def makeindex(self, req, tmpl, subdir=""):
177 def makeindex(self, req, tmpl, subdir=""):
173
178
174 def archivelist(ui, nodeid, url):
179 def archivelist(ui, nodeid, url):
175 allowed = ui.configlist("web", "allow_archive", untrusted=True)
180 allowed = ui.configlist("web", "allow_archive", untrusted=True)
176 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
181 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
177 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
182 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
178 untrusted=True):
183 untrusted=True):
179 yield {"type" : i[0], "extension": i[1],
184 yield {"type" : i[0], "extension": i[1],
180 "node": nodeid, "url": url}
185 "node": nodeid, "url": url}
181
186
182 def entries(sortcolumn="", descending=False, subdir="", **map):
187 def entries(sortcolumn="", descending=False, subdir="", **map):
183 def sessionvars(**map):
188 def sessionvars(**map):
184 fields = []
189 fields = []
185 if 'style' in req.form:
190 if 'style' in req.form:
186 style = req.form['style'][0]
191 style = req.form['style'][0]
187 if style != get('web', 'style', ''):
192 if style != get('web', 'style', ''):
188 fields.append(('style', style))
193 fields.append(('style', style))
189
194
190 separator = url[-1] == '?' and ';' or '?'
195 separator = url[-1] == '?' and ';' or '?'
191 for name, value in fields:
196 for name, value in fields:
192 yield dict(name=name, value=value, separator=separator)
197 yield dict(name=name, value=value, separator=separator)
193 separator = ';'
198 separator = ';'
194
199
195 rows = []
200 rows = []
196 parity = paritygen(self.stripecount)
201 parity = paritygen(self.stripecount)
197 for name, path in self.repos:
202 for name, path in self.repos:
198 if not name.startswith(subdir):
203 if not name.startswith(subdir):
199 continue
204 continue
200 name = name[len(subdir):]
205 name = name[len(subdir):]
201
206
202 u = ui.ui(parentui=self.parentui)
207 u = ui.ui(parentui=self.parentui)
203 try:
208 try:
204 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
209 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
205 except Exception, e:
210 except Exception, e:
206 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
211 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
207 continue
212 continue
208 def get(section, name, default=None):
213 def get(section, name, default=None):
209 return u.config(section, name, default, untrusted=True)
214 return u.config(section, name, default, untrusted=True)
210
215
211 if u.configbool("web", "hidden", untrusted=True):
216 if u.configbool("web", "hidden", untrusted=True):
212 continue
217 continue
213
218
214 if not self.read_allowed(u, req):
219 if not self.read_allowed(u, req):
215 continue
220 continue
216
221
217 parts = [name]
222 parts = [name]
218 if 'PATH_INFO' in req.env:
223 if 'PATH_INFO' in req.env:
219 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
224 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
220 if req.env['SCRIPT_NAME']:
225 if req.env['SCRIPT_NAME']:
221 parts.insert(0, req.env['SCRIPT_NAME'])
226 parts.insert(0, req.env['SCRIPT_NAME'])
222 url = ('/'.join(parts).replace("//", "/")) + '/'
227 url = ('/'.join(parts).replace("//", "/")) + '/'
223
228
224 # update time with local timezone
229 # update time with local timezone
225 try:
230 try:
226 d = (get_mtime(path), util.makedate()[1])
231 d = (get_mtime(path), util.makedate()[1])
227 except OSError:
232 except OSError:
228 continue
233 continue
229
234
230 contact = get_contact(get)
235 contact = get_contact(get)
231 description = get("web", "description", "")
236 description = get("web", "description", "")
232 name = get("web", "name", name)
237 name = get("web", "name", name)
233 row = dict(contact=contact or "unknown",
238 row = dict(contact=contact or "unknown",
234 contact_sort=contact.upper() or "unknown",
239 contact_sort=contact.upper() or "unknown",
235 name=name,
240 name=name,
236 name_sort=name,
241 name_sort=name,
237 url=url,
242 url=url,
238 description=description or "unknown",
243 description=description or "unknown",
239 description_sort=description.upper() or "unknown",
244 description_sort=description.upper() or "unknown",
240 lastchange=d,
245 lastchange=d,
241 lastchange_sort=d[1]-d[0],
246 lastchange_sort=d[1]-d[0],
242 sessionvars=sessionvars,
247 sessionvars=sessionvars,
243 archives=archivelist(u, "tip", url))
248 archives=archivelist(u, "tip", url))
244 if (not sortcolumn
249 if (not sortcolumn
245 or (sortcolumn, descending) == self.repos_sorted):
250 or (sortcolumn, descending) == self.repos_sorted):
246 # fast path for unsorted output
251 # fast path for unsorted output
247 row['parity'] = parity.next()
252 row['parity'] = parity.next()
248 yield row
253 yield row
249 else:
254 else:
250 rows.append((row["%s_sort" % sortcolumn], row))
255 rows.append((row["%s_sort" % sortcolumn], row))
251 if rows:
256 if rows:
252 rows.sort()
257 rows.sort()
253 if descending:
258 if descending:
254 rows.reverse()
259 rows.reverse()
255 for key, row in rows:
260 for key, row in rows:
256 row['parity'] = parity.next()
261 row['parity'] = parity.next()
257 yield row
262 yield row
258
263
259 sortable = ["name", "description", "contact", "lastchange"]
264 sortable = ["name", "description", "contact", "lastchange"]
260 sortcolumn, descending = self.repos_sorted
265 sortcolumn, descending = self.repos_sorted
261 if 'sort' in req.form:
266 if 'sort' in req.form:
262 sortcolumn = req.form['sort'][0]
267 sortcolumn = req.form['sort'][0]
263 descending = sortcolumn.startswith('-')
268 descending = sortcolumn.startswith('-')
264 if descending:
269 if descending:
265 sortcolumn = sortcolumn[1:]
270 sortcolumn = sortcolumn[1:]
266 if sortcolumn not in sortable:
271 if sortcolumn not in sortable:
267 sortcolumn = ""
272 sortcolumn = ""
268
273
269 sort = [("sort_%s" % column,
274 sort = [("sort_%s" % column,
270 "%s%s" % ((not descending and column == sortcolumn)
275 "%s%s" % ((not descending and column == sortcolumn)
271 and "-" or "", column))
276 and "-" or "", column))
272 for column in sortable]
277 for column in sortable]
273
278
274 if self._baseurl is not None:
279 if self._baseurl is not None:
275 req.env['SCRIPT_NAME'] = self._baseurl
280 req.env['SCRIPT_NAME'] = self._baseurl
276
281
277 return tmpl("index", entries=entries, subdir=subdir,
282 return tmpl("index", entries=entries, subdir=subdir,
278 sortcolumn=sortcolumn, descending=descending,
283 sortcolumn=sortcolumn, descending=descending,
279 **dict(sort))
284 **dict(sort))
280
285
281 def templater(self, req):
286 def templater(self, req):
282
287
283 def header(**map):
288 def header(**map):
284 yield tmpl('header', encoding=util._encoding, **map)
289 yield tmpl('header', encoding=util._encoding, **map)
285
290
286 def footer(**map):
291 def footer(**map):
287 yield tmpl("footer", **map)
292 yield tmpl("footer", **map)
288
293
289 def motd(**map):
294 def motd(**map):
290 if self.motd is not None:
295 if self.motd is not None:
291 yield self.motd
296 yield self.motd
292 else:
297 else:
293 yield config('web', 'motd', '')
298 yield config('web', 'motd', '')
294
299
295 def config(section, name, default=None, untrusted=True):
300 def config(section, name, default=None, untrusted=True):
296 return self.parentui.config(section, name, default, untrusted)
301 return self.parentui.config(section, name, default, untrusted)
297
302
298 if self._baseurl is not None:
303 if self._baseurl is not None:
299 req.env['SCRIPT_NAME'] = self._baseurl
304 req.env['SCRIPT_NAME'] = self._baseurl
300
305
301 url = req.env.get('SCRIPT_NAME', '')
306 url = req.env.get('SCRIPT_NAME', '')
302 if not url.endswith('/'):
307 if not url.endswith('/'):
303 url += '/'
308 url += '/'
304
309
305 staticurl = config('web', 'staticurl') or url + 'static/'
310 staticurl = config('web', 'staticurl') or url + 'static/'
306 if not staticurl.endswith('/'):
311 if not staticurl.endswith('/'):
307 staticurl += '/'
312 staticurl += '/'
308
313
309 style = self.style
314 style = self.style
310 if style is None:
315 if style is None:
311 style = config('web', 'style', '')
316 style = config('web', 'style', '')
312 if 'style' in req.form:
317 if 'style' in req.form:
313 style = req.form['style'][0]
318 style = req.form['style'][0]
314 if self.stripecount is None:
319 if self.stripecount is None:
315 self.stripecount = int(config('web', 'stripes', 1))
320 self.stripecount = int(config('web', 'stripes', 1))
316 mapfile = style_map(templater.templatepath(), style)
321 mapfile = style_map(templater.templatepath(), style)
317 tmpl = templater.templater(mapfile, templatefilters.filters,
322 tmpl = templater.templater(mapfile, templatefilters.filters,
318 defaults={"header": header,
323 defaults={"header": header,
319 "footer": footer,
324 "footer": footer,
320 "motd": motd,
325 "motd": motd,
321 "url": url,
326 "url": url,
322 "staticurl": staticurl})
327 "staticurl": staticurl})
323 return tmpl
328 return tmpl
@@ -1,1983 +1,1988 b''
1 """
1 """
2 util.py - Mercurial utility functions and platform specfic implementations
2 util.py - Mercurial utility functions and platform specfic implementations
3
3
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7
7
8 This software may be used and distributed according to the terms
8 This software may be used and distributed according to the terms
9 of the GNU General Public License, incorporated herein by reference.
9 of the GNU General Public License, incorporated herein by reference.
10
10
11 This contains helper routines that are independent of the SCM core and hide
11 This contains helper routines that are independent of the SCM core and hide
12 platform-specific details from the core.
12 platform-specific details from the core.
13 """
13 """
14
14
15 from i18n import _
15 from i18n import _
16 import cStringIO, errno, getpass, re, shutil, sys, tempfile, traceback
16 import cStringIO, errno, getpass, re, shutil, sys, tempfile, traceback
17 import os, stat, threading, time, calendar, ConfigParser, locale, glob, osutil
17 import os, stat, threading, time, calendar, ConfigParser, locale, glob, osutil
18 import imp
18 import imp
19
19
20 # Python compatibility
20 # Python compatibility
21
21
22 try:
22 try:
23 set = set
23 set = set
24 frozenset = frozenset
24 frozenset = frozenset
25 except NameError:
25 except NameError:
26 from sets import Set as set, ImmutableSet as frozenset
26 from sets import Set as set, ImmutableSet as frozenset
27
27
28 _md5 = None
28 _md5 = None
29 def md5(s):
29 def md5(s):
30 global _md5
30 global _md5
31 if _md5 is None:
31 if _md5 is None:
32 try:
32 try:
33 import hashlib
33 import hashlib
34 _md5 = hashlib.md5
34 _md5 = hashlib.md5
35 except ImportError:
35 except ImportError:
36 import md5
36 import md5
37 _md5 = md5.md5
37 _md5 = md5.md5
38 return _md5(s)
38 return _md5(s)
39
39
40 _sha1 = None
40 _sha1 = None
41 def sha1(s):
41 def sha1(s):
42 global _sha1
42 global _sha1
43 if _sha1 is None:
43 if _sha1 is None:
44 try:
44 try:
45 import hashlib
45 import hashlib
46 _sha1 = hashlib.sha1
46 _sha1 = hashlib.sha1
47 except ImportError:
47 except ImportError:
48 import sha
48 import sha
49 _sha1 = sha.sha
49 _sha1 = sha.sha
50 return _sha1(s)
50 return _sha1(s)
51
51
52 try:
52 try:
53 import subprocess
53 import subprocess
54 subprocess.Popen # trigger ImportError early
54 subprocess.Popen # trigger ImportError early
55 closefds = os.name == 'posix'
55 closefds = os.name == 'posix'
56 def popen2(cmd, mode='t', bufsize=-1):
56 def popen2(cmd, mode='t', bufsize=-1):
57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
58 close_fds=closefds,
58 close_fds=closefds,
59 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
59 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
60 return p.stdin, p.stdout
60 return p.stdin, p.stdout
61 def popen3(cmd, mode='t', bufsize=-1):
61 def popen3(cmd, mode='t', bufsize=-1):
62 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
62 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
63 close_fds=closefds,
63 close_fds=closefds,
64 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
64 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
65 stderr=subprocess.PIPE)
65 stderr=subprocess.PIPE)
66 return p.stdin, p.stdout, p.stderr
66 return p.stdin, p.stdout, p.stderr
67 def Popen3(cmd, capturestderr=False, bufsize=-1):
67 def Popen3(cmd, capturestderr=False, bufsize=-1):
68 stderr = capturestderr and subprocess.PIPE or None
68 stderr = capturestderr and subprocess.PIPE or None
69 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
69 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
70 close_fds=closefds,
70 close_fds=closefds,
71 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
71 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
72 stderr=stderr)
72 stderr=stderr)
73 p.fromchild = p.stdout
73 p.fromchild = p.stdout
74 p.tochild = p.stdin
74 p.tochild = p.stdin
75 p.childerr = p.stderr
75 p.childerr = p.stderr
76 return p
76 return p
77 except ImportError:
77 except ImportError:
78 subprocess = None
78 subprocess = None
79 from popen2 import Popen3
79 from popen2 import Popen3
80 popen2 = os.popen2
80 popen2 = os.popen2
81 popen3 = os.popen3
81 popen3 = os.popen3
82
82
83
83
84 _encodingfixup = {'646': 'ascii', 'ANSI_X3.4-1968': 'ascii'}
84 _encodingfixup = {'646': 'ascii', 'ANSI_X3.4-1968': 'ascii'}
85
85
86 try:
86 try:
87 _encoding = os.environ.get("HGENCODING")
87 _encoding = os.environ.get("HGENCODING")
88 if sys.platform == 'darwin' and not _encoding:
88 if sys.platform == 'darwin' and not _encoding:
89 # On darwin, getpreferredencoding ignores the locale environment and
89 # On darwin, getpreferredencoding ignores the locale environment and
90 # always returns mac-roman. We override this if the environment is
90 # always returns mac-roman. We override this if the environment is
91 # not C (has been customized by the user).
91 # not C (has been customized by the user).
92 locale.setlocale(locale.LC_CTYPE, '')
92 locale.setlocale(locale.LC_CTYPE, '')
93 _encoding = locale.getlocale()[1]
93 _encoding = locale.getlocale()[1]
94 if not _encoding:
94 if not _encoding:
95 _encoding = locale.getpreferredencoding() or 'ascii'
95 _encoding = locale.getpreferredencoding() or 'ascii'
96 _encoding = _encodingfixup.get(_encoding, _encoding)
96 _encoding = _encodingfixup.get(_encoding, _encoding)
97 except locale.Error:
97 except locale.Error:
98 _encoding = 'ascii'
98 _encoding = 'ascii'
99 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
99 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
100 _fallbackencoding = 'ISO-8859-1'
100 _fallbackencoding = 'ISO-8859-1'
101
101
102 def tolocal(s):
102 def tolocal(s):
103 """
103 """
104 Convert a string from internal UTF-8 to local encoding
104 Convert a string from internal UTF-8 to local encoding
105
105
106 All internal strings should be UTF-8 but some repos before the
106 All internal strings should be UTF-8 but some repos before the
107 implementation of locale support may contain latin1 or possibly
107 implementation of locale support may contain latin1 or possibly
108 other character sets. We attempt to decode everything strictly
108 other character sets. We attempt to decode everything strictly
109 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
109 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
110 replace unknown characters.
110 replace unknown characters.
111 """
111 """
112 for e in ('UTF-8', _fallbackencoding):
112 for e in ('UTF-8', _fallbackencoding):
113 try:
113 try:
114 u = s.decode(e) # attempt strict decoding
114 u = s.decode(e) # attempt strict decoding
115 return u.encode(_encoding, "replace")
115 return u.encode(_encoding, "replace")
116 except LookupError, k:
116 except LookupError, k:
117 raise Abort(_("%s, please check your locale settings") % k)
117 raise Abort(_("%s, please check your locale settings") % k)
118 except UnicodeDecodeError:
118 except UnicodeDecodeError:
119 pass
119 pass
120 u = s.decode("utf-8", "replace") # last ditch
120 u = s.decode("utf-8", "replace") # last ditch
121 return u.encode(_encoding, "replace")
121 return u.encode(_encoding, "replace")
122
122
123 def fromlocal(s):
123 def fromlocal(s):
124 """
124 """
125 Convert a string from the local character encoding to UTF-8
125 Convert a string from the local character encoding to UTF-8
126
126
127 We attempt to decode strings using the encoding mode set by
127 We attempt to decode strings using the encoding mode set by
128 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
128 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
129 characters will cause an error message. Other modes include
129 characters will cause an error message. Other modes include
130 'replace', which replaces unknown characters with a special
130 'replace', which replaces unknown characters with a special
131 Unicode character, and 'ignore', which drops the character.
131 Unicode character, and 'ignore', which drops the character.
132 """
132 """
133 try:
133 try:
134 return s.decode(_encoding, _encodingmode).encode("utf-8")
134 return s.decode(_encoding, _encodingmode).encode("utf-8")
135 except UnicodeDecodeError, inst:
135 except UnicodeDecodeError, inst:
136 sub = s[max(0, inst.start-10):inst.start+10]
136 sub = s[max(0, inst.start-10):inst.start+10]
137 raise Abort("decoding near '%s': %s!" % (sub, inst))
137 raise Abort("decoding near '%s': %s!" % (sub, inst))
138 except LookupError, k:
138 except LookupError, k:
139 raise Abort(_("%s, please check your locale settings") % k)
139 raise Abort(_("%s, please check your locale settings") % k)
140
140
141 def locallen(s):
141 def locallen(s):
142 """Find the length in characters of a local string"""
142 """Find the length in characters of a local string"""
143 return len(s.decode(_encoding, "replace"))
143 return len(s.decode(_encoding, "replace"))
144
144
145 # used by parsedate
145 # used by parsedate
146 defaultdateformats = (
146 defaultdateformats = (
147 '%Y-%m-%d %H:%M:%S',
147 '%Y-%m-%d %H:%M:%S',
148 '%Y-%m-%d %I:%M:%S%p',
148 '%Y-%m-%d %I:%M:%S%p',
149 '%Y-%m-%d %H:%M',
149 '%Y-%m-%d %H:%M',
150 '%Y-%m-%d %I:%M%p',
150 '%Y-%m-%d %I:%M%p',
151 '%Y-%m-%d',
151 '%Y-%m-%d',
152 '%m-%d',
152 '%m-%d',
153 '%m/%d',
153 '%m/%d',
154 '%m/%d/%y',
154 '%m/%d/%y',
155 '%m/%d/%Y',
155 '%m/%d/%Y',
156 '%a %b %d %H:%M:%S %Y',
156 '%a %b %d %H:%M:%S %Y',
157 '%a %b %d %I:%M:%S%p %Y',
157 '%a %b %d %I:%M:%S%p %Y',
158 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
158 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
159 '%b %d %H:%M:%S %Y',
159 '%b %d %H:%M:%S %Y',
160 '%b %d %I:%M:%S%p %Y',
160 '%b %d %I:%M:%S%p %Y',
161 '%b %d %H:%M:%S',
161 '%b %d %H:%M:%S',
162 '%b %d %I:%M:%S%p',
162 '%b %d %I:%M:%S%p',
163 '%b %d %H:%M',
163 '%b %d %H:%M',
164 '%b %d %I:%M%p',
164 '%b %d %I:%M%p',
165 '%b %d %Y',
165 '%b %d %Y',
166 '%b %d',
166 '%b %d',
167 '%H:%M:%S',
167 '%H:%M:%S',
168 '%I:%M:%SP',
168 '%I:%M:%SP',
169 '%H:%M',
169 '%H:%M',
170 '%I:%M%p',
170 '%I:%M%p',
171 )
171 )
172
172
173 extendeddateformats = defaultdateformats + (
173 extendeddateformats = defaultdateformats + (
174 "%Y",
174 "%Y",
175 "%Y-%m",
175 "%Y-%m",
176 "%b",
176 "%b",
177 "%b %Y",
177 "%b %Y",
178 )
178 )
179
179
180 class SignalInterrupt(Exception):
180 class SignalInterrupt(Exception):
181 """Exception raised on SIGTERM and SIGHUP."""
181 """Exception raised on SIGTERM and SIGHUP."""
182
182
183 # differences from SafeConfigParser:
183 # differences from SafeConfigParser:
184 # - case-sensitive keys
184 # - case-sensitive keys
185 # - allows values that are not strings (this means that you may not
185 # - allows values that are not strings (this means that you may not
186 # be able to save the configuration to a file)
186 # be able to save the configuration to a file)
187 class configparser(ConfigParser.SafeConfigParser):
187 class configparser(ConfigParser.SafeConfigParser):
188 def optionxform(self, optionstr):
188 def optionxform(self, optionstr):
189 return optionstr
189 return optionstr
190
190
191 def set(self, section, option, value):
191 def set(self, section, option, value):
192 return ConfigParser.ConfigParser.set(self, section, option, value)
192 return ConfigParser.ConfigParser.set(self, section, option, value)
193
193
194 def _interpolate(self, section, option, rawval, vars):
194 def _interpolate(self, section, option, rawval, vars):
195 if not isinstance(rawval, basestring):
195 if not isinstance(rawval, basestring):
196 return rawval
196 return rawval
197 return ConfigParser.SafeConfigParser._interpolate(self, section,
197 return ConfigParser.SafeConfigParser._interpolate(self, section,
198 option, rawval, vars)
198 option, rawval, vars)
199
199
200 def cachefunc(func):
200 def cachefunc(func):
201 '''cache the result of function calls'''
201 '''cache the result of function calls'''
202 # XXX doesn't handle keywords args
202 # XXX doesn't handle keywords args
203 cache = {}
203 cache = {}
204 if func.func_code.co_argcount == 1:
204 if func.func_code.co_argcount == 1:
205 # we gain a small amount of time because
205 # we gain a small amount of time because
206 # we don't need to pack/unpack the list
206 # we don't need to pack/unpack the list
207 def f(arg):
207 def f(arg):
208 if arg not in cache:
208 if arg not in cache:
209 cache[arg] = func(arg)
209 cache[arg] = func(arg)
210 return cache[arg]
210 return cache[arg]
211 else:
211 else:
212 def f(*args):
212 def f(*args):
213 if args not in cache:
213 if args not in cache:
214 cache[args] = func(*args)
214 cache[args] = func(*args)
215 return cache[args]
215 return cache[args]
216
216
217 return f
217 return f
218
218
219 def pipefilter(s, cmd):
219 def pipefilter(s, cmd):
220 '''filter string S through command CMD, returning its output'''
220 '''filter string S through command CMD, returning its output'''
221 (pin, pout) = popen2(cmd, 'b')
221 (pin, pout) = popen2(cmd, 'b')
222 def writer():
222 def writer():
223 try:
223 try:
224 pin.write(s)
224 pin.write(s)
225 pin.close()
225 pin.close()
226 except IOError, inst:
226 except IOError, inst:
227 if inst.errno != errno.EPIPE:
227 if inst.errno != errno.EPIPE:
228 raise
228 raise
229
229
230 # we should use select instead on UNIX, but this will work on most
230 # we should use select instead on UNIX, but this will work on most
231 # systems, including Windows
231 # systems, including Windows
232 w = threading.Thread(target=writer)
232 w = threading.Thread(target=writer)
233 w.start()
233 w.start()
234 f = pout.read()
234 f = pout.read()
235 pout.close()
235 pout.close()
236 w.join()
236 w.join()
237 return f
237 return f
238
238
239 def tempfilter(s, cmd):
239 def tempfilter(s, cmd):
240 '''filter string S through a pair of temporary files with CMD.
240 '''filter string S through a pair of temporary files with CMD.
241 CMD is used as a template to create the real command to be run,
241 CMD is used as a template to create the real command to be run,
242 with the strings INFILE and OUTFILE replaced by the real names of
242 with the strings INFILE and OUTFILE replaced by the real names of
243 the temporary files generated.'''
243 the temporary files generated.'''
244 inname, outname = None, None
244 inname, outname = None, None
245 try:
245 try:
246 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
246 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
247 fp = os.fdopen(infd, 'wb')
247 fp = os.fdopen(infd, 'wb')
248 fp.write(s)
248 fp.write(s)
249 fp.close()
249 fp.close()
250 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
250 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
251 os.close(outfd)
251 os.close(outfd)
252 cmd = cmd.replace('INFILE', inname)
252 cmd = cmd.replace('INFILE', inname)
253 cmd = cmd.replace('OUTFILE', outname)
253 cmd = cmd.replace('OUTFILE', outname)
254 code = os.system(cmd)
254 code = os.system(cmd)
255 if sys.platform == 'OpenVMS' and code & 1:
255 if sys.platform == 'OpenVMS' and code & 1:
256 code = 0
256 code = 0
257 if code: raise Abort(_("command '%s' failed: %s") %
257 if code: raise Abort(_("command '%s' failed: %s") %
258 (cmd, explain_exit(code)))
258 (cmd, explain_exit(code)))
259 return open(outname, 'rb').read()
259 return open(outname, 'rb').read()
260 finally:
260 finally:
261 try:
261 try:
262 if inname: os.unlink(inname)
262 if inname: os.unlink(inname)
263 except: pass
263 except: pass
264 try:
264 try:
265 if outname: os.unlink(outname)
265 if outname: os.unlink(outname)
266 except: pass
266 except: pass
267
267
268 filtertable = {
268 filtertable = {
269 'tempfile:': tempfilter,
269 'tempfile:': tempfilter,
270 'pipe:': pipefilter,
270 'pipe:': pipefilter,
271 }
271 }
272
272
273 def filter(s, cmd):
273 def filter(s, cmd):
274 "filter a string through a command that transforms its input to its output"
274 "filter a string through a command that transforms its input to its output"
275 for name, fn in filtertable.iteritems():
275 for name, fn in filtertable.iteritems():
276 if cmd.startswith(name):
276 if cmd.startswith(name):
277 return fn(s, cmd[len(name):].lstrip())
277 return fn(s, cmd[len(name):].lstrip())
278 return pipefilter(s, cmd)
278 return pipefilter(s, cmd)
279
279
280 def binary(s):
280 def binary(s):
281 """return true if a string is binary data"""
281 """return true if a string is binary data"""
282 if s and '\0' in s:
282 if s and '\0' in s:
283 return True
283 return True
284 return False
284 return False
285
285
286 def unique(g):
286 def unique(g):
287 """return the uniq elements of iterable g"""
287 """return the uniq elements of iterable g"""
288 return dict.fromkeys(g).keys()
288 return dict.fromkeys(g).keys()
289
289
290 def sort(l):
290 def sort(l):
291 if not isinstance(l, list):
291 if not isinstance(l, list):
292 l = list(l)
292 l = list(l)
293 l.sort()
293 l.sort()
294 return l
294 return l
295
295
296 def increasingchunks(source, min=1024, max=65536):
296 def increasingchunks(source, min=1024, max=65536):
297 '''return no less than min bytes per chunk while data remains,
297 '''return no less than min bytes per chunk while data remains,
298 doubling min after each chunk until it reaches max'''
298 doubling min after each chunk until it reaches max'''
299 def log2(x):
299 def log2(x):
300 if not x:
300 if not x:
301 return 0
301 return 0
302 i = 0
302 i = 0
303 while x:
303 while x:
304 x >>= 1
304 x >>= 1
305 i += 1
305 i += 1
306 return i - 1
306 return i - 1
307
307
308 buf = []
308 buf = []
309 blen = 0
309 blen = 0
310 for chunk in source:
310 for chunk in source:
311 buf.append(chunk)
311 buf.append(chunk)
312 blen += len(chunk)
312 blen += len(chunk)
313 if blen >= min:
313 if blen >= min:
314 if min < max:
314 if min < max:
315 min = min << 1
315 min = min << 1
316 nmin = 1 << log2(blen)
316 nmin = 1 << log2(blen)
317 if nmin > min:
317 if nmin > min:
318 min = nmin
318 min = nmin
319 if min > max:
319 if min > max:
320 min = max
320 min = max
321 yield ''.join(buf)
321 yield ''.join(buf)
322 blen = 0
322 blen = 0
323 buf = []
323 buf = []
324 if buf:
324 if buf:
325 yield ''.join(buf)
325 yield ''.join(buf)
326
326
327 class Abort(Exception):
327 class Abort(Exception):
328 """Raised if a command needs to print an error and exit."""
328 """Raised if a command needs to print an error and exit."""
329
329
330 class UnexpectedOutput(Abort):
330 class UnexpectedOutput(Abort):
331 """Raised to print an error with part of output and exit."""
331 """Raised to print an error with part of output and exit."""
332
332
333 def always(fn): return True
333 def always(fn): return True
334 def never(fn): return False
334 def never(fn): return False
335
335
336 def expand_glob(pats):
336 def expand_glob(pats):
337 '''On Windows, expand the implicit globs in a list of patterns'''
337 '''On Windows, expand the implicit globs in a list of patterns'''
338 if os.name != 'nt':
338 if os.name != 'nt':
339 return list(pats)
339 return list(pats)
340 ret = []
340 ret = []
341 for p in pats:
341 for p in pats:
342 kind, name = patkind(p, None)
342 kind, name = patkind(p, None)
343 if kind is None:
343 if kind is None:
344 globbed = glob.glob(name)
344 globbed = glob.glob(name)
345 if globbed:
345 if globbed:
346 ret.extend(globbed)
346 ret.extend(globbed)
347 continue
347 continue
348 # if we couldn't expand the glob, just keep it around
348 # if we couldn't expand the glob, just keep it around
349 ret.append(p)
349 ret.append(p)
350 return ret
350 return ret
351
351
352 def patkind(name, default):
352 def patkind(name, default):
353 """Split a string into an optional pattern kind prefix and the
353 """Split a string into an optional pattern kind prefix and the
354 actual pattern."""
354 actual pattern."""
355 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
355 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
356 if name.startswith(prefix + ':'): return name.split(':', 1)
356 if name.startswith(prefix + ':'): return name.split(':', 1)
357 return default, name
357 return default, name
358
358
359 def globre(pat, head='^', tail='$'):
359 def globre(pat, head='^', tail='$'):
360 "convert a glob pattern into a regexp"
360 "convert a glob pattern into a regexp"
361 i, n = 0, len(pat)
361 i, n = 0, len(pat)
362 res = ''
362 res = ''
363 group = 0
363 group = 0
364 def peek(): return i < n and pat[i]
364 def peek(): return i < n and pat[i]
365 while i < n:
365 while i < n:
366 c = pat[i]
366 c = pat[i]
367 i = i+1
367 i = i+1
368 if c == '*':
368 if c == '*':
369 if peek() == '*':
369 if peek() == '*':
370 i += 1
370 i += 1
371 res += '.*'
371 res += '.*'
372 else:
372 else:
373 res += '[^/]*'
373 res += '[^/]*'
374 elif c == '?':
374 elif c == '?':
375 res += '.'
375 res += '.'
376 elif c == '[':
376 elif c == '[':
377 j = i
377 j = i
378 if j < n and pat[j] in '!]':
378 if j < n and pat[j] in '!]':
379 j += 1
379 j += 1
380 while j < n and pat[j] != ']':
380 while j < n and pat[j] != ']':
381 j += 1
381 j += 1
382 if j >= n:
382 if j >= n:
383 res += '\\['
383 res += '\\['
384 else:
384 else:
385 stuff = pat[i:j].replace('\\','\\\\')
385 stuff = pat[i:j].replace('\\','\\\\')
386 i = j + 1
386 i = j + 1
387 if stuff[0] == '!':
387 if stuff[0] == '!':
388 stuff = '^' + stuff[1:]
388 stuff = '^' + stuff[1:]
389 elif stuff[0] == '^':
389 elif stuff[0] == '^':
390 stuff = '\\' + stuff
390 stuff = '\\' + stuff
391 res = '%s[%s]' % (res, stuff)
391 res = '%s[%s]' % (res, stuff)
392 elif c == '{':
392 elif c == '{':
393 group += 1
393 group += 1
394 res += '(?:'
394 res += '(?:'
395 elif c == '}' and group:
395 elif c == '}' and group:
396 res += ')'
396 res += ')'
397 group -= 1
397 group -= 1
398 elif c == ',' and group:
398 elif c == ',' and group:
399 res += '|'
399 res += '|'
400 elif c == '\\':
400 elif c == '\\':
401 p = peek()
401 p = peek()
402 if p:
402 if p:
403 i += 1
403 i += 1
404 res += re.escape(p)
404 res += re.escape(p)
405 else:
405 else:
406 res += re.escape(c)
406 res += re.escape(c)
407 else:
407 else:
408 res += re.escape(c)
408 res += re.escape(c)
409 return head + res + tail
409 return head + res + tail
410
410
411 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
411 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
412
412
413 def pathto(root, n1, n2):
413 def pathto(root, n1, n2):
414 '''return the relative path from one place to another.
414 '''return the relative path from one place to another.
415 root should use os.sep to separate directories
415 root should use os.sep to separate directories
416 n1 should use os.sep to separate directories
416 n1 should use os.sep to separate directories
417 n2 should use "/" to separate directories
417 n2 should use "/" to separate directories
418 returns an os.sep-separated path.
418 returns an os.sep-separated path.
419
419
420 If n1 is a relative path, it's assumed it's
420 If n1 is a relative path, it's assumed it's
421 relative to root.
421 relative to root.
422 n2 should always be relative to root.
422 n2 should always be relative to root.
423 '''
423 '''
424 if not n1: return localpath(n2)
424 if not n1: return localpath(n2)
425 if os.path.isabs(n1):
425 if os.path.isabs(n1):
426 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
426 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
427 return os.path.join(root, localpath(n2))
427 return os.path.join(root, localpath(n2))
428 n2 = '/'.join((pconvert(root), n2))
428 n2 = '/'.join((pconvert(root), n2))
429 a, b = splitpath(n1), n2.split('/')
429 a, b = splitpath(n1), n2.split('/')
430 a.reverse()
430 a.reverse()
431 b.reverse()
431 b.reverse()
432 while a and b and a[-1] == b[-1]:
432 while a and b and a[-1] == b[-1]:
433 a.pop()
433 a.pop()
434 b.pop()
434 b.pop()
435 b.reverse()
435 b.reverse()
436 return os.sep.join((['..'] * len(a)) + b) or '.'
436 return os.sep.join((['..'] * len(a)) + b) or '.'
437
437
438 def canonpath(root, cwd, myname):
438 def canonpath(root, cwd, myname):
439 """return the canonical path of myname, given cwd and root"""
439 """return the canonical path of myname, given cwd and root"""
440 if root == os.sep:
440 if root == os.sep:
441 rootsep = os.sep
441 rootsep = os.sep
442 elif endswithsep(root):
442 elif endswithsep(root):
443 rootsep = root
443 rootsep = root
444 else:
444 else:
445 rootsep = root + os.sep
445 rootsep = root + os.sep
446 name = myname
446 name = myname
447 if not os.path.isabs(name):
447 if not os.path.isabs(name):
448 name = os.path.join(root, cwd, name)
448 name = os.path.join(root, cwd, name)
449 name = os.path.normpath(name)
449 name = os.path.normpath(name)
450 audit_path = path_auditor(root)
450 audit_path = path_auditor(root)
451 if name != rootsep and name.startswith(rootsep):
451 if name != rootsep and name.startswith(rootsep):
452 name = name[len(rootsep):]
452 name = name[len(rootsep):]
453 audit_path(name)
453 audit_path(name)
454 return pconvert(name)
454 return pconvert(name)
455 elif name == root:
455 elif name == root:
456 return ''
456 return ''
457 else:
457 else:
458 # Determine whether `name' is in the hierarchy at or beneath `root',
458 # Determine whether `name' is in the hierarchy at or beneath `root',
459 # by iterating name=dirname(name) until that causes no change (can't
459 # by iterating name=dirname(name) until that causes no change (can't
460 # check name == '/', because that doesn't work on windows). For each
460 # check name == '/', because that doesn't work on windows). For each
461 # `name', compare dev/inode numbers. If they match, the list `rel'
461 # `name', compare dev/inode numbers. If they match, the list `rel'
462 # holds the reversed list of components making up the relative file
462 # holds the reversed list of components making up the relative file
463 # name we want.
463 # name we want.
464 root_st = os.stat(root)
464 root_st = os.stat(root)
465 rel = []
465 rel = []
466 while True:
466 while True:
467 try:
467 try:
468 name_st = os.stat(name)
468 name_st = os.stat(name)
469 except OSError:
469 except OSError:
470 break
470 break
471 if samestat(name_st, root_st):
471 if samestat(name_st, root_st):
472 if not rel:
472 if not rel:
473 # name was actually the same as root (maybe a symlink)
473 # name was actually the same as root (maybe a symlink)
474 return ''
474 return ''
475 rel.reverse()
475 rel.reverse()
476 name = os.path.join(*rel)
476 name = os.path.join(*rel)
477 audit_path(name)
477 audit_path(name)
478 return pconvert(name)
478 return pconvert(name)
479 dirname, basename = os.path.split(name)
479 dirname, basename = os.path.split(name)
480 rel.append(basename)
480 rel.append(basename)
481 if dirname == name:
481 if dirname == name:
482 break
482 break
483 name = dirname
483 name = dirname
484
484
485 raise Abort('%s not under root' % myname)
485 raise Abort('%s not under root' % myname)
486
486
487 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None, dflt_pat='glob'):
487 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None, dflt_pat='glob'):
488 """build a function to match a set of file patterns
488 """build a function to match a set of file patterns
489
489
490 arguments:
490 arguments:
491 canonroot - the canonical root of the tree you're matching against
491 canonroot - the canonical root of the tree you're matching against
492 cwd - the current working directory, if relevant
492 cwd - the current working directory, if relevant
493 names - patterns to find
493 names - patterns to find
494 inc - patterns to include
494 inc - patterns to include
495 exc - patterns to exclude
495 exc - patterns to exclude
496 dflt_pat - if a pattern in names has no explicit type, assume this one
496 dflt_pat - if a pattern in names has no explicit type, assume this one
497 src - where these patterns came from (e.g. .hgignore)
497 src - where these patterns came from (e.g. .hgignore)
498
498
499 a pattern is one of:
499 a pattern is one of:
500 'glob:<glob>' - a glob relative to cwd
500 'glob:<glob>' - a glob relative to cwd
501 're:<regexp>' - a regular expression
501 're:<regexp>' - a regular expression
502 'path:<path>' - a path relative to canonroot
502 'path:<path>' - a path relative to canonroot
503 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
503 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
504 'relpath:<path>' - a path relative to cwd
504 'relpath:<path>' - a path relative to cwd
505 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
505 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
506 '<something>' - one of the cases above, selected by the dflt_pat argument
506 '<something>' - one of the cases above, selected by the dflt_pat argument
507
507
508 returns:
508 returns:
509 a 3-tuple containing
509 a 3-tuple containing
510 - list of roots (places where one should start a recursive walk of the fs);
510 - list of roots (places where one should start a recursive walk of the fs);
511 this often matches the explicit non-pattern names passed in, but also
511 this often matches the explicit non-pattern names passed in, but also
512 includes the initial part of glob: patterns that has no glob characters
512 includes the initial part of glob: patterns that has no glob characters
513 - a bool match(filename) function
513 - a bool match(filename) function
514 - a bool indicating if any patterns were passed in
514 - a bool indicating if any patterns were passed in
515 """
515 """
516
516
517 # a common case: no patterns at all
517 # a common case: no patterns at all
518 if not names and not inc and not exc:
518 if not names and not inc and not exc:
519 return [], always, False
519 return [], always, False
520
520
521 def contains_glob(name):
521 def contains_glob(name):
522 for c in name:
522 for c in name:
523 if c in _globchars: return True
523 if c in _globchars: return True
524 return False
524 return False
525
525
526 def regex(kind, name, tail):
526 def regex(kind, name, tail):
527 '''convert a pattern into a regular expression'''
527 '''convert a pattern into a regular expression'''
528 if not name:
528 if not name:
529 return ''
529 return ''
530 if kind == 're':
530 if kind == 're':
531 return name
531 return name
532 elif kind == 'path':
532 elif kind == 'path':
533 return '^' + re.escape(name) + '(?:/|$)'
533 return '^' + re.escape(name) + '(?:/|$)'
534 elif kind == 'relglob':
534 elif kind == 'relglob':
535 return globre(name, '(?:|.*/)', tail)
535 return globre(name, '(?:|.*/)', tail)
536 elif kind == 'relpath':
536 elif kind == 'relpath':
537 return re.escape(name) + '(?:/|$)'
537 return re.escape(name) + '(?:/|$)'
538 elif kind == 'relre':
538 elif kind == 'relre':
539 if name.startswith('^'):
539 if name.startswith('^'):
540 return name
540 return name
541 return '.*' + name
541 return '.*' + name
542 return globre(name, '', tail)
542 return globre(name, '', tail)
543
543
544 def matchfn(pats, tail):
544 def matchfn(pats, tail):
545 """build a matching function from a set of patterns"""
545 """build a matching function from a set of patterns"""
546 if not pats:
546 if not pats:
547 return
547 return
548 try:
548 try:
549 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
549 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
550 if len(pat) > 20000:
550 if len(pat) > 20000:
551 raise OverflowError()
551 raise OverflowError()
552 return re.compile(pat).match
552 return re.compile(pat).match
553 except OverflowError:
553 except OverflowError:
554 # We're using a Python with a tiny regex engine and we
554 # We're using a Python with a tiny regex engine and we
555 # made it explode, so we'll divide the pattern list in two
555 # made it explode, so we'll divide the pattern list in two
556 # until it works
556 # until it works
557 l = len(pats)
557 l = len(pats)
558 if l < 2:
558 if l < 2:
559 raise
559 raise
560 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
560 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
561 return lambda s: a(s) or b(s)
561 return lambda s: a(s) or b(s)
562 except re.error:
562 except re.error:
563 for k, p in pats:
563 for k, p in pats:
564 try:
564 try:
565 re.compile('(?:%s)' % regex(k, p, tail))
565 re.compile('(?:%s)' % regex(k, p, tail))
566 except re.error:
566 except re.error:
567 if src:
567 if src:
568 raise Abort("%s: invalid pattern (%s): %s" %
568 raise Abort("%s: invalid pattern (%s): %s" %
569 (src, k, p))
569 (src, k, p))
570 else:
570 else:
571 raise Abort("invalid pattern (%s): %s" % (k, p))
571 raise Abort("invalid pattern (%s): %s" % (k, p))
572 raise Abort("invalid pattern")
572 raise Abort("invalid pattern")
573
573
574 def globprefix(pat):
574 def globprefix(pat):
575 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
575 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
576 root = []
576 root = []
577 for p in pat.split('/'):
577 for p in pat.split('/'):
578 if contains_glob(p): break
578 if contains_glob(p): break
579 root.append(p)
579 root.append(p)
580 return '/'.join(root) or '.'
580 return '/'.join(root) or '.'
581
581
582 def normalizepats(names, default):
582 def normalizepats(names, default):
583 pats = []
583 pats = []
584 roots = []
584 roots = []
585 anypats = False
585 anypats = False
586 for kind, name in [patkind(p, default) for p in names]:
586 for kind, name in [patkind(p, default) for p in names]:
587 if kind in ('glob', 'relpath'):
587 if kind in ('glob', 'relpath'):
588 name = canonpath(canonroot, cwd, name)
588 name = canonpath(canonroot, cwd, name)
589 elif kind in ('relglob', 'path'):
589 elif kind in ('relglob', 'path'):
590 name = normpath(name)
590 name = normpath(name)
591
591
592 pats.append((kind, name))
592 pats.append((kind, name))
593
593
594 if kind in ('glob', 're', 'relglob', 'relre'):
594 if kind in ('glob', 're', 'relglob', 'relre'):
595 anypats = True
595 anypats = True
596
596
597 if kind == 'glob':
597 if kind == 'glob':
598 root = globprefix(name)
598 root = globprefix(name)
599 roots.append(root)
599 roots.append(root)
600 elif kind in ('relpath', 'path'):
600 elif kind in ('relpath', 'path'):
601 roots.append(name or '.')
601 roots.append(name or '.')
602 elif kind == 'relglob':
602 elif kind == 'relglob':
603 roots.append('.')
603 roots.append('.')
604 return roots, pats, anypats
604 return roots, pats, anypats
605
605
606 roots, pats, anypats = normalizepats(names, dflt_pat)
606 roots, pats, anypats = normalizepats(names, dflt_pat)
607
607
608 patmatch = matchfn(pats, '$') or always
608 patmatch = matchfn(pats, '$') or always
609 incmatch = always
609 incmatch = always
610 if inc:
610 if inc:
611 dummy, inckinds, dummy = normalizepats(inc, 'glob')
611 dummy, inckinds, dummy = normalizepats(inc, 'glob')
612 incmatch = matchfn(inckinds, '(?:/|$)')
612 incmatch = matchfn(inckinds, '(?:/|$)')
613 excmatch = never
613 excmatch = never
614 if exc:
614 if exc:
615 dummy, exckinds, dummy = normalizepats(exc, 'glob')
615 dummy, exckinds, dummy = normalizepats(exc, 'glob')
616 excmatch = matchfn(exckinds, '(?:/|$)')
616 excmatch = matchfn(exckinds, '(?:/|$)')
617
617
618 if not names and inc and not exc:
618 if not names and inc and not exc:
619 # common case: hgignore patterns
619 # common case: hgignore patterns
620 match = incmatch
620 match = incmatch
621 else:
621 else:
622 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
622 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
623
623
624 return (roots, match, (inc or exc or anypats) and True)
624 return (roots, match, (inc or exc or anypats) and True)
625
625
626 _hgexecutable = None
626 _hgexecutable = None
627
627
628 def main_is_frozen():
628 def main_is_frozen():
629 """return True if we are a frozen executable.
629 """return True if we are a frozen executable.
630
630
631 The code supports py2exe (most common, Windows only) and tools/freeze
631 The code supports py2exe (most common, Windows only) and tools/freeze
632 (portable, not much used).
632 (portable, not much used).
633 """
633 """
634 return (hasattr(sys, "frozen") or # new py2exe
634 return (hasattr(sys, "frozen") or # new py2exe
635 hasattr(sys, "importers") or # old py2exe
635 hasattr(sys, "importers") or # old py2exe
636 imp.is_frozen("__main__")) # tools/freeze
636 imp.is_frozen("__main__")) # tools/freeze
637
637
638 def hgexecutable():
638 def hgexecutable():
639 """return location of the 'hg' executable.
639 """return location of the 'hg' executable.
640
640
641 Defaults to $HG or 'hg' in the search path.
641 Defaults to $HG or 'hg' in the search path.
642 """
642 """
643 if _hgexecutable is None:
643 if _hgexecutable is None:
644 hg = os.environ.get('HG')
644 hg = os.environ.get('HG')
645 if hg:
645 if hg:
646 set_hgexecutable(hg)
646 set_hgexecutable(hg)
647 elif main_is_frozen():
647 elif main_is_frozen():
648 set_hgexecutable(sys.executable)
648 set_hgexecutable(sys.executable)
649 else:
649 else:
650 set_hgexecutable(find_exe('hg', 'hg'))
650 set_hgexecutable(find_exe('hg', 'hg'))
651 return _hgexecutable
651 return _hgexecutable
652
652
653 def set_hgexecutable(path):
653 def set_hgexecutable(path):
654 """set location of the 'hg' executable"""
654 """set location of the 'hg' executable"""
655 global _hgexecutable
655 global _hgexecutable
656 _hgexecutable = path
656 _hgexecutable = path
657
657
658 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
658 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
659 '''enhanced shell command execution.
659 '''enhanced shell command execution.
660 run with environment maybe modified, maybe in different dir.
660 run with environment maybe modified, maybe in different dir.
661
661
662 if command fails and onerr is None, return status. if ui object,
662 if command fails and onerr is None, return status. if ui object,
663 print error message and return status, else raise onerr object as
663 print error message and return status, else raise onerr object as
664 exception.'''
664 exception.'''
665 def py2shell(val):
665 def py2shell(val):
666 'convert python object into string that is useful to shell'
666 'convert python object into string that is useful to shell'
667 if val in (None, False):
667 if val in (None, False):
668 return '0'
668 return '0'
669 if val == True:
669 if val == True:
670 return '1'
670 return '1'
671 return str(val)
671 return str(val)
672 oldenv = {}
672 oldenv = {}
673 for k in environ:
673 for k in environ:
674 oldenv[k] = os.environ.get(k)
674 oldenv[k] = os.environ.get(k)
675 if cwd is not None:
675 if cwd is not None:
676 oldcwd = os.getcwd()
676 oldcwd = os.getcwd()
677 origcmd = cmd
677 origcmd = cmd
678 if os.name == 'nt':
678 if os.name == 'nt':
679 cmd = '"%s"' % cmd
679 cmd = '"%s"' % cmd
680 try:
680 try:
681 for k, v in environ.iteritems():
681 for k, v in environ.iteritems():
682 os.environ[k] = py2shell(v)
682 os.environ[k] = py2shell(v)
683 os.environ['HG'] = hgexecutable()
683 os.environ['HG'] = hgexecutable()
684 if cwd is not None and oldcwd != cwd:
684 if cwd is not None and oldcwd != cwd:
685 os.chdir(cwd)
685 os.chdir(cwd)
686 rc = os.system(cmd)
686 rc = os.system(cmd)
687 if sys.platform == 'OpenVMS' and rc & 1:
687 if sys.platform == 'OpenVMS' and rc & 1:
688 rc = 0
688 rc = 0
689 if rc and onerr:
689 if rc and onerr:
690 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
690 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
691 explain_exit(rc)[0])
691 explain_exit(rc)[0])
692 if errprefix:
692 if errprefix:
693 errmsg = '%s: %s' % (errprefix, errmsg)
693 errmsg = '%s: %s' % (errprefix, errmsg)
694 try:
694 try:
695 onerr.warn(errmsg + '\n')
695 onerr.warn(errmsg + '\n')
696 except AttributeError:
696 except AttributeError:
697 raise onerr(errmsg)
697 raise onerr(errmsg)
698 return rc
698 return rc
699 finally:
699 finally:
700 for k, v in oldenv.iteritems():
700 for k, v in oldenv.iteritems():
701 if v is None:
701 if v is None:
702 del os.environ[k]
702 del os.environ[k]
703 else:
703 else:
704 os.environ[k] = v
704 os.environ[k] = v
705 if cwd is not None and oldcwd != cwd:
705 if cwd is not None and oldcwd != cwd:
706 os.chdir(oldcwd)
706 os.chdir(oldcwd)
707
707
708 class SignatureError:
708 class SignatureError:
709 pass
709 pass
710
710
711 def checksignature(func):
711 def checksignature(func):
712 '''wrap a function with code to check for calling errors'''
712 '''wrap a function with code to check for calling errors'''
713 def check(*args, **kwargs):
713 def check(*args, **kwargs):
714 try:
714 try:
715 return func(*args, **kwargs)
715 return func(*args, **kwargs)
716 except TypeError:
716 except TypeError:
717 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
717 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
718 raise SignatureError
718 raise SignatureError
719 raise
719 raise
720
720
721 return check
721 return check
722
722
723 # os.path.lexists is not available on python2.3
723 # os.path.lexists is not available on python2.3
724 def lexists(filename):
724 def lexists(filename):
725 "test whether a file with this name exists. does not follow symlinks"
725 "test whether a file with this name exists. does not follow symlinks"
726 try:
726 try:
727 os.lstat(filename)
727 os.lstat(filename)
728 except:
728 except:
729 return False
729 return False
730 return True
730 return True
731
731
732 def rename(src, dst):
732 def rename(src, dst):
733 """forcibly rename a file"""
733 """forcibly rename a file"""
734 try:
734 try:
735 os.rename(src, dst)
735 os.rename(src, dst)
736 except OSError, err: # FIXME: check err (EEXIST ?)
736 except OSError, err: # FIXME: check err (EEXIST ?)
737 # on windows, rename to existing file is not allowed, so we
737 # on windows, rename to existing file is not allowed, so we
738 # must delete destination first. but if file is open, unlink
738 # must delete destination first. but if file is open, unlink
739 # schedules it for delete but does not delete it. rename
739 # schedules it for delete but does not delete it. rename
740 # happens immediately even for open files, so we create
740 # happens immediately even for open files, so we create
741 # temporary file, delete it, rename destination to that name,
741 # temporary file, delete it, rename destination to that name,
742 # then delete that. then rename is safe to do.
742 # then delete that. then rename is safe to do.
743 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
743 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
744 os.close(fd)
744 os.close(fd)
745 os.unlink(temp)
745 os.unlink(temp)
746 os.rename(dst, temp)
746 os.rename(dst, temp)
747 os.unlink(temp)
747 os.unlink(temp)
748 os.rename(src, dst)
748 os.rename(src, dst)
749
749
750 def unlink(f):
750 def unlink(f):
751 """unlink and remove the directory if it is empty"""
751 """unlink and remove the directory if it is empty"""
752 os.unlink(f)
752 os.unlink(f)
753 # try removing directories that might now be empty
753 # try removing directories that might now be empty
754 try:
754 try:
755 os.removedirs(os.path.dirname(f))
755 os.removedirs(os.path.dirname(f))
756 except OSError:
756 except OSError:
757 pass
757 pass
758
758
759 def copyfile(src, dest):
759 def copyfile(src, dest):
760 "copy a file, preserving mode"
760 "copy a file, preserving mode"
761 if os.path.islink(src):
761 if os.path.islink(src):
762 try:
762 try:
763 os.unlink(dest)
763 os.unlink(dest)
764 except:
764 except:
765 pass
765 pass
766 os.symlink(os.readlink(src), dest)
766 os.symlink(os.readlink(src), dest)
767 else:
767 else:
768 try:
768 try:
769 shutil.copyfile(src, dest)
769 shutil.copyfile(src, dest)
770 shutil.copymode(src, dest)
770 shutil.copymode(src, dest)
771 except shutil.Error, inst:
771 except shutil.Error, inst:
772 raise Abort(str(inst))
772 raise Abort(str(inst))
773
773
774 def copyfiles(src, dst, hardlink=None):
774 def copyfiles(src, dst, hardlink=None):
775 """Copy a directory tree using hardlinks if possible"""
775 """Copy a directory tree using hardlinks if possible"""
776
776
777 if hardlink is None:
777 if hardlink is None:
778 hardlink = (os.stat(src).st_dev ==
778 hardlink = (os.stat(src).st_dev ==
779 os.stat(os.path.dirname(dst)).st_dev)
779 os.stat(os.path.dirname(dst)).st_dev)
780
780
781 if os.path.isdir(src):
781 if os.path.isdir(src):
782 os.mkdir(dst)
782 os.mkdir(dst)
783 for name, kind in osutil.listdir(src):
783 for name, kind in osutil.listdir(src):
784 srcname = os.path.join(src, name)
784 srcname = os.path.join(src, name)
785 dstname = os.path.join(dst, name)
785 dstname = os.path.join(dst, name)
786 copyfiles(srcname, dstname, hardlink)
786 copyfiles(srcname, dstname, hardlink)
787 else:
787 else:
788 if hardlink:
788 if hardlink:
789 try:
789 try:
790 os_link(src, dst)
790 os_link(src, dst)
791 except (IOError, OSError):
791 except (IOError, OSError):
792 hardlink = False
792 hardlink = False
793 shutil.copy(src, dst)
793 shutil.copy(src, dst)
794 else:
794 else:
795 shutil.copy(src, dst)
795 shutil.copy(src, dst)
796
796
797 class path_auditor(object):
797 class path_auditor(object):
798 '''ensure that a filesystem path contains no banned components.
798 '''ensure that a filesystem path contains no banned components.
799 the following properties of a path are checked:
799 the following properties of a path are checked:
800
800
801 - under top-level .hg
801 - under top-level .hg
802 - starts at the root of a windows drive
802 - starts at the root of a windows drive
803 - contains ".."
803 - contains ".."
804 - traverses a symlink (e.g. a/symlink_here/b)
804 - traverses a symlink (e.g. a/symlink_here/b)
805 - inside a nested repository'''
805 - inside a nested repository'''
806
806
807 def __init__(self, root):
807 def __init__(self, root):
808 self.audited = set()
808 self.audited = set()
809 self.auditeddir = set()
809 self.auditeddir = set()
810 self.root = root
810 self.root = root
811
811
812 def __call__(self, path):
812 def __call__(self, path):
813 if path in self.audited:
813 if path in self.audited:
814 return
814 return
815 normpath = os.path.normcase(path)
815 normpath = os.path.normcase(path)
816 parts = splitpath(normpath)
816 parts = splitpath(normpath)
817 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
817 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
818 or os.pardir in parts):
818 or os.pardir in parts):
819 raise Abort(_("path contains illegal component: %s") % path)
819 raise Abort(_("path contains illegal component: %s") % path)
820 def check(prefix):
820 def check(prefix):
821 curpath = os.path.join(self.root, prefix)
821 curpath = os.path.join(self.root, prefix)
822 try:
822 try:
823 st = os.lstat(curpath)
823 st = os.lstat(curpath)
824 except OSError, err:
824 except OSError, err:
825 # EINVAL can be raised as invalid path syntax under win32.
825 # EINVAL can be raised as invalid path syntax under win32.
826 # They must be ignored for patterns can be checked too.
826 # They must be ignored for patterns can be checked too.
827 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
827 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
828 raise
828 raise
829 else:
829 else:
830 if stat.S_ISLNK(st.st_mode):
830 if stat.S_ISLNK(st.st_mode):
831 raise Abort(_('path %r traverses symbolic link %r') %
831 raise Abort(_('path %r traverses symbolic link %r') %
832 (path, prefix))
832 (path, prefix))
833 elif (stat.S_ISDIR(st.st_mode) and
833 elif (stat.S_ISDIR(st.st_mode) and
834 os.path.isdir(os.path.join(curpath, '.hg'))):
834 os.path.isdir(os.path.join(curpath, '.hg'))):
835 raise Abort(_('path %r is inside repo %r') %
835 raise Abort(_('path %r is inside repo %r') %
836 (path, prefix))
836 (path, prefix))
837 parts.pop()
837 parts.pop()
838 prefixes = []
838 prefixes = []
839 for n in range(len(parts)):
839 for n in range(len(parts)):
840 prefix = os.sep.join(parts)
840 prefix = os.sep.join(parts)
841 if prefix in self.auditeddir:
841 if prefix in self.auditeddir:
842 break
842 break
843 check(prefix)
843 check(prefix)
844 prefixes.append(prefix)
844 prefixes.append(prefix)
845 parts.pop()
845 parts.pop()
846
846
847 self.audited.add(path)
847 self.audited.add(path)
848 # only add prefixes to the cache after checking everything: we don't
848 # only add prefixes to the cache after checking everything: we don't
849 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
849 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
850 self.auditeddir.update(prefixes)
850 self.auditeddir.update(prefixes)
851
851
852 def _makelock_file(info, pathname):
852 def _makelock_file(info, pathname):
853 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
853 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
854 os.write(ld, info)
854 os.write(ld, info)
855 os.close(ld)
855 os.close(ld)
856
856
857 def _readlock_file(pathname):
857 def _readlock_file(pathname):
858 return posixfile(pathname).read()
858 return posixfile(pathname).read()
859
859
860 def nlinks(pathname):
860 def nlinks(pathname):
861 """Return number of hardlinks for the given file."""
861 """Return number of hardlinks for the given file."""
862 return os.lstat(pathname).st_nlink
862 return os.lstat(pathname).st_nlink
863
863
864 if hasattr(os, 'link'):
864 if hasattr(os, 'link'):
865 os_link = os.link
865 os_link = os.link
866 else:
866 else:
867 def os_link(src, dst):
867 def os_link(src, dst):
868 raise OSError(0, _("Hardlinks not supported"))
868 raise OSError(0, _("Hardlinks not supported"))
869
869
870 def fstat(fp):
870 def fstat(fp):
871 '''stat file object that may not have fileno method.'''
871 '''stat file object that may not have fileno method.'''
872 try:
872 try:
873 return os.fstat(fp.fileno())
873 return os.fstat(fp.fileno())
874 except AttributeError:
874 except AttributeError:
875 return os.stat(fp.name)
875 return os.stat(fp.name)
876
876
877 posixfile = file
877 posixfile = file
878
878
879 def openhardlinks():
879 def openhardlinks():
880 '''return true if it is safe to hold open file handles to hardlinks'''
880 '''return true if it is safe to hold open file handles to hardlinks'''
881 return True
881 return True
882
882
883 def _statfiles(files):
883 def _statfiles(files):
884 'Stat each file in files and yield stat or None if file does not exist.'
884 'Stat each file in files and yield stat or None if file does not exist.'
885 lstat = os.lstat
885 lstat = os.lstat
886 for nf in files:
886 for nf in files:
887 try:
887 try:
888 st = lstat(nf)
888 st = lstat(nf)
889 except OSError, err:
889 except OSError, err:
890 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
890 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
891 raise
891 raise
892 st = None
892 st = None
893 yield st
893 yield st
894
894
895 def _statfiles_clustered(files):
895 def _statfiles_clustered(files):
896 '''Stat each file in files and yield stat or None if file does not exist.
896 '''Stat each file in files and yield stat or None if file does not exist.
897 Cluster and cache stat per directory to minimize number of OS stat calls.'''
897 Cluster and cache stat per directory to minimize number of OS stat calls.'''
898 lstat = os.lstat
898 lstat = os.lstat
899 ncase = os.path.normcase
899 ncase = os.path.normcase
900 sep = os.sep
900 sep = os.sep
901 dircache = {} # dirname -> filename -> status | None if file does not exist
901 dircache = {} # dirname -> filename -> status | None if file does not exist
902 for nf in files:
902 for nf in files:
903 nf = ncase(nf)
903 nf = ncase(nf)
904 pos = nf.rfind(sep)
904 pos = nf.rfind(sep)
905 if pos == -1:
905 if pos == -1:
906 dir, base = '.', nf
906 dir, base = '.', nf
907 else:
907 else:
908 dir, base = nf[:pos+1], nf[pos+1:]
908 dir, base = nf[:pos+1], nf[pos+1:]
909 cache = dircache.get(dir, None)
909 cache = dircache.get(dir, None)
910 if cache is None:
910 if cache is None:
911 try:
911 try:
912 dmap = dict([(ncase(n), s)
912 dmap = dict([(ncase(n), s)
913 for n, k, s in osutil.listdir(dir, True)])
913 for n, k, s in osutil.listdir(dir, True)])
914 except OSError, err:
914 except OSError, err:
915 # handle directory not found in Python version prior to 2.5
915 # handle directory not found in Python version prior to 2.5
916 # Python <= 2.4 returns native Windows code 3 in errno
916 # Python <= 2.4 returns native Windows code 3 in errno
917 # Python >= 2.5 returns ENOENT and adds winerror field
917 # Python >= 2.5 returns ENOENT and adds winerror field
918 # EINVAL is raised if dir is not a directory.
918 # EINVAL is raised if dir is not a directory.
919 if err.errno not in (3, errno.ENOENT, errno.EINVAL,
919 if err.errno not in (3, errno.ENOENT, errno.EINVAL,
920 errno.ENOTDIR):
920 errno.ENOTDIR):
921 raise
921 raise
922 dmap = {}
922 dmap = {}
923 cache = dircache.setdefault(dir, dmap)
923 cache = dircache.setdefault(dir, dmap)
924 yield cache.get(base, None)
924 yield cache.get(base, None)
925
925
926 if sys.platform == 'win32':
926 if sys.platform == 'win32':
927 statfiles = _statfiles_clustered
927 statfiles = _statfiles_clustered
928 else:
928 else:
929 statfiles = _statfiles
929 statfiles = _statfiles
930
930
931 getuser_fallback = None
931 getuser_fallback = None
932
932
933 def getuser():
933 def getuser():
934 '''return name of current user'''
934 '''return name of current user'''
935 try:
935 try:
936 return getpass.getuser()
936 return getpass.getuser()
937 except ImportError:
937 except ImportError:
938 # import of pwd will fail on windows - try fallback
938 # import of pwd will fail on windows - try fallback
939 if getuser_fallback:
939 if getuser_fallback:
940 return getuser_fallback()
940 return getuser_fallback()
941 # raised if win32api not available
941 # raised if win32api not available
942 raise Abort(_('user name not available - set USERNAME '
942 raise Abort(_('user name not available - set USERNAME '
943 'environment variable'))
943 'environment variable'))
944
944
945 def username(uid=None):
945 def username(uid=None):
946 """Return the name of the user with the given uid.
946 """Return the name of the user with the given uid.
947
947
948 If uid is None, return the name of the current user."""
948 If uid is None, return the name of the current user."""
949 try:
949 try:
950 import pwd
950 import pwd
951 if uid is None:
951 if uid is None:
952 uid = os.getuid()
952 uid = os.getuid()
953 try:
953 try:
954 return pwd.getpwuid(uid)[0]
954 return pwd.getpwuid(uid)[0]
955 except KeyError:
955 except KeyError:
956 return str(uid)
956 return str(uid)
957 except ImportError:
957 except ImportError:
958 return None
958 return None
959
959
960 def groupname(gid=None):
960 def groupname(gid=None):
961 """Return the name of the group with the given gid.
961 """Return the name of the group with the given gid.
962
962
963 If gid is None, return the name of the current group."""
963 If gid is None, return the name of the current group."""
964 try:
964 try:
965 import grp
965 import grp
966 if gid is None:
966 if gid is None:
967 gid = os.getgid()
967 gid = os.getgid()
968 try:
968 try:
969 return grp.getgrgid(gid)[0]
969 return grp.getgrgid(gid)[0]
970 except KeyError:
970 except KeyError:
971 return str(gid)
971 return str(gid)
972 except ImportError:
972 except ImportError:
973 return None
973 return None
974
974
975 # File system features
975 # File system features
976
976
977 def checkcase(path):
977 def checkcase(path):
978 """
978 """
979 Check whether the given path is on a case-sensitive filesystem
979 Check whether the given path is on a case-sensitive filesystem
980
980
981 Requires a path (like /foo/.hg) ending with a foldable final
981 Requires a path (like /foo/.hg) ending with a foldable final
982 directory component.
982 directory component.
983 """
983 """
984 s1 = os.stat(path)
984 s1 = os.stat(path)
985 d, b = os.path.split(path)
985 d, b = os.path.split(path)
986 p2 = os.path.join(d, b.upper())
986 p2 = os.path.join(d, b.upper())
987 if path == p2:
987 if path == p2:
988 p2 = os.path.join(d, b.lower())
988 p2 = os.path.join(d, b.lower())
989 try:
989 try:
990 s2 = os.stat(p2)
990 s2 = os.stat(p2)
991 if s2 == s1:
991 if s2 == s1:
992 return False
992 return False
993 return True
993 return True
994 except:
994 except:
995 return True
995 return True
996
996
997 _fspathcache = {}
997 _fspathcache = {}
998 def fspath(name, root):
998 def fspath(name, root):
999 '''Get name in the case stored in the filesystem
999 '''Get name in the case stored in the filesystem
1000
1000
1001 The name is either relative to root, or it is an absolute path starting
1001 The name is either relative to root, or it is an absolute path starting
1002 with root. Note that this function is unnecessary, and should not be
1002 with root. Note that this function is unnecessary, and should not be
1003 called, for case-sensitive filesystems (simply because it's expensive).
1003 called, for case-sensitive filesystems (simply because it's expensive).
1004 '''
1004 '''
1005 # If name is absolute, make it relative
1005 # If name is absolute, make it relative
1006 if name.lower().startswith(root.lower()):
1006 if name.lower().startswith(root.lower()):
1007 l = len(root)
1007 l = len(root)
1008 if name[l] == os.sep or name[l] == os.altsep:
1008 if name[l] == os.sep or name[l] == os.altsep:
1009 l = l + 1
1009 l = l + 1
1010 name = name[l:]
1010 name = name[l:]
1011
1011
1012 if not os.path.exists(os.path.join(root, name)):
1012 if not os.path.exists(os.path.join(root, name)):
1013 return None
1013 return None
1014
1014
1015 seps = os.sep
1015 seps = os.sep
1016 if os.altsep:
1016 if os.altsep:
1017 seps = seps + os.altsep
1017 seps = seps + os.altsep
1018 # Protect backslashes. This gets silly very quickly.
1018 # Protect backslashes. This gets silly very quickly.
1019 seps.replace('\\','\\\\')
1019 seps.replace('\\','\\\\')
1020 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1020 pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1021 dir = os.path.normcase(os.path.normpath(root))
1021 dir = os.path.normcase(os.path.normpath(root))
1022 result = []
1022 result = []
1023 for part, sep in pattern.findall(name):
1023 for part, sep in pattern.findall(name):
1024 if sep:
1024 if sep:
1025 result.append(sep)
1025 result.append(sep)
1026 continue
1026 continue
1027
1027
1028 if dir not in _fspathcache:
1028 if dir not in _fspathcache:
1029 _fspathcache[dir] = os.listdir(dir)
1029 _fspathcache[dir] = os.listdir(dir)
1030 contents = _fspathcache[dir]
1030 contents = _fspathcache[dir]
1031
1031
1032 lpart = part.lower()
1032 lpart = part.lower()
1033 for n in contents:
1033 for n in contents:
1034 if n.lower() == lpart:
1034 if n.lower() == lpart:
1035 result.append(n)
1035 result.append(n)
1036 break
1036 break
1037 else:
1037 else:
1038 # Cannot happen, as the file exists!
1038 # Cannot happen, as the file exists!
1039 result.append(part)
1039 result.append(part)
1040 dir = os.path.join(dir, lpart)
1040 dir = os.path.join(dir, lpart)
1041
1041
1042 return ''.join(result)
1042 return ''.join(result)
1043
1043
1044 def checkexec(path):
1044 def checkexec(path):
1045 """
1045 """
1046 Check whether the given path is on a filesystem with UNIX-like exec flags
1046 Check whether the given path is on a filesystem with UNIX-like exec flags
1047
1047
1048 Requires a directory (like /foo/.hg)
1048 Requires a directory (like /foo/.hg)
1049 """
1049 """
1050
1050
1051 # VFAT on some Linux versions can flip mode but it doesn't persist
1051 # VFAT on some Linux versions can flip mode but it doesn't persist
1052 # a FS remount. Frequently we can detect it if files are created
1052 # a FS remount. Frequently we can detect it if files are created
1053 # with exec bit on.
1053 # with exec bit on.
1054
1054
1055 try:
1055 try:
1056 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
1056 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
1057 fh, fn = tempfile.mkstemp("", "", path)
1057 fh, fn = tempfile.mkstemp("", "", path)
1058 try:
1058 try:
1059 os.close(fh)
1059 os.close(fh)
1060 m = os.stat(fn).st_mode & 0777
1060 m = os.stat(fn).st_mode & 0777
1061 new_file_has_exec = m & EXECFLAGS
1061 new_file_has_exec = m & EXECFLAGS
1062 os.chmod(fn, m ^ EXECFLAGS)
1062 os.chmod(fn, m ^ EXECFLAGS)
1063 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
1063 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m)
1064 finally:
1064 finally:
1065 os.unlink(fn)
1065 os.unlink(fn)
1066 except (IOError, OSError):
1066 except (IOError, OSError):
1067 # we don't care, the user probably won't be able to commit anyway
1067 # we don't care, the user probably won't be able to commit anyway
1068 return False
1068 return False
1069 return not (new_file_has_exec or exec_flags_cannot_flip)
1069 return not (new_file_has_exec or exec_flags_cannot_flip)
1070
1070
1071 def checklink(path):
1071 def checklink(path):
1072 """check whether the given path is on a symlink-capable filesystem"""
1072 """check whether the given path is on a symlink-capable filesystem"""
1073 # mktemp is not racy because symlink creation will fail if the
1073 # mktemp is not racy because symlink creation will fail if the
1074 # file already exists
1074 # file already exists
1075 name = tempfile.mktemp(dir=path)
1075 name = tempfile.mktemp(dir=path)
1076 try:
1076 try:
1077 os.symlink(".", name)
1077 os.symlink(".", name)
1078 os.unlink(name)
1078 os.unlink(name)
1079 return True
1079 return True
1080 except (OSError, AttributeError):
1080 except (OSError, AttributeError):
1081 return False
1081 return False
1082
1082
1083 _umask = os.umask(0)
1083 _umask = os.umask(0)
1084 os.umask(_umask)
1084 os.umask(_umask)
1085
1085
1086 def needbinarypatch():
1086 def needbinarypatch():
1087 """return True if patches should be applied in binary mode by default."""
1087 """return True if patches should be applied in binary mode by default."""
1088 return os.name == 'nt'
1088 return os.name == 'nt'
1089
1089
1090 def endswithsep(path):
1090 def endswithsep(path):
1091 '''Check path ends with os.sep or os.altsep.'''
1091 '''Check path ends with os.sep or os.altsep.'''
1092 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1092 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1093
1093
1094 def splitpath(path):
1094 def splitpath(path):
1095 '''Split path by os.sep.
1095 '''Split path by os.sep.
1096 Note that this function does not use os.altsep because this is
1096 Note that this function does not use os.altsep because this is
1097 an alternative of simple "xxx.split(os.sep)".
1097 an alternative of simple "xxx.split(os.sep)".
1098 It is recommended to use os.path.normpath() before using this
1098 It is recommended to use os.path.normpath() before using this
1099 function if need.'''
1099 function if need.'''
1100 return path.split(os.sep)
1100 return path.split(os.sep)
1101
1101
1102 def gui():
1102 def gui():
1103 '''Are we running in a GUI?'''
1103 '''Are we running in a GUI?'''
1104 return os.name == "nt" or os.name == "mac" or os.environ.get("DISPLAY")
1104 return os.name == "nt" or os.name == "mac" or os.environ.get("DISPLAY")
1105
1105
1106 def lookup_reg(key, name=None, scope=None):
1106 def lookup_reg(key, name=None, scope=None):
1107 return None
1107 return None
1108
1108
1109 # Platform specific variants
1109 # Platform specific variants
1110 if os.name == 'nt':
1110 if os.name == 'nt':
1111 import msvcrt
1111 import msvcrt
1112 nulldev = 'NUL:'
1112 nulldev = 'NUL:'
1113
1113
1114 class winstdout:
1114 class winstdout:
1115 '''stdout on windows misbehaves if sent through a pipe'''
1115 '''stdout on windows misbehaves if sent through a pipe'''
1116
1116
1117 def __init__(self, fp):
1117 def __init__(self, fp):
1118 self.fp = fp
1118 self.fp = fp
1119
1119
1120 def __getattr__(self, key):
1120 def __getattr__(self, key):
1121 return getattr(self.fp, key)
1121 return getattr(self.fp, key)
1122
1122
1123 def close(self):
1123 def close(self):
1124 try:
1124 try:
1125 self.fp.close()
1125 self.fp.close()
1126 except: pass
1126 except: pass
1127
1127
1128 def write(self, s):
1128 def write(self, s):
1129 try:
1129 try:
1130 # This is workaround for "Not enough space" error on
1130 # This is workaround for "Not enough space" error on
1131 # writing large size of data to console.
1131 # writing large size of data to console.
1132 limit = 16000
1132 limit = 16000
1133 l = len(s)
1133 l = len(s)
1134 start = 0
1134 start = 0
1135 while start < l:
1135 while start < l:
1136 end = start + limit
1136 end = start + limit
1137 self.fp.write(s[start:end])
1137 self.fp.write(s[start:end])
1138 start = end
1138 start = end
1139 except IOError, inst:
1139 except IOError, inst:
1140 if inst.errno != 0: raise
1140 if inst.errno != 0: raise
1141 self.close()
1141 self.close()
1142 raise IOError(errno.EPIPE, 'Broken pipe')
1142 raise IOError(errno.EPIPE, 'Broken pipe')
1143
1143
1144 def flush(self):
1144 def flush(self):
1145 try:
1145 try:
1146 return self.fp.flush()
1146 return self.fp.flush()
1147 except IOError, inst:
1147 except IOError, inst:
1148 if inst.errno != errno.EINVAL: raise
1148 if inst.errno != errno.EINVAL: raise
1149 self.close()
1149 self.close()
1150 raise IOError(errno.EPIPE, 'Broken pipe')
1150 raise IOError(errno.EPIPE, 'Broken pipe')
1151
1151
1152 sys.stdout = winstdout(sys.stdout)
1152 sys.stdout = winstdout(sys.stdout)
1153
1153
1154 def _is_win_9x():
1154 def _is_win_9x():
1155 '''return true if run on windows 95, 98 or me.'''
1155 '''return true if run on windows 95, 98 or me.'''
1156 try:
1156 try:
1157 return sys.getwindowsversion()[3] == 1
1157 return sys.getwindowsversion()[3] == 1
1158 except AttributeError:
1158 except AttributeError:
1159 return 'command' in os.environ.get('comspec', '')
1159 return 'command' in os.environ.get('comspec', '')
1160
1160
1161 def openhardlinks():
1161 def openhardlinks():
1162 return not _is_win_9x and "win32api" in locals()
1162 return not _is_win_9x and "win32api" in locals()
1163
1163
1164 def system_rcpath():
1164 def system_rcpath():
1165 try:
1165 try:
1166 return system_rcpath_win32()
1166 return system_rcpath_win32()
1167 except:
1167 except:
1168 return [r'c:\mercurial\mercurial.ini']
1168 return [r'c:\mercurial\mercurial.ini']
1169
1169
1170 def user_rcpath():
1170 def user_rcpath():
1171 '''return os-specific hgrc search path to the user dir'''
1171 '''return os-specific hgrc search path to the user dir'''
1172 try:
1172 try:
1173 path = user_rcpath_win32()
1173 path = user_rcpath_win32()
1174 except:
1174 except:
1175 home = os.path.expanduser('~')
1175 home = os.path.expanduser('~')
1176 path = [os.path.join(home, 'mercurial.ini'),
1176 path = [os.path.join(home, 'mercurial.ini'),
1177 os.path.join(home, '.hgrc')]
1177 os.path.join(home, '.hgrc')]
1178 userprofile = os.environ.get('USERPROFILE')
1178 userprofile = os.environ.get('USERPROFILE')
1179 if userprofile:
1179 if userprofile:
1180 path.append(os.path.join(userprofile, 'mercurial.ini'))
1180 path.append(os.path.join(userprofile, 'mercurial.ini'))
1181 path.append(os.path.join(userprofile, '.hgrc'))
1181 path.append(os.path.join(userprofile, '.hgrc'))
1182 return path
1182 return path
1183
1183
1184 def parse_patch_output(output_line):
1184 def parse_patch_output(output_line):
1185 """parses the output produced by patch and returns the file name"""
1185 """parses the output produced by patch and returns the file name"""
1186 pf = output_line[14:]
1186 pf = output_line[14:]
1187 if pf[0] == '`':
1187 if pf[0] == '`':
1188 pf = pf[1:-1] # Remove the quotes
1188 pf = pf[1:-1] # Remove the quotes
1189 return pf
1189 return pf
1190
1190
1191 def sshargs(sshcmd, host, user, port):
1191 def sshargs(sshcmd, host, user, port):
1192 '''Build argument list for ssh or Plink'''
1192 '''Build argument list for ssh or Plink'''
1193 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
1193 pflag = 'plink' in sshcmd.lower() and '-P' or '-p'
1194 args = user and ("%s@%s" % (user, host)) or host
1194 args = user and ("%s@%s" % (user, host)) or host
1195 return port and ("%s %s %s" % (args, pflag, port)) or args
1195 return port and ("%s %s %s" % (args, pflag, port)) or args
1196
1196
1197 def testpid(pid):
1197 def testpid(pid):
1198 '''return False if pid dead, True if running or not known'''
1198 '''return False if pid dead, True if running or not known'''
1199 return True
1199 return True
1200
1200
1201 def set_flags(f, l, x):
1201 def set_flags(f, l, x):
1202 pass
1202 pass
1203
1203
1204 def set_binary(fd):
1204 def set_binary(fd):
1205 # When run without console, pipes may expose invalid
1205 # When run without console, pipes may expose invalid
1206 # fileno(), usually set to -1.
1206 # fileno(), usually set to -1.
1207 if hasattr(fd, 'fileno') and fd.fileno() >= 0:
1207 if hasattr(fd, 'fileno') and fd.fileno() >= 0:
1208 msvcrt.setmode(fd.fileno(), os.O_BINARY)
1208 msvcrt.setmode(fd.fileno(), os.O_BINARY)
1209
1209
1210 def pconvert(path):
1210 def pconvert(path):
1211 return '/'.join(splitpath(path))
1211 return '/'.join(splitpath(path))
1212
1212
1213 def localpath(path):
1213 def localpath(path):
1214 return path.replace('/', '\\')
1214 return path.replace('/', '\\')
1215
1215
1216 def normpath(path):
1216 def normpath(path):
1217 return pconvert(os.path.normpath(path))
1217 return pconvert(os.path.normpath(path))
1218
1218
1219 makelock = _makelock_file
1219 makelock = _makelock_file
1220 readlock = _readlock_file
1220 readlock = _readlock_file
1221
1221
1222 def samestat(s1, s2):
1222 def samestat(s1, s2):
1223 return False
1223 return False
1224
1224
1225 # A sequence of backslashes is special iff it precedes a double quote:
1225 # A sequence of backslashes is special iff it precedes a double quote:
1226 # - if there's an even number of backslashes, the double quote is not
1226 # - if there's an even number of backslashes, the double quote is not
1227 # quoted (i.e. it ends the quoted region)
1227 # quoted (i.e. it ends the quoted region)
1228 # - if there's an odd number of backslashes, the double quote is quoted
1228 # - if there's an odd number of backslashes, the double quote is quoted
1229 # - in both cases, every pair of backslashes is unquoted into a single
1229 # - in both cases, every pair of backslashes is unquoted into a single
1230 # backslash
1230 # backslash
1231 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
1231 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
1232 # So, to quote a string, we must surround it in double quotes, double
1232 # So, to quote a string, we must surround it in double quotes, double
1233 # the number of backslashes that preceed double quotes and add another
1233 # the number of backslashes that preceed double quotes and add another
1234 # backslash before every double quote (being careful with the double
1234 # backslash before every double quote (being careful with the double
1235 # quote we've appended to the end)
1235 # quote we've appended to the end)
1236 _quotere = None
1236 _quotere = None
1237 def shellquote(s):
1237 def shellquote(s):
1238 global _quotere
1238 global _quotere
1239 if _quotere is None:
1239 if _quotere is None:
1240 _quotere = re.compile(r'(\\*)("|\\$)')
1240 _quotere = re.compile(r'(\\*)("|\\$)')
1241 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1241 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1242
1242
1243 def quotecommand(cmd):
1243 def quotecommand(cmd):
1244 """Build a command string suitable for os.popen* calls."""
1244 """Build a command string suitable for os.popen* calls."""
1245 # The extra quotes are needed because popen* runs the command
1245 # The extra quotes are needed because popen* runs the command
1246 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1246 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1247 return '"' + cmd + '"'
1247 return '"' + cmd + '"'
1248
1248
1249 def popen(command, mode='r'):
1249 def popen(command, mode='r'):
1250 # Work around "popen spawned process may not write to stdout
1250 # Work around "popen spawned process may not write to stdout
1251 # under windows"
1251 # under windows"
1252 # http://bugs.python.org/issue1366
1252 # http://bugs.python.org/issue1366
1253 command += " 2> %s" % nulldev
1253 command += " 2> %s" % nulldev
1254 return os.popen(quotecommand(command), mode)
1254 return os.popen(quotecommand(command), mode)
1255
1255
1256 def explain_exit(code):
1256 def explain_exit(code):
1257 return _("exited with status %d") % code, code
1257 return _("exited with status %d") % code, code
1258
1258
1259 # if you change this stub into a real check, please try to implement the
1259 # if you change this stub into a real check, please try to implement the
1260 # username and groupname functions above, too.
1260 # username and groupname functions above, too.
1261 def isowner(fp, st=None):
1261 def isowner(fp, st=None):
1262 return True
1262 return True
1263
1263
1264 def find_in_path(name, path, default=None):
1264 def find_in_path(name, path, default=None):
1265 '''find name in search path. path can be string (will be split
1265 '''find name in search path. path can be string (will be split
1266 with os.pathsep), or iterable thing that returns strings. if name
1266 with os.pathsep), or iterable thing that returns strings. if name
1267 found, return path to name. else return default. name is looked up
1267 found, return path to name. else return default. name is looked up
1268 using cmd.exe rules, using PATHEXT.'''
1268 using cmd.exe rules, using PATHEXT.'''
1269 if isinstance(path, str):
1269 if isinstance(path, str):
1270 path = path.split(os.pathsep)
1270 path = path.split(os.pathsep)
1271
1271
1272 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1272 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1273 pathext = pathext.lower().split(os.pathsep)
1273 pathext = pathext.lower().split(os.pathsep)
1274 isexec = os.path.splitext(name)[1].lower() in pathext
1274 isexec = os.path.splitext(name)[1].lower() in pathext
1275
1275
1276 for p in path:
1276 for p in path:
1277 p_name = os.path.join(p, name)
1277 p_name = os.path.join(p, name)
1278
1278
1279 if isexec and os.path.exists(p_name):
1279 if isexec and os.path.exists(p_name):
1280 return p_name
1280 return p_name
1281
1281
1282 for ext in pathext:
1282 for ext in pathext:
1283 p_name_ext = p_name + ext
1283 p_name_ext = p_name + ext
1284 if os.path.exists(p_name_ext):
1284 if os.path.exists(p_name_ext):
1285 return p_name_ext
1285 return p_name_ext
1286 return default
1286 return default
1287
1287
1288 def set_signal_handler():
1288 def set_signal_handler():
1289 try:
1289 try:
1290 set_signal_handler_win32()
1290 set_signal_handler_win32()
1291 except NameError:
1291 except NameError:
1292 pass
1292 pass
1293
1293
1294 try:
1294 try:
1295 # override functions with win32 versions if possible
1295 # override functions with win32 versions if possible
1296 from util_win32 import *
1296 from util_win32 import *
1297 if not _is_win_9x():
1297 if not _is_win_9x():
1298 posixfile = posixfile_nt
1298 posixfile = posixfile_nt
1299 except ImportError:
1299 except ImportError:
1300 pass
1300 pass
1301
1301
1302 else:
1302 else:
1303 nulldev = '/dev/null'
1303 nulldev = '/dev/null'
1304
1304
1305 def rcfiles(path):
1305 def rcfiles(path):
1306 rcs = [os.path.join(path, 'hgrc')]
1306 rcs = [os.path.join(path, 'hgrc')]
1307 rcdir = os.path.join(path, 'hgrc.d')
1307 rcdir = os.path.join(path, 'hgrc.d')
1308 try:
1308 try:
1309 rcs.extend([os.path.join(rcdir, f)
1309 rcs.extend([os.path.join(rcdir, f)
1310 for f, kind in osutil.listdir(rcdir)
1310 for f, kind in osutil.listdir(rcdir)
1311 if f.endswith(".rc")])
1311 if f.endswith(".rc")])
1312 except OSError:
1312 except OSError:
1313 pass
1313 pass
1314 return rcs
1314 return rcs
1315
1315
1316 def system_rcpath():
1316 def system_rcpath():
1317 path = []
1317 path = []
1318 # old mod_python does not set sys.argv
1318 # old mod_python does not set sys.argv
1319 if len(getattr(sys, 'argv', [])) > 0:
1319 if len(getattr(sys, 'argv', [])) > 0:
1320 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1320 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1321 '/../etc/mercurial'))
1321 '/../etc/mercurial'))
1322 path.extend(rcfiles('/etc/mercurial'))
1322 path.extend(rcfiles('/etc/mercurial'))
1323 return path
1323 return path
1324
1324
1325 def user_rcpath():
1325 def user_rcpath():
1326 return [os.path.expanduser('~/.hgrc')]
1326 return [os.path.expanduser('~/.hgrc')]
1327
1327
1328 def parse_patch_output(output_line):
1328 def parse_patch_output(output_line):
1329 """parses the output produced by patch and returns the file name"""
1329 """parses the output produced by patch and returns the file name"""
1330 pf = output_line[14:]
1330 pf = output_line[14:]
1331 if os.sys.platform == 'OpenVMS':
1331 if os.sys.platform == 'OpenVMS':
1332 if pf[0] == '`':
1332 if pf[0] == '`':
1333 pf = pf[1:-1] # Remove the quotes
1333 pf = pf[1:-1] # Remove the quotes
1334 else:
1334 else:
1335 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1335 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1336 pf = pf[1:-1] # Remove the quotes
1336 pf = pf[1:-1] # Remove the quotes
1337 return pf
1337 return pf
1338
1338
1339 def sshargs(sshcmd, host, user, port):
1339 def sshargs(sshcmd, host, user, port):
1340 '''Build argument list for ssh'''
1340 '''Build argument list for ssh'''
1341 args = user and ("%s@%s" % (user, host)) or host
1341 args = user and ("%s@%s" % (user, host)) or host
1342 return port and ("%s -p %s" % (args, port)) or args
1342 return port and ("%s -p %s" % (args, port)) or args
1343
1343
1344 def is_exec(f):
1344 def is_exec(f):
1345 """check whether a file is executable"""
1345 """check whether a file is executable"""
1346 return (os.lstat(f).st_mode & 0100 != 0)
1346 return (os.lstat(f).st_mode & 0100 != 0)
1347
1347
1348 def set_flags(f, l, x):
1348 def set_flags(f, l, x):
1349 s = os.lstat(f).st_mode
1349 s = os.lstat(f).st_mode
1350 if l:
1350 if l:
1351 if not stat.S_ISLNK(s):
1351 if not stat.S_ISLNK(s):
1352 # switch file to link
1352 # switch file to link
1353 data = file(f).read()
1353 data = file(f).read()
1354 os.unlink(f)
1354 os.unlink(f)
1355 try:
1355 try:
1356 os.symlink(data, f)
1356 os.symlink(data, f)
1357 except:
1357 except:
1358 # failed to make a link, rewrite file
1358 # failed to make a link, rewrite file
1359 file(f, "w").write(data)
1359 file(f, "w").write(data)
1360 # no chmod needed at this point
1360 # no chmod needed at this point
1361 return
1361 return
1362 if stat.S_ISLNK(s):
1362 if stat.S_ISLNK(s):
1363 # switch link to file
1363 # switch link to file
1364 data = os.readlink(f)
1364 data = os.readlink(f)
1365 os.unlink(f)
1365 os.unlink(f)
1366 file(f, "w").write(data)
1366 file(f, "w").write(data)
1367 s = 0666 & ~_umask # avoid restatting for chmod
1367 s = 0666 & ~_umask # avoid restatting for chmod
1368
1368
1369 sx = s & 0100
1369 sx = s & 0100
1370 if x and not sx:
1370 if x and not sx:
1371 # Turn on +x for every +r bit when making a file executable
1371 # Turn on +x for every +r bit when making a file executable
1372 # and obey umask.
1372 # and obey umask.
1373 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1373 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1374 elif not x and sx:
1374 elif not x and sx:
1375 # Turn off all +x bits
1375 # Turn off all +x bits
1376 os.chmod(f, s & 0666)
1376 os.chmod(f, s & 0666)
1377
1377
1378 def set_binary(fd):
1378 def set_binary(fd):
1379 pass
1379 pass
1380
1380
1381 def pconvert(path):
1381 def pconvert(path):
1382 return path
1382 return path
1383
1383
1384 def localpath(path):
1384 def localpath(path):
1385 return path
1385 return path
1386
1386
1387 normpath = os.path.normpath
1387 normpath = os.path.normpath
1388 samestat = os.path.samestat
1388 samestat = os.path.samestat
1389
1389
1390 def makelock(info, pathname):
1390 def makelock(info, pathname):
1391 try:
1391 try:
1392 os.symlink(info, pathname)
1392 os.symlink(info, pathname)
1393 except OSError, why:
1393 except OSError, why:
1394 if why.errno == errno.EEXIST:
1394 if why.errno == errno.EEXIST:
1395 raise
1395 raise
1396 else:
1396 else:
1397 _makelock_file(info, pathname)
1397 _makelock_file(info, pathname)
1398
1398
1399 def readlock(pathname):
1399 def readlock(pathname):
1400 try:
1400 try:
1401 return os.readlink(pathname)
1401 return os.readlink(pathname)
1402 except OSError, why:
1402 except OSError, why:
1403 if why.errno in (errno.EINVAL, errno.ENOSYS):
1403 if why.errno in (errno.EINVAL, errno.ENOSYS):
1404 return _readlock_file(pathname)
1404 return _readlock_file(pathname)
1405 else:
1405 else:
1406 raise
1406 raise
1407
1407
1408 def shellquote(s):
1408 def shellquote(s):
1409 if os.sys.platform == 'OpenVMS':
1409 if os.sys.platform == 'OpenVMS':
1410 return '"%s"' % s
1410 return '"%s"' % s
1411 else:
1411 else:
1412 return "'%s'" % s.replace("'", "'\\''")
1412 return "'%s'" % s.replace("'", "'\\''")
1413
1413
1414 def quotecommand(cmd):
1414 def quotecommand(cmd):
1415 return cmd
1415 return cmd
1416
1416
1417 def popen(command, mode='r'):
1417 def popen(command, mode='r'):
1418 return os.popen(command, mode)
1418 return os.popen(command, mode)
1419
1419
1420 def testpid(pid):
1420 def testpid(pid):
1421 '''return False if pid dead, True if running or not sure'''
1421 '''return False if pid dead, True if running or not sure'''
1422 if os.sys.platform == 'OpenVMS':
1422 if os.sys.platform == 'OpenVMS':
1423 return True
1423 return True
1424 try:
1424 try:
1425 os.kill(pid, 0)
1425 os.kill(pid, 0)
1426 return True
1426 return True
1427 except OSError, inst:
1427 except OSError, inst:
1428 return inst.errno != errno.ESRCH
1428 return inst.errno != errno.ESRCH
1429
1429
1430 def explain_exit(code):
1430 def explain_exit(code):
1431 """return a 2-tuple (desc, code) describing a process's status"""
1431 """return a 2-tuple (desc, code) describing a process's status"""
1432 if os.WIFEXITED(code):
1432 if os.WIFEXITED(code):
1433 val = os.WEXITSTATUS(code)
1433 val = os.WEXITSTATUS(code)
1434 return _("exited with status %d") % val, val
1434 return _("exited with status %d") % val, val
1435 elif os.WIFSIGNALED(code):
1435 elif os.WIFSIGNALED(code):
1436 val = os.WTERMSIG(code)
1436 val = os.WTERMSIG(code)
1437 return _("killed by signal %d") % val, val
1437 return _("killed by signal %d") % val, val
1438 elif os.WIFSTOPPED(code):
1438 elif os.WIFSTOPPED(code):
1439 val = os.WSTOPSIG(code)
1439 val = os.WSTOPSIG(code)
1440 return _("stopped by signal %d") % val, val
1440 return _("stopped by signal %d") % val, val
1441 raise ValueError(_("invalid exit code"))
1441 raise ValueError(_("invalid exit code"))
1442
1442
1443 def isowner(fp, st=None):
1443 def isowner(fp, st=None):
1444 """Return True if the file object f belongs to the current user.
1444 """Return True if the file object f belongs to the current user.
1445
1445
1446 The return value of a util.fstat(f) may be passed as the st argument.
1446 The return value of a util.fstat(f) may be passed as the st argument.
1447 """
1447 """
1448 if st is None:
1448 if st is None:
1449 st = fstat(fp)
1449 st = fstat(fp)
1450 return st.st_uid == os.getuid()
1450 return st.st_uid == os.getuid()
1451
1451
1452 def find_in_path(name, path, default=None):
1452 def find_in_path(name, path, default=None):
1453 '''find name in search path. path can be string (will be split
1453 '''find name in search path. path can be string (will be split
1454 with os.pathsep), or iterable thing that returns strings. if name
1454 with os.pathsep), or iterable thing that returns strings. if name
1455 found, return path to name. else return default.'''
1455 found, return path to name. else return default.'''
1456 if isinstance(path, str):
1456 if isinstance(path, str):
1457 path = path.split(os.pathsep)
1457 path = path.split(os.pathsep)
1458 for p in path:
1458 for p in path:
1459 p_name = os.path.join(p, name)
1459 p_name = os.path.join(p, name)
1460 if os.path.exists(p_name):
1460 if os.path.exists(p_name):
1461 return p_name
1461 return p_name
1462 return default
1462 return default
1463
1463
1464 def set_signal_handler():
1464 def set_signal_handler():
1465 pass
1465 pass
1466
1466
1467 def find_exe(name, default=None):
1467 def find_exe(name, default=None):
1468 '''find path of an executable.
1468 '''find path of an executable.
1469 if name contains a path component, return it as is. otherwise,
1469 if name contains a path component, return it as is. otherwise,
1470 use normal executable search path.'''
1470 use normal executable search path.'''
1471
1471
1472 if os.sep in name or sys.platform == 'OpenVMS':
1472 if os.sep in name or sys.platform == 'OpenVMS':
1473 # don't check the executable bit. if the file isn't
1473 # don't check the executable bit. if the file isn't
1474 # executable, whoever tries to actually run it will give a
1474 # executable, whoever tries to actually run it will give a
1475 # much more useful error message.
1475 # much more useful error message.
1476 return name
1476 return name
1477 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1477 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1478
1478
1479 def mktempcopy(name, emptyok=False, createmode=None):
1479 def mktempcopy(name, emptyok=False, createmode=None):
1480 """Create a temporary file with the same contents from name
1480 """Create a temporary file with the same contents from name
1481
1481
1482 The permission bits are copied from the original file.
1482 The permission bits are copied from the original file.
1483
1483
1484 If the temporary file is going to be truncated immediately, you
1484 If the temporary file is going to be truncated immediately, you
1485 can use emptyok=True as an optimization.
1485 can use emptyok=True as an optimization.
1486
1486
1487 Returns the name of the temporary file.
1487 Returns the name of the temporary file.
1488 """
1488 """
1489 d, fn = os.path.split(name)
1489 d, fn = os.path.split(name)
1490 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1490 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1491 os.close(fd)
1491 os.close(fd)
1492 # Temporary files are created with mode 0600, which is usually not
1492 # Temporary files are created with mode 0600, which is usually not
1493 # what we want. If the original file already exists, just copy
1493 # what we want. If the original file already exists, just copy
1494 # its mode. Otherwise, manually obey umask.
1494 # its mode. Otherwise, manually obey umask.
1495 try:
1495 try:
1496 st_mode = os.lstat(name).st_mode & 0777
1496 st_mode = os.lstat(name).st_mode & 0777
1497 except OSError, inst:
1497 except OSError, inst:
1498 if inst.errno != errno.ENOENT:
1498 if inst.errno != errno.ENOENT:
1499 raise
1499 raise
1500 st_mode = createmode
1500 st_mode = createmode
1501 if st_mode is None:
1501 if st_mode is None:
1502 st_mode = ~_umask
1502 st_mode = ~_umask
1503 st_mode &= 0666
1503 st_mode &= 0666
1504 os.chmod(temp, st_mode)
1504 os.chmod(temp, st_mode)
1505 if emptyok:
1505 if emptyok:
1506 return temp
1506 return temp
1507 try:
1507 try:
1508 try:
1508 try:
1509 ifp = posixfile(name, "rb")
1509 ifp = posixfile(name, "rb")
1510 except IOError, inst:
1510 except IOError, inst:
1511 if inst.errno == errno.ENOENT:
1511 if inst.errno == errno.ENOENT:
1512 return temp
1512 return temp
1513 if not getattr(inst, 'filename', None):
1513 if not getattr(inst, 'filename', None):
1514 inst.filename = name
1514 inst.filename = name
1515 raise
1515 raise
1516 ofp = posixfile(temp, "wb")
1516 ofp = posixfile(temp, "wb")
1517 for chunk in filechunkiter(ifp):
1517 for chunk in filechunkiter(ifp):
1518 ofp.write(chunk)
1518 ofp.write(chunk)
1519 ifp.close()
1519 ifp.close()
1520 ofp.close()
1520 ofp.close()
1521 except:
1521 except:
1522 try: os.unlink(temp)
1522 try: os.unlink(temp)
1523 except: pass
1523 except: pass
1524 raise
1524 raise
1525 return temp
1525 return temp
1526
1526
1527 class atomictempfile(posixfile):
1527 class atomictempfile(posixfile):
1528 """file-like object that atomically updates a file
1528 """file-like object that atomically updates a file
1529
1529
1530 All writes will be redirected to a temporary copy of the original
1530 All writes will be redirected to a temporary copy of the original
1531 file. When rename is called, the copy is renamed to the original
1531 file. When rename is called, the copy is renamed to the original
1532 name, making the changes visible.
1532 name, making the changes visible.
1533 """
1533 """
1534 def __init__(self, name, mode, createmode):
1534 def __init__(self, name, mode, createmode):
1535 self.__name = name
1535 self.__name = name
1536 self.temp = mktempcopy(name, emptyok=('w' in mode),
1536 self.temp = mktempcopy(name, emptyok=('w' in mode),
1537 createmode=createmode)
1537 createmode=createmode)
1538 posixfile.__init__(self, self.temp, mode)
1538 posixfile.__init__(self, self.temp, mode)
1539
1539
1540 def rename(self):
1540 def rename(self):
1541 if not self.closed:
1541 if not self.closed:
1542 posixfile.close(self)
1542 posixfile.close(self)
1543 rename(self.temp, localpath(self.__name))
1543 rename(self.temp, localpath(self.__name))
1544
1544
1545 def __del__(self):
1545 def __del__(self):
1546 if not self.closed:
1546 if not self.closed:
1547 try:
1547 try:
1548 os.unlink(self.temp)
1548 os.unlink(self.temp)
1549 except: pass
1549 except: pass
1550 posixfile.close(self)
1550 posixfile.close(self)
1551
1551
1552 def makedirs(name, mode=None):
1552 def makedirs(name, mode=None):
1553 """recursive directory creation with parent mode inheritance"""
1553 """recursive directory creation with parent mode inheritance"""
1554 try:
1554 try:
1555 os.mkdir(name)
1555 os.mkdir(name)
1556 if mode is not None:
1556 if mode is not None:
1557 os.chmod(name, mode)
1557 os.chmod(name, mode)
1558 return
1558 return
1559 except OSError, err:
1559 except OSError, err:
1560 if err.errno == errno.EEXIST:
1560 if err.errno == errno.EEXIST:
1561 return
1561 return
1562 if err.errno != errno.ENOENT:
1562 if err.errno != errno.ENOENT:
1563 raise
1563 raise
1564 parent = os.path.abspath(os.path.dirname(name))
1564 parent = os.path.abspath(os.path.dirname(name))
1565 makedirs(parent, mode)
1565 makedirs(parent, mode)
1566 makedirs(name, mode)
1566 makedirs(name, mode)
1567
1567
1568 class opener(object):
1568 class opener(object):
1569 """Open files relative to a base directory
1569 """Open files relative to a base directory
1570
1570
1571 This class is used to hide the details of COW semantics and
1571 This class is used to hide the details of COW semantics and
1572 remote file access from higher level code.
1572 remote file access from higher level code.
1573 """
1573 """
1574 def __init__(self, base, audit=True):
1574 def __init__(self, base, audit=True):
1575 self.base = base
1575 self.base = base
1576 if audit:
1576 if audit:
1577 self.audit_path = path_auditor(base)
1577 self.audit_path = path_auditor(base)
1578 else:
1578 else:
1579 self.audit_path = always
1579 self.audit_path = always
1580 self.createmode = None
1580 self.createmode = None
1581
1581
1582 def __getattr__(self, name):
1582 def __getattr__(self, name):
1583 if name == '_can_symlink':
1583 if name == '_can_symlink':
1584 self._can_symlink = checklink(self.base)
1584 self._can_symlink = checklink(self.base)
1585 return self._can_symlink
1585 return self._can_symlink
1586 raise AttributeError(name)
1586 raise AttributeError(name)
1587
1587
1588 def _fixfilemode(self, name):
1588 def _fixfilemode(self, name):
1589 if self.createmode is None:
1589 if self.createmode is None:
1590 return
1590 return
1591 os.chmod(name, self.createmode & 0666)
1591 os.chmod(name, self.createmode & 0666)
1592
1592
1593 def __call__(self, path, mode="r", text=False, atomictemp=False):
1593 def __call__(self, path, mode="r", text=False, atomictemp=False):
1594 self.audit_path(path)
1594 self.audit_path(path)
1595 f = os.path.join(self.base, path)
1595 f = os.path.join(self.base, path)
1596
1596
1597 if not text and "b" not in mode:
1597 if not text and "b" not in mode:
1598 mode += "b" # for that other OS
1598 mode += "b" # for that other OS
1599
1599
1600 nlink = -1
1600 nlink = -1
1601 if mode not in ("r", "rb"):
1601 if mode not in ("r", "rb"):
1602 try:
1602 try:
1603 nlink = nlinks(f)
1603 nlink = nlinks(f)
1604 except OSError:
1604 except OSError:
1605 nlink = 0
1605 nlink = 0
1606 d = os.path.dirname(f)
1606 d = os.path.dirname(f)
1607 if not os.path.isdir(d):
1607 if not os.path.isdir(d):
1608 makedirs(d, self.createmode)
1608 makedirs(d, self.createmode)
1609 if atomictemp:
1609 if atomictemp:
1610 return atomictempfile(f, mode, self.createmode)
1610 return atomictempfile(f, mode, self.createmode)
1611 if nlink > 1:
1611 if nlink > 1:
1612 rename(mktempcopy(f), f)
1612 rename(mktempcopy(f), f)
1613 fp = posixfile(f, mode)
1613 fp = posixfile(f, mode)
1614 if nlink == 0:
1614 if nlink == 0:
1615 self._fixfilemode(f)
1615 self._fixfilemode(f)
1616 return fp
1616 return fp
1617
1617
1618 def symlink(self, src, dst):
1618 def symlink(self, src, dst):
1619 self.audit_path(dst)
1619 self.audit_path(dst)
1620 linkname = os.path.join(self.base, dst)
1620 linkname = os.path.join(self.base, dst)
1621 try:
1621 try:
1622 os.unlink(linkname)
1622 os.unlink(linkname)
1623 except OSError:
1623 except OSError:
1624 pass
1624 pass
1625
1625
1626 dirname = os.path.dirname(linkname)
1626 dirname = os.path.dirname(linkname)
1627 if not os.path.exists(dirname):
1627 if not os.path.exists(dirname):
1628 makedirs(dirname, self.createmode)
1628 makedirs(dirname, self.createmode)
1629
1629
1630 if self._can_symlink:
1630 if self._can_symlink:
1631 try:
1631 try:
1632 os.symlink(src, linkname)
1632 os.symlink(src, linkname)
1633 except OSError, err:
1633 except OSError, err:
1634 raise OSError(err.errno, _('could not symlink to %r: %s') %
1634 raise OSError(err.errno, _('could not symlink to %r: %s') %
1635 (src, err.strerror), linkname)
1635 (src, err.strerror), linkname)
1636 else:
1636 else:
1637 f = self(dst, "w")
1637 f = self(dst, "w")
1638 f.write(src)
1638 f.write(src)
1639 f.close()
1639 f.close()
1640 self._fixfilemode(dst)
1640 self._fixfilemode(dst)
1641
1641
1642 class chunkbuffer(object):
1642 class chunkbuffer(object):
1643 """Allow arbitrary sized chunks of data to be efficiently read from an
1643 """Allow arbitrary sized chunks of data to be efficiently read from an
1644 iterator over chunks of arbitrary size."""
1644 iterator over chunks of arbitrary size."""
1645
1645
1646 def __init__(self, in_iter):
1646 def __init__(self, in_iter):
1647 """in_iter is the iterator that's iterating over the input chunks.
1647 """in_iter is the iterator that's iterating over the input chunks.
1648 targetsize is how big a buffer to try to maintain."""
1648 targetsize is how big a buffer to try to maintain."""
1649 self.iter = iter(in_iter)
1649 self.iter = iter(in_iter)
1650 self.buf = ''
1650 self.buf = ''
1651 self.targetsize = 2**16
1651 self.targetsize = 2**16
1652
1652
1653 def read(self, l):
1653 def read(self, l):
1654 """Read L bytes of data from the iterator of chunks of data.
1654 """Read L bytes of data from the iterator of chunks of data.
1655 Returns less than L bytes if the iterator runs dry."""
1655 Returns less than L bytes if the iterator runs dry."""
1656 if l > len(self.buf) and self.iter:
1656 if l > len(self.buf) and self.iter:
1657 # Clamp to a multiple of self.targetsize
1657 # Clamp to a multiple of self.targetsize
1658 targetsize = max(l, self.targetsize)
1658 targetsize = max(l, self.targetsize)
1659 collector = cStringIO.StringIO()
1659 collector = cStringIO.StringIO()
1660 collector.write(self.buf)
1660 collector.write(self.buf)
1661 collected = len(self.buf)
1661 collected = len(self.buf)
1662 for chunk in self.iter:
1662 for chunk in self.iter:
1663 collector.write(chunk)
1663 collector.write(chunk)
1664 collected += len(chunk)
1664 collected += len(chunk)
1665 if collected >= targetsize:
1665 if collected >= targetsize:
1666 break
1666 break
1667 if collected < targetsize:
1667 if collected < targetsize:
1668 self.iter = False
1668 self.iter = False
1669 self.buf = collector.getvalue()
1669 self.buf = collector.getvalue()
1670 if len(self.buf) == l:
1670 if len(self.buf) == l:
1671 s, self.buf = str(self.buf), ''
1671 s, self.buf = str(self.buf), ''
1672 else:
1672 else:
1673 s, self.buf = self.buf[:l], buffer(self.buf, l)
1673 s, self.buf = self.buf[:l], buffer(self.buf, l)
1674 return s
1674 return s
1675
1675
1676 def filechunkiter(f, size=65536, limit=None):
1676 def filechunkiter(f, size=65536, limit=None):
1677 """Create a generator that produces the data in the file size
1677 """Create a generator that produces the data in the file size
1678 (default 65536) bytes at a time, up to optional limit (default is
1678 (default 65536) bytes at a time, up to optional limit (default is
1679 to read all data). Chunks may be less than size bytes if the
1679 to read all data). Chunks may be less than size bytes if the
1680 chunk is the last chunk in the file, or the file is a socket or
1680 chunk is the last chunk in the file, or the file is a socket or
1681 some other type of file that sometimes reads less data than is
1681 some other type of file that sometimes reads less data than is
1682 requested."""
1682 requested."""
1683 assert size >= 0
1683 assert size >= 0
1684 assert limit is None or limit >= 0
1684 assert limit is None or limit >= 0
1685 while True:
1685 while True:
1686 if limit is None: nbytes = size
1686 if limit is None: nbytes = size
1687 else: nbytes = min(limit, size)
1687 else: nbytes = min(limit, size)
1688 s = nbytes and f.read(nbytes)
1688 s = nbytes and f.read(nbytes)
1689 if not s: break
1689 if not s: break
1690 if limit: limit -= len(s)
1690 if limit: limit -= len(s)
1691 yield s
1691 yield s
1692
1692
1693 def makedate():
1693 def makedate():
1694 lt = time.localtime()
1694 lt = time.localtime()
1695 if lt[8] == 1 and time.daylight:
1695 if lt[8] == 1 and time.daylight:
1696 tz = time.altzone
1696 tz = time.altzone
1697 else:
1697 else:
1698 tz = time.timezone
1698 tz = time.timezone
1699 return time.mktime(lt), tz
1699 return time.mktime(lt), tz
1700
1700
1701 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1701 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1702 """represent a (unixtime, offset) tuple as a localized time.
1702 """represent a (unixtime, offset) tuple as a localized time.
1703 unixtime is seconds since the epoch, and offset is the time zone's
1703 unixtime is seconds since the epoch, and offset is the time zone's
1704 number of seconds away from UTC. if timezone is false, do not
1704 number of seconds away from UTC. if timezone is false, do not
1705 append time zone to string."""
1705 append time zone to string."""
1706 t, tz = date or makedate()
1706 t, tz = date or makedate()
1707 if "%1" in format or "%2" in format:
1707 if "%1" in format or "%2" in format:
1708 sign = (tz > 0) and "-" or "+"
1708 sign = (tz > 0) and "-" or "+"
1709 minutes = abs(tz) / 60
1709 minutes = abs(tz) / 60
1710 format = format.replace("%1", "%c%02d" % (sign, minutes / 60))
1710 format = format.replace("%1", "%c%02d" % (sign, minutes / 60))
1711 format = format.replace("%2", "%02d" % (minutes % 60))
1711 format = format.replace("%2", "%02d" % (minutes % 60))
1712 s = time.strftime(format, time.gmtime(float(t) - tz))
1712 s = time.strftime(format, time.gmtime(float(t) - tz))
1713 return s
1713 return s
1714
1714
1715 def shortdate(date=None):
1715 def shortdate(date=None):
1716 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1716 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1717 return datestr(date, format='%Y-%m-%d')
1717 return datestr(date, format='%Y-%m-%d')
1718
1718
1719 def strdate(string, format, defaults=[]):
1719 def strdate(string, format, defaults=[]):
1720 """parse a localized time string and return a (unixtime, offset) tuple.
1720 """parse a localized time string and return a (unixtime, offset) tuple.
1721 if the string cannot be parsed, ValueError is raised."""
1721 if the string cannot be parsed, ValueError is raised."""
1722 def timezone(string):
1722 def timezone(string):
1723 tz = string.split()[-1]
1723 tz = string.split()[-1]
1724 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1724 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1725 sign = (tz[0] == "+") and 1 or -1
1725 sign = (tz[0] == "+") and 1 or -1
1726 hours = int(tz[1:3])
1726 hours = int(tz[1:3])
1727 minutes = int(tz[3:5])
1727 minutes = int(tz[3:5])
1728 return -sign * (hours * 60 + minutes) * 60
1728 return -sign * (hours * 60 + minutes) * 60
1729 if tz == "GMT" or tz == "UTC":
1729 if tz == "GMT" or tz == "UTC":
1730 return 0
1730 return 0
1731 return None
1731 return None
1732
1732
1733 # NOTE: unixtime = localunixtime + offset
1733 # NOTE: unixtime = localunixtime + offset
1734 offset, date = timezone(string), string
1734 offset, date = timezone(string), string
1735 if offset != None:
1735 if offset != None:
1736 date = " ".join(string.split()[:-1])
1736 date = " ".join(string.split()[:-1])
1737
1737
1738 # add missing elements from defaults
1738 # add missing elements from defaults
1739 for part in defaults:
1739 for part in defaults:
1740 found = [True for p in part if ("%"+p) in format]
1740 found = [True for p in part if ("%"+p) in format]
1741 if not found:
1741 if not found:
1742 date += "@" + defaults[part]
1742 date += "@" + defaults[part]
1743 format += "@%" + part[0]
1743 format += "@%" + part[0]
1744
1744
1745 timetuple = time.strptime(date, format)
1745 timetuple = time.strptime(date, format)
1746 localunixtime = int(calendar.timegm(timetuple))
1746 localunixtime = int(calendar.timegm(timetuple))
1747 if offset is None:
1747 if offset is None:
1748 # local timezone
1748 # local timezone
1749 unixtime = int(time.mktime(timetuple))
1749 unixtime = int(time.mktime(timetuple))
1750 offset = unixtime - localunixtime
1750 offset = unixtime - localunixtime
1751 else:
1751 else:
1752 unixtime = localunixtime + offset
1752 unixtime = localunixtime + offset
1753 return unixtime, offset
1753 return unixtime, offset
1754
1754
1755 def parsedate(date, formats=None, defaults=None):
1755 def parsedate(date, formats=None, defaults=None):
1756 """parse a localized date/time string and return a (unixtime, offset) tuple.
1756 """parse a localized date/time string and return a (unixtime, offset) tuple.
1757
1757
1758 The date may be a "unixtime offset" string or in one of the specified
1758 The date may be a "unixtime offset" string or in one of the specified
1759 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1759 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1760 """
1760 """
1761 if not date:
1761 if not date:
1762 return 0, 0
1762 return 0, 0
1763 if isinstance(date, tuple) and len(date) == 2:
1763 if isinstance(date, tuple) and len(date) == 2:
1764 return date
1764 return date
1765 if not formats:
1765 if not formats:
1766 formats = defaultdateformats
1766 formats = defaultdateformats
1767 date = date.strip()
1767 date = date.strip()
1768 try:
1768 try:
1769 when, offset = map(int, date.split(' '))
1769 when, offset = map(int, date.split(' '))
1770 except ValueError:
1770 except ValueError:
1771 # fill out defaults
1771 # fill out defaults
1772 if not defaults:
1772 if not defaults:
1773 defaults = {}
1773 defaults = {}
1774 now = makedate()
1774 now = makedate()
1775 for part in "d mb yY HI M S".split():
1775 for part in "d mb yY HI M S".split():
1776 if part not in defaults:
1776 if part not in defaults:
1777 if part[0] in "HMS":
1777 if part[0] in "HMS":
1778 defaults[part] = "00"
1778 defaults[part] = "00"
1779 else:
1779 else:
1780 defaults[part] = datestr(now, "%" + part[0])
1780 defaults[part] = datestr(now, "%" + part[0])
1781
1781
1782 for format in formats:
1782 for format in formats:
1783 try:
1783 try:
1784 when, offset = strdate(date, format, defaults)
1784 when, offset = strdate(date, format, defaults)
1785 except (ValueError, OverflowError):
1785 except (ValueError, OverflowError):
1786 pass
1786 pass
1787 else:
1787 else:
1788 break
1788 break
1789 else:
1789 else:
1790 raise Abort(_('invalid date: %r ') % date)
1790 raise Abort(_('invalid date: %r ') % date)
1791 # validate explicit (probably user-specified) date and
1791 # validate explicit (probably user-specified) date and
1792 # time zone offset. values must fit in signed 32 bits for
1792 # time zone offset. values must fit in signed 32 bits for
1793 # current 32-bit linux runtimes. timezones go from UTC-12
1793 # current 32-bit linux runtimes. timezones go from UTC-12
1794 # to UTC+14
1794 # to UTC+14
1795 if abs(when) > 0x7fffffff:
1795 if abs(when) > 0x7fffffff:
1796 raise Abort(_('date exceeds 32 bits: %d') % when)
1796 raise Abort(_('date exceeds 32 bits: %d') % when)
1797 if offset < -50400 or offset > 43200:
1797 if offset < -50400 or offset > 43200:
1798 raise Abort(_('impossible time zone offset: %d') % offset)
1798 raise Abort(_('impossible time zone offset: %d') % offset)
1799 return when, offset
1799 return when, offset
1800
1800
1801 def matchdate(date):
1801 def matchdate(date):
1802 """Return a function that matches a given date match specifier
1802 """Return a function that matches a given date match specifier
1803
1803
1804 Formats include:
1804 Formats include:
1805
1805
1806 '{date}' match a given date to the accuracy provided
1806 '{date}' match a given date to the accuracy provided
1807
1807
1808 '<{date}' on or before a given date
1808 '<{date}' on or before a given date
1809
1809
1810 '>{date}' on or after a given date
1810 '>{date}' on or after a given date
1811
1811
1812 """
1812 """
1813
1813
1814 def lower(date):
1814 def lower(date):
1815 d = dict(mb="1", d="1")
1815 d = dict(mb="1", d="1")
1816 return parsedate(date, extendeddateformats, d)[0]
1816 return parsedate(date, extendeddateformats, d)[0]
1817
1817
1818 def upper(date):
1818 def upper(date):
1819 d = dict(mb="12", HI="23", M="59", S="59")
1819 d = dict(mb="12", HI="23", M="59", S="59")
1820 for days in "31 30 29".split():
1820 for days in "31 30 29".split():
1821 try:
1821 try:
1822 d["d"] = days
1822 d["d"] = days
1823 return parsedate(date, extendeddateformats, d)[0]
1823 return parsedate(date, extendeddateformats, d)[0]
1824 except:
1824 except:
1825 pass
1825 pass
1826 d["d"] = "28"
1826 d["d"] = "28"
1827 return parsedate(date, extendeddateformats, d)[0]
1827 return parsedate(date, extendeddateformats, d)[0]
1828
1828
1829 if date[0] == "<":
1829 if date[0] == "<":
1830 when = upper(date[1:])
1830 when = upper(date[1:])
1831 return lambda x: x <= when
1831 return lambda x: x <= when
1832 elif date[0] == ">":
1832 elif date[0] == ">":
1833 when = lower(date[1:])
1833 when = lower(date[1:])
1834 return lambda x: x >= when
1834 return lambda x: x >= when
1835 elif date[0] == "-":
1835 elif date[0] == "-":
1836 try:
1836 try:
1837 days = int(date[1:])
1837 days = int(date[1:])
1838 except ValueError:
1838 except ValueError:
1839 raise Abort(_("invalid day spec: %s") % date[1:])
1839 raise Abort(_("invalid day spec: %s") % date[1:])
1840 when = makedate()[0] - days * 3600 * 24
1840 when = makedate()[0] - days * 3600 * 24
1841 return lambda x: x >= when
1841 return lambda x: x >= when
1842 elif " to " in date:
1842 elif " to " in date:
1843 a, b = date.split(" to ")
1843 a, b = date.split(" to ")
1844 start, stop = lower(a), upper(b)
1844 start, stop = lower(a), upper(b)
1845 return lambda x: x >= start and x <= stop
1845 return lambda x: x >= start and x <= stop
1846 else:
1846 else:
1847 start, stop = lower(date), upper(date)
1847 start, stop = lower(date), upper(date)
1848 return lambda x: x >= start and x <= stop
1848 return lambda x: x >= start and x <= stop
1849
1849
1850 def shortuser(user):
1850 def shortuser(user):
1851 """Return a short representation of a user name or email address."""
1851 """Return a short representation of a user name or email address."""
1852 f = user.find('@')
1852 f = user.find('@')
1853 if f >= 0:
1853 if f >= 0:
1854 user = user[:f]
1854 user = user[:f]
1855 f = user.find('<')
1855 f = user.find('<')
1856 if f >= 0:
1856 if f >= 0:
1857 user = user[f+1:]
1857 user = user[f+1:]
1858 f = user.find(' ')
1858 f = user.find(' ')
1859 if f >= 0:
1859 if f >= 0:
1860 user = user[:f]
1860 user = user[:f]
1861 f = user.find('.')
1861 f = user.find('.')
1862 if f >= 0:
1862 if f >= 0:
1863 user = user[:f]
1863 user = user[:f]
1864 return user
1864 return user
1865
1865
1866 def email(author):
1866 def email(author):
1867 '''get email of author.'''
1867 '''get email of author.'''
1868 r = author.find('>')
1868 r = author.find('>')
1869 if r == -1: r = None
1869 if r == -1: r = None
1870 return author[author.find('<')+1:r]
1870 return author[author.find('<')+1:r]
1871
1871
1872 def ellipsis(text, maxlength=400):
1872 def ellipsis(text, maxlength=400):
1873 """Trim string to at most maxlength (default: 400) characters."""
1873 """Trim string to at most maxlength (default: 400) characters."""
1874 if len(text) <= maxlength:
1874 if len(text) <= maxlength:
1875 return text
1875 return text
1876 else:
1876 else:
1877 return "%s..." % (text[:maxlength-3])
1877 return "%s..." % (text[:maxlength-3])
1878
1878
1879 def walkrepos(path, followsym=False, seen_dirs=None):
1879 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
1880 '''yield every hg repository under path, recursively.'''
1880 '''yield every hg repository under path, recursively.'''
1881 def errhandler(err):
1881 def errhandler(err):
1882 if err.filename == path:
1882 if err.filename == path:
1883 raise err
1883 raise err
1884 if followsym and hasattr(os.path, 'samestat'):
1884 if followsym and hasattr(os.path, 'samestat'):
1885 def _add_dir_if_not_there(dirlst, dirname):
1885 def _add_dir_if_not_there(dirlst, dirname):
1886 match = False
1886 match = False
1887 samestat = os.path.samestat
1887 samestat = os.path.samestat
1888 dirstat = os.stat(dirname)
1888 dirstat = os.stat(dirname)
1889 for lstdirstat in dirlst:
1889 for lstdirstat in dirlst:
1890 if samestat(dirstat, lstdirstat):
1890 if samestat(dirstat, lstdirstat):
1891 match = True
1891 match = True
1892 break
1892 break
1893 if not match:
1893 if not match:
1894 dirlst.append(dirstat)
1894 dirlst.append(dirstat)
1895 return not match
1895 return not match
1896 else:
1896 else:
1897 followsym = False
1897 followsym = False
1898
1898
1899 if (seen_dirs is None) and followsym:
1899 if (seen_dirs is None) and followsym:
1900 seen_dirs = []
1900 seen_dirs = []
1901 _add_dir_if_not_there(seen_dirs, path)
1901 _add_dir_if_not_there(seen_dirs, path)
1902 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
1902 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
1903 if '.hg' in dirs:
1903 if '.hg' in dirs:
1904 dirs[:] = [] # don't descend further
1905 yield root # found a repository
1904 yield root # found a repository
1906 qroot = os.path.join(root, '.hg', 'patches')
1905 if recurse:
1907 if os.path.isdir(os.path.join(qroot, '.hg')):
1906 # avoid recursing inside the .hg directory
1908 yield qroot # we have a patch queue repo here
1907 # the mq repository is added in any case
1908 dirs.remove('.hg')
1909 qroot = os.path.join(root, '.hg', 'patches')
1910 if os.path.isdir(os.path.join(qroot, '.hg')):
1911 yield qroot # we have a patch queue repo here
1912 else:
1913 dirs[:] = [] # don't descend further
1909 elif followsym:
1914 elif followsym:
1910 newdirs = []
1915 newdirs = []
1911 for d in dirs:
1916 for d in dirs:
1912 fname = os.path.join(root, d)
1917 fname = os.path.join(root, d)
1913 if _add_dir_if_not_there(seen_dirs, fname):
1918 if _add_dir_if_not_there(seen_dirs, fname):
1914 if os.path.islink(fname):
1919 if os.path.islink(fname):
1915 for hgname in walkrepos(fname, True, seen_dirs):
1920 for hgname in walkrepos(fname, True, seen_dirs):
1916 yield hgname
1921 yield hgname
1917 else:
1922 else:
1918 newdirs.append(d)
1923 newdirs.append(d)
1919 dirs[:] = newdirs
1924 dirs[:] = newdirs
1920
1925
1921 _rcpath = None
1926 _rcpath = None
1922
1927
1923 def os_rcpath():
1928 def os_rcpath():
1924 '''return default os-specific hgrc search path'''
1929 '''return default os-specific hgrc search path'''
1925 path = system_rcpath()
1930 path = system_rcpath()
1926 path.extend(user_rcpath())
1931 path.extend(user_rcpath())
1927 path = [os.path.normpath(f) for f in path]
1932 path = [os.path.normpath(f) for f in path]
1928 return path
1933 return path
1929
1934
1930 def rcpath():
1935 def rcpath():
1931 '''return hgrc search path. if env var HGRCPATH is set, use it.
1936 '''return hgrc search path. if env var HGRCPATH is set, use it.
1932 for each item in path, if directory, use files ending in .rc,
1937 for each item in path, if directory, use files ending in .rc,
1933 else use item.
1938 else use item.
1934 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1939 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1935 if no HGRCPATH, use default os-specific path.'''
1940 if no HGRCPATH, use default os-specific path.'''
1936 global _rcpath
1941 global _rcpath
1937 if _rcpath is None:
1942 if _rcpath is None:
1938 if 'HGRCPATH' in os.environ:
1943 if 'HGRCPATH' in os.environ:
1939 _rcpath = []
1944 _rcpath = []
1940 for p in os.environ['HGRCPATH'].split(os.pathsep):
1945 for p in os.environ['HGRCPATH'].split(os.pathsep):
1941 if not p: continue
1946 if not p: continue
1942 if os.path.isdir(p):
1947 if os.path.isdir(p):
1943 for f, kind in osutil.listdir(p):
1948 for f, kind in osutil.listdir(p):
1944 if f.endswith('.rc'):
1949 if f.endswith('.rc'):
1945 _rcpath.append(os.path.join(p, f))
1950 _rcpath.append(os.path.join(p, f))
1946 else:
1951 else:
1947 _rcpath.append(p)
1952 _rcpath.append(p)
1948 else:
1953 else:
1949 _rcpath = os_rcpath()
1954 _rcpath = os_rcpath()
1950 return _rcpath
1955 return _rcpath
1951
1956
1952 def bytecount(nbytes):
1957 def bytecount(nbytes):
1953 '''return byte count formatted as readable string, with units'''
1958 '''return byte count formatted as readable string, with units'''
1954
1959
1955 units = (
1960 units = (
1956 (100, 1<<30, _('%.0f GB')),
1961 (100, 1<<30, _('%.0f GB')),
1957 (10, 1<<30, _('%.1f GB')),
1962 (10, 1<<30, _('%.1f GB')),
1958 (1, 1<<30, _('%.2f GB')),
1963 (1, 1<<30, _('%.2f GB')),
1959 (100, 1<<20, _('%.0f MB')),
1964 (100, 1<<20, _('%.0f MB')),
1960 (10, 1<<20, _('%.1f MB')),
1965 (10, 1<<20, _('%.1f MB')),
1961 (1, 1<<20, _('%.2f MB')),
1966 (1, 1<<20, _('%.2f MB')),
1962 (100, 1<<10, _('%.0f KB')),
1967 (100, 1<<10, _('%.0f KB')),
1963 (10, 1<<10, _('%.1f KB')),
1968 (10, 1<<10, _('%.1f KB')),
1964 (1, 1<<10, _('%.2f KB')),
1969 (1, 1<<10, _('%.2f KB')),
1965 (1, 1, _('%.0f bytes')),
1970 (1, 1, _('%.0f bytes')),
1966 )
1971 )
1967
1972
1968 for multiplier, divisor, format in units:
1973 for multiplier, divisor, format in units:
1969 if nbytes >= divisor * multiplier:
1974 if nbytes >= divisor * multiplier:
1970 return format % (nbytes / float(divisor))
1975 return format % (nbytes / float(divisor))
1971 return units[-1][2] % nbytes
1976 return units[-1][2] % nbytes
1972
1977
1973 def drop_scheme(scheme, path):
1978 def drop_scheme(scheme, path):
1974 sc = scheme + ':'
1979 sc = scheme + ':'
1975 if path.startswith(sc):
1980 if path.startswith(sc):
1976 path = path[len(sc):]
1981 path = path[len(sc):]
1977 if path.startswith('//'):
1982 if path.startswith('//'):
1978 path = path[2:]
1983 path = path[2:]
1979 return path
1984 return path
1980
1985
1981 def uirepr(s):
1986 def uirepr(s):
1982 # Avoid double backslash in Windows path repr()
1987 # Avoid double backslash in Windows path repr()
1983 return repr(s).replace('\\\\', '\\')
1988 return repr(s).replace('\\\\', '\\')
@@ -1,88 +1,102 b''
1 #!/bin/sh
1 #!/bin/sh
2 # Tests some basic hgwebdir functionality. Tests setting up paths and
2 # Tests some basic hgwebdir functionality. Tests setting up paths and
3 # collection, different forms of 404s and the subdirectory support.
3 # collection, different forms of 404s and the subdirectory support.
4
4
5 mkdir webdir
5 mkdir webdir
6 cd webdir
6 cd webdir
7
7
8 hg init a
8 hg init a
9 echo a > a/a
9 echo a > a/a
10 hg --cwd a ci -Ama -d'1 0'
10 hg --cwd a ci -Ama -d'1 0'
11 # create a mercurial queue repository
12 hg --cwd a qinit --config extensions.hgext.mq= -c
11
13
12 hg init b
14 hg init b
13 echo b > b/b
15 echo b > b/b
14 hg --cwd b ci -Amb -d'2 0'
16 hg --cwd b ci -Amb -d'2 0'
15
17
18 # create a nested repository
19 cd b
20 hg init d
21 echo d > d/d
22 hg --cwd d ci -Amd -d'3 0'
23 cd ..
24
16 hg init c
25 hg init c
17 echo c > c/c
26 echo c > c/c
18 hg --cwd c ci -Amc -d'3 0'
27 hg --cwd c ci -Amc -d'3 0'
28
19 root=`pwd`
29 root=`pwd`
20
21 cd ..
30 cd ..
22
31
23 cat > paths.conf <<EOF
32 cat > paths.conf <<EOF
24 [paths]
33 [paths]
25 a=$root/a
34 a=$root/a
26 b=$root/b
35 b=$root/b
27 EOF
36 EOF
28
37
29 hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
38 hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
30 -A access-paths.log -E error-paths-1.log
39 -A access-paths.log -E error-paths-1.log
31 cat hg.pid >> $DAEMON_PIDS
40 cat hg.pid >> $DAEMON_PIDS
32
41
33 echo % should give a 404 - file does not exist
42 echo % should give a 404 - file does not exist
34 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
43 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
35
44
36 echo % should succeed
45 echo % should succeed
37 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
46 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
38 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
47 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
39 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
48 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
40
49
41 echo % should give a 404 - repo is not published
50 echo % should give a 404 - repo is not published
42 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
51 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
43
52
44 cat > paths.conf <<EOF
53 cat > paths.conf <<EOF
45 [paths]
54 [paths]
46 t/a/=$root/a
55 t/a/=$root/a
47 b=$root/b
56 b=$root/b
48 coll=$root/*
57 coll=$root/*
58 rcoll=$root/**
49 EOF
59 EOF
50
60
51 hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
61 hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf paths.conf \
52 -A access-paths.log -E error-paths-2.log
62 -A access-paths.log -E error-paths-2.log
53 cat hg.pid >> $DAEMON_PIDS
63 cat hg.pid >> $DAEMON_PIDS
54
64
55 echo % should succeed, slashy names
65 echo % should succeed, slashy names
56 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
66 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
57 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
67 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t?style=raw'
58 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
68 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/?style=raw'
59 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \
69 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a?style=atom' \
60 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
70 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
61 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \
71 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/?style=atom' \
62 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
72 | sed "s/http:\/\/[^/]*\//http:\/\/127.0.0.1\//"
63 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
73 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/t/a/file/tip/a?style=raw'
64 # Test [paths] '*' extension
74 # Test [paths] '*' extension
65 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/?style=raw'
75 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/?style=raw'
66 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/a/file/tip/a?style=raw'
76 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/coll/a/file/tip/a?style=raw'
77 #test [paths] '**' extension
78 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/?style=raw'
79 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/rcoll/b/d/file/tip/d?style=raw'
80
67
81
68 cat > collections.conf <<EOF
82 cat > collections.conf <<EOF
69 [collections]
83 [collections]
70 $root=$root
84 $root=$root
71 EOF
85 EOF
72
86
73 hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \
87 hg serve -p $HGPORT2 -d --pid-file=hg.pid --webdir-conf collections.conf \
74 -A access-collections.log -E error-collections.log
88 -A access-collections.log -E error-collections.log
75 cat hg.pid >> $DAEMON_PIDS
89 cat hg.pid >> $DAEMON_PIDS
76
90
77 echo % should succeed
91 echo % collections: should succeed
78 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
92 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/?style=raw'
79 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
93 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/a/file/tip/a?style=raw'
80 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
94 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/b/file/tip/b?style=raw'
81 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
95 "$TESTDIR/get-with-headers.py" localhost:$HGPORT2 '/c/file/tip/c?style=raw'
82
96
83 echo % paths errors 1
97 echo % paths errors 1
84 cat error-paths-1.log
98 cat error-paths-1.log
85 echo % paths errors 2
99 echo % paths errors 2
86 cat error-paths-2.log
100 cat error-paths-2.log
87 echo % collections errors
101 echo % collections errors
88 cat error-collections.log
102 cat error-collections.log
@@ -1,137 +1,155 b''
1 adding a
1 adding a
2 adding b
2 adding b
3 adding d
3 adding c
4 adding c
4 % should give a 404 - file does not exist
5 % should give a 404 - file does not exist
5 404 Not Found
6 404 Not Found
6
7
7
8
8 error: bork@8580ff50825a: not found in manifest
9 error: bork@8580ff50825a: not found in manifest
9 % should succeed
10 % should succeed
10 200 Script output follows
11 200 Script output follows
11
12
12
13
13 /a/
14 /a/
14 /b/
15 /b/
15
16
16 200 Script output follows
17 200 Script output follows
17
18
18 a
19 a
19 200 Script output follows
20 200 Script output follows
20
21
21 b
22 b
22 % should give a 404 - repo is not published
23 % should give a 404 - repo is not published
23 404 Not Found
24 404 Not Found
24
25
25
26
26 error: repository c not found
27 error: repository c not found
27 % should succeed, slashy names
28 % should succeed, slashy names
28 200 Script output follows
29 200 Script output follows
29
30
30
31
31 /b/
32 /b/
32 /coll/a/
33 /coll/a/
33 /coll/b/
34 /coll/b/
34 /coll/c/
35 /coll/c/
36 /rcoll/a/
37 /rcoll/a/.hg/patches/
38 /rcoll/b/
39 /rcoll/b/d/
40 /rcoll/c/
35 /t/a/
41 /t/a/
36
42
37 200 Script output follows
43 200 Script output follows
38
44
39
45
40 /t/a/
46 /t/a/
41
47
42 200 Script output follows
48 200 Script output follows
43
49
44
50
45 /t/a/
51 /t/a/
46
52
47 200 Script output follows
53 200 Script output follows
48
54
49 <?xml version="1.0" encoding="ascii"?>
55 <?xml version="1.0" encoding="ascii"?>
50 <feed xmlns="http://127.0.0.1/2005/Atom">
56 <feed xmlns="http://127.0.0.1/2005/Atom">
51 <!-- Changelog -->
57 <!-- Changelog -->
52 <id>http://127.0.0.1/t/a/</id>
58 <id>http://127.0.0.1/t/a/</id>
53 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
59 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
54 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
60 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
55 <title>t/a Changelog</title>
61 <title>t/a Changelog</title>
56 <updated>1970-01-01T00:00:01+00:00</updated>
62 <updated>1970-01-01T00:00:01+00:00</updated>
57
63
58 <entry>
64 <entry>
59 <title>a</title>
65 <title>a</title>
60 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
66 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
61 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
67 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
62 <author>
68 <author>
63 <name>test</name>
69 <name>test</name>
64 <email>&#116;&#101;&#115;&#116;</email>
70 <email>&#116;&#101;&#115;&#116;</email>
65 </author>
71 </author>
66 <updated>1970-01-01T00:00:01+00:00</updated>
72 <updated>1970-01-01T00:00:01+00:00</updated>
67 <published>1970-01-01T00:00:01+00:00</published>
73 <published>1970-01-01T00:00:01+00:00</published>
68 <content type="xhtml">
74 <content type="xhtml">
69 <div xmlns="http://127.0.0.1/1999/xhtml">
75 <div xmlns="http://127.0.0.1/1999/xhtml">
70 <pre xml:space="preserve">a</pre>
76 <pre xml:space="preserve">a</pre>
71 </div>
77 </div>
72 </content>
78 </content>
73 </entry>
79 </entry>
74
80
75 </feed>
81 </feed>
76 200 Script output follows
82 200 Script output follows
77
83
78 <?xml version="1.0" encoding="ascii"?>
84 <?xml version="1.0" encoding="ascii"?>
79 <feed xmlns="http://127.0.0.1/2005/Atom">
85 <feed xmlns="http://127.0.0.1/2005/Atom">
80 <!-- Changelog -->
86 <!-- Changelog -->
81 <id>http://127.0.0.1/t/a/</id>
87 <id>http://127.0.0.1/t/a/</id>
82 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
88 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
83 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
89 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
84 <title>t/a Changelog</title>
90 <title>t/a Changelog</title>
85 <updated>1970-01-01T00:00:01+00:00</updated>
91 <updated>1970-01-01T00:00:01+00:00</updated>
86
92
87 <entry>
93 <entry>
88 <title>a</title>
94 <title>a</title>
89 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
95 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
90 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
96 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
91 <author>
97 <author>
92 <name>test</name>
98 <name>test</name>
93 <email>&#116;&#101;&#115;&#116;</email>
99 <email>&#116;&#101;&#115;&#116;</email>
94 </author>
100 </author>
95 <updated>1970-01-01T00:00:01+00:00</updated>
101 <updated>1970-01-01T00:00:01+00:00</updated>
96 <published>1970-01-01T00:00:01+00:00</published>
102 <published>1970-01-01T00:00:01+00:00</published>
97 <content type="xhtml">
103 <content type="xhtml">
98 <div xmlns="http://127.0.0.1/1999/xhtml">
104 <div xmlns="http://127.0.0.1/1999/xhtml">
99 <pre xml:space="preserve">a</pre>
105 <pre xml:space="preserve">a</pre>
100 </div>
106 </div>
101 </content>
107 </content>
102 </entry>
108 </entry>
103
109
104 </feed>
110 </feed>
105 200 Script output follows
111 200 Script output follows
106
112
107 a
113 a
108 200 Script output follows
114 200 Script output follows
109
115
110
116
111 /coll/a/
117 /coll/a/
112 /coll/b/
118 /coll/b/
113 /coll/c/
119 /coll/c/
114
120
115 200 Script output follows
121 200 Script output follows
116
122
117 a
123 a
118 % should succeed
124 200 Script output follows
125
126
127 /rcoll/a/
128 /rcoll/a/.hg/patches/
129 /rcoll/b/
130 /rcoll/b/d/
131 /rcoll/c/
132
133 200 Script output follows
134
135 d
136 % collections: should succeed
119 200 Script output follows
137 200 Script output follows
120
138
121
139
122 /a/
140 /a/
123 /b/
141 /b/
124 /c/
142 /c/
125
143
126 200 Script output follows
144 200 Script output follows
127
145
128 a
146 a
129 200 Script output follows
147 200 Script output follows
130
148
131 b
149 b
132 200 Script output follows
150 200 Script output follows
133
151
134 c
152 c
135 % paths errors 1
153 % paths errors 1
136 % paths errors 2
154 % paths errors 2
137 % collections errors
155 % collections errors
General Comments 0
You need to be logged in to leave comments. Login now