##// END OF EJS Templates
windows: force specified path to be audited in localpath form...
FUJIWARA Katsunori -
r15721:4751d513 stable
parent child Browse files
Show More
@@ -1,813 +1,814 b''
1 # scmutil.py - Mercurial core utility functions
1 # scmutil.py - Mercurial core utility functions
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import util, error, osutil, revset, similar, encoding
9 import util, error, osutil, revset, similar, encoding
10 import match as matchmod
10 import match as matchmod
11 import os, errno, re, stat, sys, glob
11 import os, errno, re, stat, sys, glob
12
12
13 def checkfilename(f):
13 def checkfilename(f):
14 '''Check that the filename f is an acceptable filename for a tracked file'''
14 '''Check that the filename f is an acceptable filename for a tracked file'''
15 if '\r' in f or '\n' in f:
15 if '\r' in f or '\n' in f:
16 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
16 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
17
17
18 def checkportable(ui, f):
18 def checkportable(ui, f):
19 '''Check if filename f is portable and warn or abort depending on config'''
19 '''Check if filename f is portable and warn or abort depending on config'''
20 checkfilename(f)
20 checkfilename(f)
21 abort, warn = checkportabilityalert(ui)
21 abort, warn = checkportabilityalert(ui)
22 if abort or warn:
22 if abort or warn:
23 msg = util.checkwinfilename(f)
23 msg = util.checkwinfilename(f)
24 if msg:
24 if msg:
25 msg = "%s: %r" % (msg, f)
25 msg = "%s: %r" % (msg, f)
26 if abort:
26 if abort:
27 raise util.Abort(msg)
27 raise util.Abort(msg)
28 ui.warn(_("warning: %s\n") % msg)
28 ui.warn(_("warning: %s\n") % msg)
29
29
30 def checkportabilityalert(ui):
30 def checkportabilityalert(ui):
31 '''check if the user's config requests nothing, a warning, or abort for
31 '''check if the user's config requests nothing, a warning, or abort for
32 non-portable filenames'''
32 non-portable filenames'''
33 val = ui.config('ui', 'portablefilenames', 'warn')
33 val = ui.config('ui', 'portablefilenames', 'warn')
34 lval = val.lower()
34 lval = val.lower()
35 bval = util.parsebool(val)
35 bval = util.parsebool(val)
36 abort = os.name == 'nt' or lval == 'abort'
36 abort = os.name == 'nt' or lval == 'abort'
37 warn = bval or lval == 'warn'
37 warn = bval or lval == 'warn'
38 if bval is None and not (warn or abort or lval == 'ignore'):
38 if bval is None and not (warn or abort or lval == 'ignore'):
39 raise error.ConfigError(
39 raise error.ConfigError(
40 _("ui.portablefilenames value is invalid ('%s')") % val)
40 _("ui.portablefilenames value is invalid ('%s')") % val)
41 return abort, warn
41 return abort, warn
42
42
43 class casecollisionauditor(object):
43 class casecollisionauditor(object):
44 def __init__(self, ui, abort, existingiter):
44 def __init__(self, ui, abort, existingiter):
45 self._ui = ui
45 self._ui = ui
46 self._abort = abort
46 self._abort = abort
47 self._map = {}
47 self._map = {}
48 for f in existingiter:
48 for f in existingiter:
49 self._map[encoding.lower(f)] = f
49 self._map[encoding.lower(f)] = f
50
50
51 def __call__(self, f):
51 def __call__(self, f):
52 fl = encoding.lower(f)
52 fl = encoding.lower(f)
53 map = self._map
53 map = self._map
54 if fl in map and map[fl] != f:
54 if fl in map and map[fl] != f:
55 msg = _('possible case-folding collision for %s') % f
55 msg = _('possible case-folding collision for %s') % f
56 if self._abort:
56 if self._abort:
57 raise util.Abort(msg)
57 raise util.Abort(msg)
58 self._ui.warn(_("warning: %s\n") % msg)
58 self._ui.warn(_("warning: %s\n") % msg)
59 map[fl] = f
59 map[fl] = f
60
60
61 class pathauditor(object):
61 class pathauditor(object):
62 '''ensure that a filesystem path contains no banned components.
62 '''ensure that a filesystem path contains no banned components.
63 the following properties of a path are checked:
63 the following properties of a path are checked:
64
64
65 - ends with a directory separator
65 - ends with a directory separator
66 - under top-level .hg
66 - under top-level .hg
67 - starts at the root of a windows drive
67 - starts at the root of a windows drive
68 - contains ".."
68 - contains ".."
69 - traverses a symlink (e.g. a/symlink_here/b)
69 - traverses a symlink (e.g. a/symlink_here/b)
70 - inside a nested repository (a callback can be used to approve
70 - inside a nested repository (a callback can be used to approve
71 some nested repositories, e.g., subrepositories)
71 some nested repositories, e.g., subrepositories)
72 '''
72 '''
73
73
74 def __init__(self, root, callback=None):
74 def __init__(self, root, callback=None):
75 self.audited = set()
75 self.audited = set()
76 self.auditeddir = set()
76 self.auditeddir = set()
77 self.root = root
77 self.root = root
78 self.callback = callback
78 self.callback = callback
79 if os.path.lexists(root) and not util.checkcase(root):
79 if os.path.lexists(root) and not util.checkcase(root):
80 self.normcase = util.normcase
80 self.normcase = util.normcase
81 else:
81 else:
82 self.normcase = lambda x: x
82 self.normcase = lambda x: x
83
83
84 def __call__(self, path):
84 def __call__(self, path):
85 '''Check the relative path.
85 '''Check the relative path.
86 path may contain a pattern (e.g. foodir/**.txt)'''
86 path may contain a pattern (e.g. foodir/**.txt)'''
87
87
88 path = util.localpath(path)
88 normpath = self.normcase(path)
89 normpath = self.normcase(path)
89 if normpath in self.audited:
90 if normpath in self.audited:
90 return
91 return
91 # AIX ignores "/" at end of path, others raise EISDIR.
92 # AIX ignores "/" at end of path, others raise EISDIR.
92 if util.endswithsep(path):
93 if util.endswithsep(path):
93 raise util.Abort(_("path ends in directory separator: %s") % path)
94 raise util.Abort(_("path ends in directory separator: %s") % path)
94 parts = util.splitpath(path)
95 parts = util.splitpath(path)
95 if (os.path.splitdrive(path)[0]
96 if (os.path.splitdrive(path)[0]
96 or parts[0].lower() in ('.hg', '.hg.', '')
97 or parts[0].lower() in ('.hg', '.hg.', '')
97 or os.pardir in parts):
98 or os.pardir in parts):
98 raise util.Abort(_("path contains illegal component: %s") % path)
99 raise util.Abort(_("path contains illegal component: %s") % path)
99 if '.hg' in path.lower():
100 if '.hg' in path.lower():
100 lparts = [p.lower() for p in parts]
101 lparts = [p.lower() for p in parts]
101 for p in '.hg', '.hg.':
102 for p in '.hg', '.hg.':
102 if p in lparts[1:]:
103 if p in lparts[1:]:
103 pos = lparts.index(p)
104 pos = lparts.index(p)
104 base = os.path.join(*parts[:pos])
105 base = os.path.join(*parts[:pos])
105 raise util.Abort(_('path %r is inside nested repo %r')
106 raise util.Abort(_('path %r is inside nested repo %r')
106 % (path, base))
107 % (path, base))
107
108
108 normparts = util.splitpath(normpath)
109 normparts = util.splitpath(normpath)
109 assert len(parts) == len(normparts)
110 assert len(parts) == len(normparts)
110
111
111 parts.pop()
112 parts.pop()
112 normparts.pop()
113 normparts.pop()
113 prefixes = []
114 prefixes = []
114 while parts:
115 while parts:
115 prefix = os.sep.join(parts)
116 prefix = os.sep.join(parts)
116 normprefix = os.sep.join(normparts)
117 normprefix = os.sep.join(normparts)
117 if normprefix in self.auditeddir:
118 if normprefix in self.auditeddir:
118 break
119 break
119 curpath = os.path.join(self.root, prefix)
120 curpath = os.path.join(self.root, prefix)
120 try:
121 try:
121 st = os.lstat(curpath)
122 st = os.lstat(curpath)
122 except OSError, err:
123 except OSError, err:
123 # EINVAL can be raised as invalid path syntax under win32.
124 # EINVAL can be raised as invalid path syntax under win32.
124 # They must be ignored for patterns can be checked too.
125 # They must be ignored for patterns can be checked too.
125 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
126 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
126 raise
127 raise
127 else:
128 else:
128 if stat.S_ISLNK(st.st_mode):
129 if stat.S_ISLNK(st.st_mode):
129 raise util.Abort(
130 raise util.Abort(
130 _('path %r traverses symbolic link %r')
131 _('path %r traverses symbolic link %r')
131 % (path, prefix))
132 % (path, prefix))
132 elif (stat.S_ISDIR(st.st_mode) and
133 elif (stat.S_ISDIR(st.st_mode) and
133 os.path.isdir(os.path.join(curpath, '.hg'))):
134 os.path.isdir(os.path.join(curpath, '.hg'))):
134 if not self.callback or not self.callback(curpath):
135 if not self.callback or not self.callback(curpath):
135 raise util.Abort(_('path %r is inside nested repo %r') %
136 raise util.Abort(_('path %r is inside nested repo %r') %
136 (path, prefix))
137 (path, prefix))
137 prefixes.append(normprefix)
138 prefixes.append(normprefix)
138 parts.pop()
139 parts.pop()
139 normparts.pop()
140 normparts.pop()
140
141
141 self.audited.add(normpath)
142 self.audited.add(normpath)
142 # only add prefixes to the cache after checking everything: we don't
143 # only add prefixes to the cache after checking everything: we don't
143 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
144 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
144 self.auditeddir.update(prefixes)
145 self.auditeddir.update(prefixes)
145
146
146 class abstractopener(object):
147 class abstractopener(object):
147 """Abstract base class; cannot be instantiated"""
148 """Abstract base class; cannot be instantiated"""
148
149
149 def __init__(self, *args, **kwargs):
150 def __init__(self, *args, **kwargs):
150 '''Prevent instantiation; don't call this from subclasses.'''
151 '''Prevent instantiation; don't call this from subclasses.'''
151 raise NotImplementedError('attempted instantiating ' + str(type(self)))
152 raise NotImplementedError('attempted instantiating ' + str(type(self)))
152
153
153 def read(self, path):
154 def read(self, path):
154 fp = self(path, 'rb')
155 fp = self(path, 'rb')
155 try:
156 try:
156 return fp.read()
157 return fp.read()
157 finally:
158 finally:
158 fp.close()
159 fp.close()
159
160
160 def write(self, path, data):
161 def write(self, path, data):
161 fp = self(path, 'wb')
162 fp = self(path, 'wb')
162 try:
163 try:
163 return fp.write(data)
164 return fp.write(data)
164 finally:
165 finally:
165 fp.close()
166 fp.close()
166
167
167 def append(self, path, data):
168 def append(self, path, data):
168 fp = self(path, 'ab')
169 fp = self(path, 'ab')
169 try:
170 try:
170 return fp.write(data)
171 return fp.write(data)
171 finally:
172 finally:
172 fp.close()
173 fp.close()
173
174
174 class opener(abstractopener):
175 class opener(abstractopener):
175 '''Open files relative to a base directory
176 '''Open files relative to a base directory
176
177
177 This class is used to hide the details of COW semantics and
178 This class is used to hide the details of COW semantics and
178 remote file access from higher level code.
179 remote file access from higher level code.
179 '''
180 '''
180 def __init__(self, base, audit=True):
181 def __init__(self, base, audit=True):
181 self.base = base
182 self.base = base
182 self._audit = audit
183 self._audit = audit
183 if audit:
184 if audit:
184 self.auditor = pathauditor(base)
185 self.auditor = pathauditor(base)
185 else:
186 else:
186 self.auditor = util.always
187 self.auditor = util.always
187 self.createmode = None
188 self.createmode = None
188 self._trustnlink = None
189 self._trustnlink = None
189
190
190 @util.propertycache
191 @util.propertycache
191 def _cansymlink(self):
192 def _cansymlink(self):
192 return util.checklink(self.base)
193 return util.checklink(self.base)
193
194
194 def _fixfilemode(self, name):
195 def _fixfilemode(self, name):
195 if self.createmode is None:
196 if self.createmode is None:
196 return
197 return
197 os.chmod(name, self.createmode & 0666)
198 os.chmod(name, self.createmode & 0666)
198
199
199 def __call__(self, path, mode="r", text=False, atomictemp=False):
200 def __call__(self, path, mode="r", text=False, atomictemp=False):
200 if self._audit:
201 if self._audit:
201 r = util.checkosfilename(path)
202 r = util.checkosfilename(path)
202 if r:
203 if r:
203 raise util.Abort("%s: %r" % (r, path))
204 raise util.Abort("%s: %r" % (r, path))
204 self.auditor(path)
205 self.auditor(path)
205 f = os.path.join(self.base, path)
206 f = os.path.join(self.base, path)
206
207
207 if not text and "b" not in mode:
208 if not text and "b" not in mode:
208 mode += "b" # for that other OS
209 mode += "b" # for that other OS
209
210
210 nlink = -1
211 nlink = -1
211 dirname, basename = os.path.split(f)
212 dirname, basename = os.path.split(f)
212 # If basename is empty, then the path is malformed because it points
213 # If basename is empty, then the path is malformed because it points
213 # to a directory. Let the posixfile() call below raise IOError.
214 # to a directory. Let the posixfile() call below raise IOError.
214 if basename and mode not in ('r', 'rb'):
215 if basename and mode not in ('r', 'rb'):
215 if atomictemp:
216 if atomictemp:
216 if not os.path.isdir(dirname):
217 if not os.path.isdir(dirname):
217 util.makedirs(dirname, self.createmode)
218 util.makedirs(dirname, self.createmode)
218 return util.atomictempfile(f, mode, self.createmode)
219 return util.atomictempfile(f, mode, self.createmode)
219 try:
220 try:
220 if 'w' in mode:
221 if 'w' in mode:
221 util.unlink(f)
222 util.unlink(f)
222 nlink = 0
223 nlink = 0
223 else:
224 else:
224 # nlinks() may behave differently for files on Windows
225 # nlinks() may behave differently for files on Windows
225 # shares if the file is open.
226 # shares if the file is open.
226 fd = util.posixfile(f)
227 fd = util.posixfile(f)
227 nlink = util.nlinks(f)
228 nlink = util.nlinks(f)
228 if nlink < 1:
229 if nlink < 1:
229 nlink = 2 # force mktempcopy (issue1922)
230 nlink = 2 # force mktempcopy (issue1922)
230 fd.close()
231 fd.close()
231 except (OSError, IOError), e:
232 except (OSError, IOError), e:
232 if e.errno != errno.ENOENT:
233 if e.errno != errno.ENOENT:
233 raise
234 raise
234 nlink = 0
235 nlink = 0
235 if not os.path.isdir(dirname):
236 if not os.path.isdir(dirname):
236 util.makedirs(dirname, self.createmode)
237 util.makedirs(dirname, self.createmode)
237 if nlink > 0:
238 if nlink > 0:
238 if self._trustnlink is None:
239 if self._trustnlink is None:
239 self._trustnlink = nlink > 1 or util.checknlink(f)
240 self._trustnlink = nlink > 1 or util.checknlink(f)
240 if nlink > 1 or not self._trustnlink:
241 if nlink > 1 or not self._trustnlink:
241 util.rename(util.mktempcopy(f), f)
242 util.rename(util.mktempcopy(f), f)
242 fp = util.posixfile(f, mode)
243 fp = util.posixfile(f, mode)
243 if nlink == 0:
244 if nlink == 0:
244 self._fixfilemode(f)
245 self._fixfilemode(f)
245 return fp
246 return fp
246
247
247 def symlink(self, src, dst):
248 def symlink(self, src, dst):
248 self.auditor(dst)
249 self.auditor(dst)
249 linkname = os.path.join(self.base, dst)
250 linkname = os.path.join(self.base, dst)
250 try:
251 try:
251 os.unlink(linkname)
252 os.unlink(linkname)
252 except OSError:
253 except OSError:
253 pass
254 pass
254
255
255 dirname = os.path.dirname(linkname)
256 dirname = os.path.dirname(linkname)
256 if not os.path.exists(dirname):
257 if not os.path.exists(dirname):
257 util.makedirs(dirname, self.createmode)
258 util.makedirs(dirname, self.createmode)
258
259
259 if self._cansymlink:
260 if self._cansymlink:
260 try:
261 try:
261 os.symlink(src, linkname)
262 os.symlink(src, linkname)
262 except OSError, err:
263 except OSError, err:
263 raise OSError(err.errno, _('could not symlink to %r: %s') %
264 raise OSError(err.errno, _('could not symlink to %r: %s') %
264 (src, err.strerror), linkname)
265 (src, err.strerror), linkname)
265 else:
266 else:
266 f = self(dst, "w")
267 f = self(dst, "w")
267 f.write(src)
268 f.write(src)
268 f.close()
269 f.close()
269 self._fixfilemode(dst)
270 self._fixfilemode(dst)
270
271
271 def audit(self, path):
272 def audit(self, path):
272 self.auditor(path)
273 self.auditor(path)
273
274
274 class filteropener(abstractopener):
275 class filteropener(abstractopener):
275 '''Wrapper opener for filtering filenames with a function.'''
276 '''Wrapper opener for filtering filenames with a function.'''
276
277
277 def __init__(self, opener, filter):
278 def __init__(self, opener, filter):
278 self._filter = filter
279 self._filter = filter
279 self._orig = opener
280 self._orig = opener
280
281
281 def __call__(self, path, *args, **kwargs):
282 def __call__(self, path, *args, **kwargs):
282 return self._orig(self._filter(path), *args, **kwargs)
283 return self._orig(self._filter(path), *args, **kwargs)
283
284
284 def canonpath(root, cwd, myname, auditor=None):
285 def canonpath(root, cwd, myname, auditor=None):
285 '''return the canonical path of myname, given cwd and root'''
286 '''return the canonical path of myname, given cwd and root'''
286 if util.endswithsep(root):
287 if util.endswithsep(root):
287 rootsep = root
288 rootsep = root
288 else:
289 else:
289 rootsep = root + os.sep
290 rootsep = root + os.sep
290 name = myname
291 name = myname
291 if not os.path.isabs(name):
292 if not os.path.isabs(name):
292 name = os.path.join(root, cwd, name)
293 name = os.path.join(root, cwd, name)
293 name = os.path.normpath(name)
294 name = os.path.normpath(name)
294 if auditor is None:
295 if auditor is None:
295 auditor = pathauditor(root)
296 auditor = pathauditor(root)
296 if name != rootsep and name.startswith(rootsep):
297 if name != rootsep and name.startswith(rootsep):
297 name = name[len(rootsep):]
298 name = name[len(rootsep):]
298 auditor(name)
299 auditor(name)
299 return util.pconvert(name)
300 return util.pconvert(name)
300 elif name == root:
301 elif name == root:
301 return ''
302 return ''
302 else:
303 else:
303 # Determine whether `name' is in the hierarchy at or beneath `root',
304 # Determine whether `name' is in the hierarchy at or beneath `root',
304 # by iterating name=dirname(name) until that causes no change (can't
305 # by iterating name=dirname(name) until that causes no change (can't
305 # check name == '/', because that doesn't work on windows). For each
306 # check name == '/', because that doesn't work on windows). For each
306 # `name', compare dev/inode numbers. If they match, the list `rel'
307 # `name', compare dev/inode numbers. If they match, the list `rel'
307 # holds the reversed list of components making up the relative file
308 # holds the reversed list of components making up the relative file
308 # name we want.
309 # name we want.
309 root_st = os.stat(root)
310 root_st = os.stat(root)
310 rel = []
311 rel = []
311 while True:
312 while True:
312 try:
313 try:
313 name_st = os.stat(name)
314 name_st = os.stat(name)
314 except OSError:
315 except OSError:
315 break
316 break
316 if util.samestat(name_st, root_st):
317 if util.samestat(name_st, root_st):
317 if not rel:
318 if not rel:
318 # name was actually the same as root (maybe a symlink)
319 # name was actually the same as root (maybe a symlink)
319 return ''
320 return ''
320 rel.reverse()
321 rel.reverse()
321 name = os.path.join(*rel)
322 name = os.path.join(*rel)
322 auditor(name)
323 auditor(name)
323 return util.pconvert(name)
324 return util.pconvert(name)
324 dirname, basename = os.path.split(name)
325 dirname, basename = os.path.split(name)
325 rel.append(basename)
326 rel.append(basename)
326 if dirname == name:
327 if dirname == name:
327 break
328 break
328 name = dirname
329 name = dirname
329
330
330 raise util.Abort('%s not under root' % myname)
331 raise util.Abort('%s not under root' % myname)
331
332
332 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
333 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
333 '''yield every hg repository under path, recursively.'''
334 '''yield every hg repository under path, recursively.'''
334 def errhandler(err):
335 def errhandler(err):
335 if err.filename == path:
336 if err.filename == path:
336 raise err
337 raise err
337 samestat = getattr(os.path, 'samestat', None)
338 samestat = getattr(os.path, 'samestat', None)
338 if followsym and samestat is not None:
339 if followsym and samestat is not None:
339 def adddir(dirlst, dirname):
340 def adddir(dirlst, dirname):
340 match = False
341 match = False
341 dirstat = os.stat(dirname)
342 dirstat = os.stat(dirname)
342 for lstdirstat in dirlst:
343 for lstdirstat in dirlst:
343 if samestat(dirstat, lstdirstat):
344 if samestat(dirstat, lstdirstat):
344 match = True
345 match = True
345 break
346 break
346 if not match:
347 if not match:
347 dirlst.append(dirstat)
348 dirlst.append(dirstat)
348 return not match
349 return not match
349 else:
350 else:
350 followsym = False
351 followsym = False
351
352
352 if (seen_dirs is None) and followsym:
353 if (seen_dirs is None) and followsym:
353 seen_dirs = []
354 seen_dirs = []
354 adddir(seen_dirs, path)
355 adddir(seen_dirs, path)
355 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
356 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
356 dirs.sort()
357 dirs.sort()
357 if '.hg' in dirs:
358 if '.hg' in dirs:
358 yield root # found a repository
359 yield root # found a repository
359 qroot = os.path.join(root, '.hg', 'patches')
360 qroot = os.path.join(root, '.hg', 'patches')
360 if os.path.isdir(os.path.join(qroot, '.hg')):
361 if os.path.isdir(os.path.join(qroot, '.hg')):
361 yield qroot # we have a patch queue repo here
362 yield qroot # we have a patch queue repo here
362 if recurse:
363 if recurse:
363 # avoid recursing inside the .hg directory
364 # avoid recursing inside the .hg directory
364 dirs.remove('.hg')
365 dirs.remove('.hg')
365 else:
366 else:
366 dirs[:] = [] # don't descend further
367 dirs[:] = [] # don't descend further
367 elif followsym:
368 elif followsym:
368 newdirs = []
369 newdirs = []
369 for d in dirs:
370 for d in dirs:
370 fname = os.path.join(root, d)
371 fname = os.path.join(root, d)
371 if adddir(seen_dirs, fname):
372 if adddir(seen_dirs, fname):
372 if os.path.islink(fname):
373 if os.path.islink(fname):
373 for hgname in walkrepos(fname, True, seen_dirs):
374 for hgname in walkrepos(fname, True, seen_dirs):
374 yield hgname
375 yield hgname
375 else:
376 else:
376 newdirs.append(d)
377 newdirs.append(d)
377 dirs[:] = newdirs
378 dirs[:] = newdirs
378
379
379 def osrcpath():
380 def osrcpath():
380 '''return default os-specific hgrc search path'''
381 '''return default os-specific hgrc search path'''
381 path = systemrcpath()
382 path = systemrcpath()
382 path.extend(userrcpath())
383 path.extend(userrcpath())
383 path = [os.path.normpath(f) for f in path]
384 path = [os.path.normpath(f) for f in path]
384 return path
385 return path
385
386
386 _rcpath = None
387 _rcpath = None
387
388
388 def rcpath():
389 def rcpath():
389 '''return hgrc search path. if env var HGRCPATH is set, use it.
390 '''return hgrc search path. if env var HGRCPATH is set, use it.
390 for each item in path, if directory, use files ending in .rc,
391 for each item in path, if directory, use files ending in .rc,
391 else use item.
392 else use item.
392 make HGRCPATH empty to only look in .hg/hgrc of current repo.
393 make HGRCPATH empty to only look in .hg/hgrc of current repo.
393 if no HGRCPATH, use default os-specific path.'''
394 if no HGRCPATH, use default os-specific path.'''
394 global _rcpath
395 global _rcpath
395 if _rcpath is None:
396 if _rcpath is None:
396 if 'HGRCPATH' in os.environ:
397 if 'HGRCPATH' in os.environ:
397 _rcpath = []
398 _rcpath = []
398 for p in os.environ['HGRCPATH'].split(os.pathsep):
399 for p in os.environ['HGRCPATH'].split(os.pathsep):
399 if not p:
400 if not p:
400 continue
401 continue
401 p = util.expandpath(p)
402 p = util.expandpath(p)
402 if os.path.isdir(p):
403 if os.path.isdir(p):
403 for f, kind in osutil.listdir(p):
404 for f, kind in osutil.listdir(p):
404 if f.endswith('.rc'):
405 if f.endswith('.rc'):
405 _rcpath.append(os.path.join(p, f))
406 _rcpath.append(os.path.join(p, f))
406 else:
407 else:
407 _rcpath.append(p)
408 _rcpath.append(p)
408 else:
409 else:
409 _rcpath = osrcpath()
410 _rcpath = osrcpath()
410 return _rcpath
411 return _rcpath
411
412
412 if os.name != 'nt':
413 if os.name != 'nt':
413
414
414 def rcfiles(path):
415 def rcfiles(path):
415 rcs = [os.path.join(path, 'hgrc')]
416 rcs = [os.path.join(path, 'hgrc')]
416 rcdir = os.path.join(path, 'hgrc.d')
417 rcdir = os.path.join(path, 'hgrc.d')
417 try:
418 try:
418 rcs.extend([os.path.join(rcdir, f)
419 rcs.extend([os.path.join(rcdir, f)
419 for f, kind in osutil.listdir(rcdir)
420 for f, kind in osutil.listdir(rcdir)
420 if f.endswith(".rc")])
421 if f.endswith(".rc")])
421 except OSError:
422 except OSError:
422 pass
423 pass
423 return rcs
424 return rcs
424
425
425 def systemrcpath():
426 def systemrcpath():
426 path = []
427 path = []
427 # old mod_python does not set sys.argv
428 # old mod_python does not set sys.argv
428 if len(getattr(sys, 'argv', [])) > 0:
429 if len(getattr(sys, 'argv', [])) > 0:
429 p = os.path.dirname(os.path.dirname(sys.argv[0]))
430 p = os.path.dirname(os.path.dirname(sys.argv[0]))
430 path.extend(rcfiles(os.path.join(p, 'etc/mercurial')))
431 path.extend(rcfiles(os.path.join(p, 'etc/mercurial')))
431 path.extend(rcfiles('/etc/mercurial'))
432 path.extend(rcfiles('/etc/mercurial'))
432 return path
433 return path
433
434
434 def userrcpath():
435 def userrcpath():
435 return [os.path.expanduser('~/.hgrc')]
436 return [os.path.expanduser('~/.hgrc')]
436
437
437 else:
438 else:
438
439
439 _HKEY_LOCAL_MACHINE = 0x80000002L
440 _HKEY_LOCAL_MACHINE = 0x80000002L
440
441
441 def systemrcpath():
442 def systemrcpath():
442 '''return default os-specific hgrc search path'''
443 '''return default os-specific hgrc search path'''
443 rcpath = []
444 rcpath = []
444 filename = util.executablepath()
445 filename = util.executablepath()
445 # Use mercurial.ini found in directory with hg.exe
446 # Use mercurial.ini found in directory with hg.exe
446 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
447 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
447 if os.path.isfile(progrc):
448 if os.path.isfile(progrc):
448 rcpath.append(progrc)
449 rcpath.append(progrc)
449 return rcpath
450 return rcpath
450 # Use hgrc.d found in directory with hg.exe
451 # Use hgrc.d found in directory with hg.exe
451 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
452 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
452 if os.path.isdir(progrcd):
453 if os.path.isdir(progrcd):
453 for f, kind in osutil.listdir(progrcd):
454 for f, kind in osutil.listdir(progrcd):
454 if f.endswith('.rc'):
455 if f.endswith('.rc'):
455 rcpath.append(os.path.join(progrcd, f))
456 rcpath.append(os.path.join(progrcd, f))
456 return rcpath
457 return rcpath
457 # else look for a system rcpath in the registry
458 # else look for a system rcpath in the registry
458 value = util.lookupreg('SOFTWARE\\Mercurial', None,
459 value = util.lookupreg('SOFTWARE\\Mercurial', None,
459 _HKEY_LOCAL_MACHINE)
460 _HKEY_LOCAL_MACHINE)
460 if not isinstance(value, str) or not value:
461 if not isinstance(value, str) or not value:
461 return rcpath
462 return rcpath
462 value = value.replace('/', os.sep)
463 value = value.replace('/', os.sep)
463 for p in value.split(os.pathsep):
464 for p in value.split(os.pathsep):
464 if p.lower().endswith('mercurial.ini'):
465 if p.lower().endswith('mercurial.ini'):
465 rcpath.append(p)
466 rcpath.append(p)
466 elif os.path.isdir(p):
467 elif os.path.isdir(p):
467 for f, kind in osutil.listdir(p):
468 for f, kind in osutil.listdir(p):
468 if f.endswith('.rc'):
469 if f.endswith('.rc'):
469 rcpath.append(os.path.join(p, f))
470 rcpath.append(os.path.join(p, f))
470 return rcpath
471 return rcpath
471
472
472 def userrcpath():
473 def userrcpath():
473 '''return os-specific hgrc search path to the user dir'''
474 '''return os-specific hgrc search path to the user dir'''
474 home = os.path.expanduser('~')
475 home = os.path.expanduser('~')
475 path = [os.path.join(home, 'mercurial.ini'),
476 path = [os.path.join(home, 'mercurial.ini'),
476 os.path.join(home, '.hgrc')]
477 os.path.join(home, '.hgrc')]
477 userprofile = os.environ.get('USERPROFILE')
478 userprofile = os.environ.get('USERPROFILE')
478 if userprofile:
479 if userprofile:
479 path.append(os.path.join(userprofile, 'mercurial.ini'))
480 path.append(os.path.join(userprofile, 'mercurial.ini'))
480 path.append(os.path.join(userprofile, '.hgrc'))
481 path.append(os.path.join(userprofile, '.hgrc'))
481 return path
482 return path
482
483
483 def revsingle(repo, revspec, default='.'):
484 def revsingle(repo, revspec, default='.'):
484 if not revspec:
485 if not revspec:
485 return repo[default]
486 return repo[default]
486
487
487 l = revrange(repo, [revspec])
488 l = revrange(repo, [revspec])
488 if len(l) < 1:
489 if len(l) < 1:
489 raise util.Abort(_('empty revision set'))
490 raise util.Abort(_('empty revision set'))
490 return repo[l[-1]]
491 return repo[l[-1]]
491
492
492 def revpair(repo, revs):
493 def revpair(repo, revs):
493 if not revs:
494 if not revs:
494 return repo.dirstate.p1(), None
495 return repo.dirstate.p1(), None
495
496
496 l = revrange(repo, revs)
497 l = revrange(repo, revs)
497
498
498 if len(l) == 0:
499 if len(l) == 0:
499 return repo.dirstate.p1(), None
500 return repo.dirstate.p1(), None
500
501
501 if len(l) == 1:
502 if len(l) == 1:
502 return repo.lookup(l[0]), None
503 return repo.lookup(l[0]), None
503
504
504 return repo.lookup(l[0]), repo.lookup(l[-1])
505 return repo.lookup(l[0]), repo.lookup(l[-1])
505
506
506 _revrangesep = ':'
507 _revrangesep = ':'
507
508
508 def revrange(repo, revs):
509 def revrange(repo, revs):
509 """Yield revision as strings from a list of revision specifications."""
510 """Yield revision as strings from a list of revision specifications."""
510
511
511 def revfix(repo, val, defval):
512 def revfix(repo, val, defval):
512 if not val and val != 0 and defval is not None:
513 if not val and val != 0 and defval is not None:
513 return defval
514 return defval
514 return repo.changelog.rev(repo.lookup(val))
515 return repo.changelog.rev(repo.lookup(val))
515
516
516 seen, l = set(), []
517 seen, l = set(), []
517 for spec in revs:
518 for spec in revs:
518 # attempt to parse old-style ranges first to deal with
519 # attempt to parse old-style ranges first to deal with
519 # things like old-tag which contain query metacharacters
520 # things like old-tag which contain query metacharacters
520 try:
521 try:
521 if isinstance(spec, int):
522 if isinstance(spec, int):
522 seen.add(spec)
523 seen.add(spec)
523 l.append(spec)
524 l.append(spec)
524 continue
525 continue
525
526
526 if _revrangesep in spec:
527 if _revrangesep in spec:
527 start, end = spec.split(_revrangesep, 1)
528 start, end = spec.split(_revrangesep, 1)
528 start = revfix(repo, start, 0)
529 start = revfix(repo, start, 0)
529 end = revfix(repo, end, len(repo) - 1)
530 end = revfix(repo, end, len(repo) - 1)
530 step = start > end and -1 or 1
531 step = start > end and -1 or 1
531 for rev in xrange(start, end + step, step):
532 for rev in xrange(start, end + step, step):
532 if rev in seen:
533 if rev in seen:
533 continue
534 continue
534 seen.add(rev)
535 seen.add(rev)
535 l.append(rev)
536 l.append(rev)
536 continue
537 continue
537 elif spec and spec in repo: # single unquoted rev
538 elif spec and spec in repo: # single unquoted rev
538 rev = revfix(repo, spec, None)
539 rev = revfix(repo, spec, None)
539 if rev in seen:
540 if rev in seen:
540 continue
541 continue
541 seen.add(rev)
542 seen.add(rev)
542 l.append(rev)
543 l.append(rev)
543 continue
544 continue
544 except error.RepoLookupError:
545 except error.RepoLookupError:
545 pass
546 pass
546
547
547 # fall through to new-style queries if old-style fails
548 # fall through to new-style queries if old-style fails
548 m = revset.match(repo.ui, spec)
549 m = revset.match(repo.ui, spec)
549 for r in m(repo, range(len(repo))):
550 for r in m(repo, range(len(repo))):
550 if r not in seen:
551 if r not in seen:
551 l.append(r)
552 l.append(r)
552 seen.update(l)
553 seen.update(l)
553
554
554 return l
555 return l
555
556
556 def expandpats(pats):
557 def expandpats(pats):
557 if not util.expandglobs:
558 if not util.expandglobs:
558 return list(pats)
559 return list(pats)
559 ret = []
560 ret = []
560 for p in pats:
561 for p in pats:
561 kind, name = matchmod._patsplit(p, None)
562 kind, name = matchmod._patsplit(p, None)
562 if kind is None:
563 if kind is None:
563 try:
564 try:
564 globbed = glob.glob(name)
565 globbed = glob.glob(name)
565 except re.error:
566 except re.error:
566 globbed = [name]
567 globbed = [name]
567 if globbed:
568 if globbed:
568 ret.extend(globbed)
569 ret.extend(globbed)
569 continue
570 continue
570 ret.append(p)
571 ret.append(p)
571 return ret
572 return ret
572
573
573 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
574 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
574 if pats == ("",):
575 if pats == ("",):
575 pats = []
576 pats = []
576 if not globbed and default == 'relpath':
577 if not globbed and default == 'relpath':
577 pats = expandpats(pats or [])
578 pats = expandpats(pats or [])
578
579
579 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
580 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
580 default)
581 default)
581 def badfn(f, msg):
582 def badfn(f, msg):
582 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
583 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
583 m.bad = badfn
584 m.bad = badfn
584 return m
585 return m
585
586
586 def matchall(repo):
587 def matchall(repo):
587 return matchmod.always(repo.root, repo.getcwd())
588 return matchmod.always(repo.root, repo.getcwd())
588
589
589 def matchfiles(repo, files):
590 def matchfiles(repo, files):
590 return matchmod.exact(repo.root, repo.getcwd(), files)
591 return matchmod.exact(repo.root, repo.getcwd(), files)
591
592
592 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
593 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
593 if dry_run is None:
594 if dry_run is None:
594 dry_run = opts.get('dry_run')
595 dry_run = opts.get('dry_run')
595 if similarity is None:
596 if similarity is None:
596 similarity = float(opts.get('similarity') or 0)
597 similarity = float(opts.get('similarity') or 0)
597 # we'd use status here, except handling of symlinks and ignore is tricky
598 # we'd use status here, except handling of symlinks and ignore is tricky
598 added, unknown, deleted, removed = [], [], [], []
599 added, unknown, deleted, removed = [], [], [], []
599 audit_path = pathauditor(repo.root)
600 audit_path = pathauditor(repo.root)
600 m = match(repo[None], pats, opts)
601 m = match(repo[None], pats, opts)
601 for abs in repo.walk(m):
602 for abs in repo.walk(m):
602 target = repo.wjoin(abs)
603 target = repo.wjoin(abs)
603 good = True
604 good = True
604 try:
605 try:
605 audit_path(abs)
606 audit_path(abs)
606 except (OSError, util.Abort):
607 except (OSError, util.Abort):
607 good = False
608 good = False
608 rel = m.rel(abs)
609 rel = m.rel(abs)
609 exact = m.exact(abs)
610 exact = m.exact(abs)
610 if good and abs not in repo.dirstate:
611 if good and abs not in repo.dirstate:
611 unknown.append(abs)
612 unknown.append(abs)
612 if repo.ui.verbose or not exact:
613 if repo.ui.verbose or not exact:
613 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
614 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
614 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
615 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
615 or (os.path.isdir(target) and not os.path.islink(target))):
616 or (os.path.isdir(target) and not os.path.islink(target))):
616 deleted.append(abs)
617 deleted.append(abs)
617 if repo.ui.verbose or not exact:
618 if repo.ui.verbose or not exact:
618 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
619 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
619 # for finding renames
620 # for finding renames
620 elif repo.dirstate[abs] == 'r':
621 elif repo.dirstate[abs] == 'r':
621 removed.append(abs)
622 removed.append(abs)
622 elif repo.dirstate[abs] == 'a':
623 elif repo.dirstate[abs] == 'a':
623 added.append(abs)
624 added.append(abs)
624 copies = {}
625 copies = {}
625 if similarity > 0:
626 if similarity > 0:
626 for old, new, score in similar.findrenames(repo,
627 for old, new, score in similar.findrenames(repo,
627 added + unknown, removed + deleted, similarity):
628 added + unknown, removed + deleted, similarity):
628 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
629 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
629 repo.ui.status(_('recording removal of %s as rename to %s '
630 repo.ui.status(_('recording removal of %s as rename to %s '
630 '(%d%% similar)\n') %
631 '(%d%% similar)\n') %
631 (m.rel(old), m.rel(new), score * 100))
632 (m.rel(old), m.rel(new), score * 100))
632 copies[new] = old
633 copies[new] = old
633
634
634 if not dry_run:
635 if not dry_run:
635 wctx = repo[None]
636 wctx = repo[None]
636 wlock = repo.wlock()
637 wlock = repo.wlock()
637 try:
638 try:
638 wctx.forget(deleted)
639 wctx.forget(deleted)
639 wctx.add(unknown)
640 wctx.add(unknown)
640 for new, old in copies.iteritems():
641 for new, old in copies.iteritems():
641 wctx.copy(old, new)
642 wctx.copy(old, new)
642 finally:
643 finally:
643 wlock.release()
644 wlock.release()
644
645
645 def updatedir(ui, repo, patches, similarity=0):
646 def updatedir(ui, repo, patches, similarity=0):
646 '''Update dirstate after patch application according to metadata'''
647 '''Update dirstate after patch application according to metadata'''
647 if not patches:
648 if not patches:
648 return []
649 return []
649 copies = []
650 copies = []
650 removes = set()
651 removes = set()
651 cfiles = patches.keys()
652 cfiles = patches.keys()
652 cwd = repo.getcwd()
653 cwd = repo.getcwd()
653 if cwd:
654 if cwd:
654 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
655 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
655 for f in patches:
656 for f in patches:
656 gp = patches[f]
657 gp = patches[f]
657 if not gp:
658 if not gp:
658 continue
659 continue
659 if gp.op == 'RENAME':
660 if gp.op == 'RENAME':
660 copies.append((gp.oldpath, gp.path))
661 copies.append((gp.oldpath, gp.path))
661 removes.add(gp.oldpath)
662 removes.add(gp.oldpath)
662 elif gp.op == 'COPY':
663 elif gp.op == 'COPY':
663 copies.append((gp.oldpath, gp.path))
664 copies.append((gp.oldpath, gp.path))
664 elif gp.op == 'DELETE':
665 elif gp.op == 'DELETE':
665 removes.add(gp.path)
666 removes.add(gp.path)
666
667
667 wctx = repo[None]
668 wctx = repo[None]
668 for src, dst in copies:
669 for src, dst in copies:
669 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
670 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
670 if (not similarity) and removes:
671 if (not similarity) and removes:
671 wctx.remove(sorted(removes), True)
672 wctx.remove(sorted(removes), True)
672
673
673 for f in patches:
674 for f in patches:
674 gp = patches[f]
675 gp = patches[f]
675 if gp and gp.mode:
676 if gp and gp.mode:
676 islink, isexec = gp.mode
677 islink, isexec = gp.mode
677 dst = repo.wjoin(gp.path)
678 dst = repo.wjoin(gp.path)
678 # patch won't create empty files
679 # patch won't create empty files
679 if gp.op == 'ADD' and not os.path.lexists(dst):
680 if gp.op == 'ADD' and not os.path.lexists(dst):
680 flags = (isexec and 'x' or '') + (islink and 'l' or '')
681 flags = (isexec and 'x' or '') + (islink and 'l' or '')
681 repo.wwrite(gp.path, '', flags)
682 repo.wwrite(gp.path, '', flags)
682 util.setflags(dst, islink, isexec)
683 util.setflags(dst, islink, isexec)
683 addremove(repo, cfiles, similarity=similarity)
684 addremove(repo, cfiles, similarity=similarity)
684 files = patches.keys()
685 files = patches.keys()
685 files.extend([r for r in removes if r not in files])
686 files.extend([r for r in removes if r not in files])
686 return sorted(files)
687 return sorted(files)
687
688
688 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
689 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
689 """Update the dirstate to reflect the intent of copying src to dst. For
690 """Update the dirstate to reflect the intent of copying src to dst. For
690 different reasons it might not end with dst being marked as copied from src.
691 different reasons it might not end with dst being marked as copied from src.
691 """
692 """
692 origsrc = repo.dirstate.copied(src) or src
693 origsrc = repo.dirstate.copied(src) or src
693 if dst == origsrc: # copying back a copy?
694 if dst == origsrc: # copying back a copy?
694 if repo.dirstate[dst] not in 'mn' and not dryrun:
695 if repo.dirstate[dst] not in 'mn' and not dryrun:
695 repo.dirstate.normallookup(dst)
696 repo.dirstate.normallookup(dst)
696 else:
697 else:
697 if repo.dirstate[origsrc] == 'a' and origsrc == src:
698 if repo.dirstate[origsrc] == 'a' and origsrc == src:
698 if not ui.quiet:
699 if not ui.quiet:
699 ui.warn(_("%s has not been committed yet, so no copy "
700 ui.warn(_("%s has not been committed yet, so no copy "
700 "data will be stored for %s.\n")
701 "data will be stored for %s.\n")
701 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
702 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
702 if repo.dirstate[dst] in '?r' and not dryrun:
703 if repo.dirstate[dst] in '?r' and not dryrun:
703 wctx.add([dst])
704 wctx.add([dst])
704 elif not dryrun:
705 elif not dryrun:
705 wctx.copy(origsrc, dst)
706 wctx.copy(origsrc, dst)
706
707
707 def readrequires(opener, supported):
708 def readrequires(opener, supported):
708 '''Reads and parses .hg/requires and checks if all entries found
709 '''Reads and parses .hg/requires and checks if all entries found
709 are in the list of supported features.'''
710 are in the list of supported features.'''
710 requirements = set(opener.read("requires").splitlines())
711 requirements = set(opener.read("requires").splitlines())
711 missings = []
712 missings = []
712 for r in requirements:
713 for r in requirements:
713 if r not in supported:
714 if r not in supported:
714 if not r or not r[0].isalnum():
715 if not r or not r[0].isalnum():
715 raise error.RequirementError(_(".hg/requires file is corrupt"))
716 raise error.RequirementError(_(".hg/requires file is corrupt"))
716 missings.append(r)
717 missings.append(r)
717 missings.sort()
718 missings.sort()
718 if missings:
719 if missings:
719 raise error.RequirementError(_("unknown repository format: "
720 raise error.RequirementError(_("unknown repository format: "
720 "requires features '%s' (upgrade Mercurial)") % "', '".join(missings))
721 "requires features '%s' (upgrade Mercurial)") % "', '".join(missings))
721 return requirements
722 return requirements
722
723
723 class filecacheentry(object):
724 class filecacheentry(object):
724 def __init__(self, path):
725 def __init__(self, path):
725 self.path = path
726 self.path = path
726 self.cachestat = filecacheentry.stat(self.path)
727 self.cachestat = filecacheentry.stat(self.path)
727
728
728 if self.cachestat:
729 if self.cachestat:
729 self._cacheable = self.cachestat.cacheable()
730 self._cacheable = self.cachestat.cacheable()
730 else:
731 else:
731 # None means we don't know yet
732 # None means we don't know yet
732 self._cacheable = None
733 self._cacheable = None
733
734
734 def refresh(self):
735 def refresh(self):
735 if self.cacheable():
736 if self.cacheable():
736 self.cachestat = filecacheentry.stat(self.path)
737 self.cachestat = filecacheentry.stat(self.path)
737
738
738 def cacheable(self):
739 def cacheable(self):
739 if self._cacheable is not None:
740 if self._cacheable is not None:
740 return self._cacheable
741 return self._cacheable
741
742
742 # we don't know yet, assume it is for now
743 # we don't know yet, assume it is for now
743 return True
744 return True
744
745
745 def changed(self):
746 def changed(self):
746 # no point in going further if we can't cache it
747 # no point in going further if we can't cache it
747 if not self.cacheable():
748 if not self.cacheable():
748 return True
749 return True
749
750
750 newstat = filecacheentry.stat(self.path)
751 newstat = filecacheentry.stat(self.path)
751
752
752 # we may not know if it's cacheable yet, check again now
753 # we may not know if it's cacheable yet, check again now
753 if newstat and self._cacheable is None:
754 if newstat and self._cacheable is None:
754 self._cacheable = newstat.cacheable()
755 self._cacheable = newstat.cacheable()
755
756
756 # check again
757 # check again
757 if not self._cacheable:
758 if not self._cacheable:
758 return True
759 return True
759
760
760 if self.cachestat != newstat:
761 if self.cachestat != newstat:
761 self.cachestat = newstat
762 self.cachestat = newstat
762 return True
763 return True
763 else:
764 else:
764 return False
765 return False
765
766
766 @staticmethod
767 @staticmethod
767 def stat(path):
768 def stat(path):
768 try:
769 try:
769 return util.cachestat(path)
770 return util.cachestat(path)
770 except OSError, e:
771 except OSError, e:
771 if e.errno != errno.ENOENT:
772 if e.errno != errno.ENOENT:
772 raise
773 raise
773
774
774 class filecache(object):
775 class filecache(object):
775 '''A property like decorator that tracks a file under .hg/ for updates.
776 '''A property like decorator that tracks a file under .hg/ for updates.
776
777
777 Records stat info when called in _filecache.
778 Records stat info when called in _filecache.
778
779
779 On subsequent calls, compares old stat info with new info, and recreates
780 On subsequent calls, compares old stat info with new info, and recreates
780 the object when needed, updating the new stat info in _filecache.
781 the object when needed, updating the new stat info in _filecache.
781
782
782 Mercurial either atomic renames or appends for files under .hg,
783 Mercurial either atomic renames or appends for files under .hg,
783 so to ensure the cache is reliable we need the filesystem to be able
784 so to ensure the cache is reliable we need the filesystem to be able
784 to tell us if a file has been replaced. If it can't, we fallback to
785 to tell us if a file has been replaced. If it can't, we fallback to
785 recreating the object on every call (essentially the same behaviour as
786 recreating the object on every call (essentially the same behaviour as
786 propertycache).'''
787 propertycache).'''
787 def __init__(self, path, instore=False):
788 def __init__(self, path, instore=False):
788 self.path = path
789 self.path = path
789 self.instore = instore
790 self.instore = instore
790
791
791 def __call__(self, func):
792 def __call__(self, func):
792 self.func = func
793 self.func = func
793 self.name = func.__name__
794 self.name = func.__name__
794 return self
795 return self
795
796
796 def __get__(self, obj, type=None):
797 def __get__(self, obj, type=None):
797 entry = obj._filecache.get(self.name)
798 entry = obj._filecache.get(self.name)
798
799
799 if entry:
800 if entry:
800 if entry.changed():
801 if entry.changed():
801 entry.obj = self.func(obj)
802 entry.obj = self.func(obj)
802 else:
803 else:
803 path = self.instore and obj.sjoin(self.path) or obj.join(self.path)
804 path = self.instore and obj.sjoin(self.path) or obj.join(self.path)
804
805
805 # We stat -before- creating the object so our cache doesn't lie if
806 # We stat -before- creating the object so our cache doesn't lie if
806 # a writer modified between the time we read and stat
807 # a writer modified between the time we read and stat
807 entry = filecacheentry(path)
808 entry = filecacheentry(path)
808 entry.obj = self.func(obj)
809 entry.obj = self.func(obj)
809
810
810 obj._filecache[self.name] = entry
811 obj._filecache[self.name] = entry
811
812
812 setattr(obj, self.name, entry.obj)
813 setattr(obj, self.name, entry.obj)
813 return entry.obj
814 return entry.obj
General Comments 0
You need to be logged in to leave comments. Login now