##// END OF EJS Templates
dirstate.walk: fast path match-always case during traversal...
Siddharth Agarwal -
r18814:1413ba41 default
parent child Browse files
Show More
@@ -1,829 +1,830
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 import errno
7 import errno
8
8
9 from node import nullid
9 from node import nullid
10 from i18n import _
10 from i18n import _
11 import scmutil, util, ignore, osutil, parsers, encoding
11 import scmutil, util, ignore, osutil, parsers, encoding
12 import os, stat, errno, gc
12 import os, stat, errno, gc
13
13
14 propertycache = util.propertycache
14 propertycache = util.propertycache
15 filecache = scmutil.filecache
15 filecache = scmutil.filecache
16 _rangemask = 0x7fffffff
16 _rangemask = 0x7fffffff
17
17
18 class repocache(filecache):
18 class repocache(filecache):
19 """filecache for files in .hg/"""
19 """filecache for files in .hg/"""
20 def join(self, obj, fname):
20 def join(self, obj, fname):
21 return obj._opener.join(fname)
21 return obj._opener.join(fname)
22
22
23 class rootcache(filecache):
23 class rootcache(filecache):
24 """filecache for files in the repository root"""
24 """filecache for files in the repository root"""
25 def join(self, obj, fname):
25 def join(self, obj, fname):
26 return obj._join(fname)
26 return obj._join(fname)
27
27
28 def _finddirs(path):
28 def _finddirs(path):
29 pos = path.rfind('/')
29 pos = path.rfind('/')
30 while pos != -1:
30 while pos != -1:
31 yield path[:pos]
31 yield path[:pos]
32 pos = path.rfind('/', 0, pos)
32 pos = path.rfind('/', 0, pos)
33
33
34 def _incdirs(dirs, path):
34 def _incdirs(dirs, path):
35 for base in _finddirs(path):
35 for base in _finddirs(path):
36 if base in dirs:
36 if base in dirs:
37 dirs[base] += 1
37 dirs[base] += 1
38 return
38 return
39 dirs[base] = 1
39 dirs[base] = 1
40
40
41 def _decdirs(dirs, path):
41 def _decdirs(dirs, path):
42 for base in _finddirs(path):
42 for base in _finddirs(path):
43 if dirs[base] > 1:
43 if dirs[base] > 1:
44 dirs[base] -= 1
44 dirs[base] -= 1
45 return
45 return
46 del dirs[base]
46 del dirs[base]
47
47
48 class dirstate(object):
48 class dirstate(object):
49
49
50 def __init__(self, opener, ui, root, validate):
50 def __init__(self, opener, ui, root, validate):
51 '''Create a new dirstate object.
51 '''Create a new dirstate object.
52
52
53 opener is an open()-like callable that can be used to open the
53 opener is an open()-like callable that can be used to open the
54 dirstate file; root is the root of the directory tracked by
54 dirstate file; root is the root of the directory tracked by
55 the dirstate.
55 the dirstate.
56 '''
56 '''
57 self._opener = opener
57 self._opener = opener
58 self._validate = validate
58 self._validate = validate
59 self._root = root
59 self._root = root
60 self._rootdir = os.path.join(root, '')
60 self._rootdir = os.path.join(root, '')
61 self._dirty = False
61 self._dirty = False
62 self._dirtypl = False
62 self._dirtypl = False
63 self._lastnormaltime = 0
63 self._lastnormaltime = 0
64 self._ui = ui
64 self._ui = ui
65 self._filecache = {}
65 self._filecache = {}
66
66
67 @propertycache
67 @propertycache
68 def _map(self):
68 def _map(self):
69 '''Return the dirstate contents as a map from filename to
69 '''Return the dirstate contents as a map from filename to
70 (state, mode, size, time).'''
70 (state, mode, size, time).'''
71 self._read()
71 self._read()
72 return self._map
72 return self._map
73
73
74 @propertycache
74 @propertycache
75 def _copymap(self):
75 def _copymap(self):
76 self._read()
76 self._read()
77 return self._copymap
77 return self._copymap
78
78
79 @propertycache
79 @propertycache
80 def _foldmap(self):
80 def _foldmap(self):
81 f = {}
81 f = {}
82 for name in self._map:
82 for name in self._map:
83 f[util.normcase(name)] = name
83 f[util.normcase(name)] = name
84 for name in self._dirs:
84 for name in self._dirs:
85 f[util.normcase(name)] = name
85 f[util.normcase(name)] = name
86 f['.'] = '.' # prevents useless util.fspath() invocation
86 f['.'] = '.' # prevents useless util.fspath() invocation
87 return f
87 return f
88
88
89 @repocache('branch')
89 @repocache('branch')
90 def _branch(self):
90 def _branch(self):
91 try:
91 try:
92 return self._opener.read("branch").strip() or "default"
92 return self._opener.read("branch").strip() or "default"
93 except IOError, inst:
93 except IOError, inst:
94 if inst.errno != errno.ENOENT:
94 if inst.errno != errno.ENOENT:
95 raise
95 raise
96 return "default"
96 return "default"
97
97
98 @propertycache
98 @propertycache
99 def _pl(self):
99 def _pl(self):
100 try:
100 try:
101 fp = self._opener("dirstate")
101 fp = self._opener("dirstate")
102 st = fp.read(40)
102 st = fp.read(40)
103 fp.close()
103 fp.close()
104 l = len(st)
104 l = len(st)
105 if l == 40:
105 if l == 40:
106 return st[:20], st[20:40]
106 return st[:20], st[20:40]
107 elif l > 0 and l < 40:
107 elif l > 0 and l < 40:
108 raise util.Abort(_('working directory state appears damaged!'))
108 raise util.Abort(_('working directory state appears damaged!'))
109 except IOError, err:
109 except IOError, err:
110 if err.errno != errno.ENOENT:
110 if err.errno != errno.ENOENT:
111 raise
111 raise
112 return [nullid, nullid]
112 return [nullid, nullid]
113
113
114 @propertycache
114 @propertycache
115 def _dirs(self):
115 def _dirs(self):
116 dirs = {}
116 dirs = {}
117 for f, s in self._map.iteritems():
117 for f, s in self._map.iteritems():
118 if s[0] != 'r':
118 if s[0] != 'r':
119 _incdirs(dirs, f)
119 _incdirs(dirs, f)
120 return dirs
120 return dirs
121
121
122 def dirs(self):
122 def dirs(self):
123 return self._dirs
123 return self._dirs
124
124
125 @rootcache('.hgignore')
125 @rootcache('.hgignore')
126 def _ignore(self):
126 def _ignore(self):
127 files = [self._join('.hgignore')]
127 files = [self._join('.hgignore')]
128 for name, path in self._ui.configitems("ui"):
128 for name, path in self._ui.configitems("ui"):
129 if name == 'ignore' or name.startswith('ignore.'):
129 if name == 'ignore' or name.startswith('ignore.'):
130 files.append(util.expandpath(path))
130 files.append(util.expandpath(path))
131 return ignore.ignore(self._root, files, self._ui.warn)
131 return ignore.ignore(self._root, files, self._ui.warn)
132
132
133 @propertycache
133 @propertycache
134 def _slash(self):
134 def _slash(self):
135 return self._ui.configbool('ui', 'slash') and os.sep != '/'
135 return self._ui.configbool('ui', 'slash') and os.sep != '/'
136
136
137 @propertycache
137 @propertycache
138 def _checklink(self):
138 def _checklink(self):
139 return util.checklink(self._root)
139 return util.checklink(self._root)
140
140
141 @propertycache
141 @propertycache
142 def _checkexec(self):
142 def _checkexec(self):
143 return util.checkexec(self._root)
143 return util.checkexec(self._root)
144
144
145 @propertycache
145 @propertycache
146 def _checkcase(self):
146 def _checkcase(self):
147 return not util.checkcase(self._join('.hg'))
147 return not util.checkcase(self._join('.hg'))
148
148
149 def _join(self, f):
149 def _join(self, f):
150 # much faster than os.path.join()
150 # much faster than os.path.join()
151 # it's safe because f is always a relative path
151 # it's safe because f is always a relative path
152 return self._rootdir + f
152 return self._rootdir + f
153
153
154 def flagfunc(self, buildfallback):
154 def flagfunc(self, buildfallback):
155 if self._checklink and self._checkexec:
155 if self._checklink and self._checkexec:
156 def f(x):
156 def f(x):
157 p = self._join(x)
157 p = self._join(x)
158 if os.path.islink(p):
158 if os.path.islink(p):
159 return 'l'
159 return 'l'
160 if util.isexec(p):
160 if util.isexec(p):
161 return 'x'
161 return 'x'
162 return ''
162 return ''
163 return f
163 return f
164
164
165 fallback = buildfallback()
165 fallback = buildfallback()
166 if self._checklink:
166 if self._checklink:
167 def f(x):
167 def f(x):
168 if os.path.islink(self._join(x)):
168 if os.path.islink(self._join(x)):
169 return 'l'
169 return 'l'
170 if 'x' in fallback(x):
170 if 'x' in fallback(x):
171 return 'x'
171 return 'x'
172 return ''
172 return ''
173 return f
173 return f
174 if self._checkexec:
174 if self._checkexec:
175 def f(x):
175 def f(x):
176 if 'l' in fallback(x):
176 if 'l' in fallback(x):
177 return 'l'
177 return 'l'
178 if util.isexec(self._join(x)):
178 if util.isexec(self._join(x)):
179 return 'x'
179 return 'x'
180 return ''
180 return ''
181 return f
181 return f
182 else:
182 else:
183 return fallback
183 return fallback
184
184
185 def getcwd(self):
185 def getcwd(self):
186 cwd = os.getcwd()
186 cwd = os.getcwd()
187 if cwd == self._root:
187 if cwd == self._root:
188 return ''
188 return ''
189 # self._root ends with a path separator if self._root is '/' or 'C:\'
189 # self._root ends with a path separator if self._root is '/' or 'C:\'
190 rootsep = self._root
190 rootsep = self._root
191 if not util.endswithsep(rootsep):
191 if not util.endswithsep(rootsep):
192 rootsep += os.sep
192 rootsep += os.sep
193 if cwd.startswith(rootsep):
193 if cwd.startswith(rootsep):
194 return cwd[len(rootsep):]
194 return cwd[len(rootsep):]
195 else:
195 else:
196 # we're outside the repo. return an absolute path.
196 # we're outside the repo. return an absolute path.
197 return cwd
197 return cwd
198
198
199 def pathto(self, f, cwd=None):
199 def pathto(self, f, cwd=None):
200 if cwd is None:
200 if cwd is None:
201 cwd = self.getcwd()
201 cwd = self.getcwd()
202 path = util.pathto(self._root, cwd, f)
202 path = util.pathto(self._root, cwd, f)
203 if self._slash:
203 if self._slash:
204 return util.normpath(path)
204 return util.normpath(path)
205 return path
205 return path
206
206
207 def __getitem__(self, key):
207 def __getitem__(self, key):
208 '''Return the current state of key (a filename) in the dirstate.
208 '''Return the current state of key (a filename) in the dirstate.
209
209
210 States are:
210 States are:
211 n normal
211 n normal
212 m needs merging
212 m needs merging
213 r marked for removal
213 r marked for removal
214 a marked for addition
214 a marked for addition
215 ? not tracked
215 ? not tracked
216 '''
216 '''
217 return self._map.get(key, ("?",))[0]
217 return self._map.get(key, ("?",))[0]
218
218
219 def __contains__(self, key):
219 def __contains__(self, key):
220 return key in self._map
220 return key in self._map
221
221
222 def __iter__(self):
222 def __iter__(self):
223 for x in sorted(self._map):
223 for x in sorted(self._map):
224 yield x
224 yield x
225
225
226 def iteritems(self):
226 def iteritems(self):
227 return self._map.iteritems()
227 return self._map.iteritems()
228
228
229 def parents(self):
229 def parents(self):
230 return [self._validate(p) for p in self._pl]
230 return [self._validate(p) for p in self._pl]
231
231
232 def p1(self):
232 def p1(self):
233 return self._validate(self._pl[0])
233 return self._validate(self._pl[0])
234
234
235 def p2(self):
235 def p2(self):
236 return self._validate(self._pl[1])
236 return self._validate(self._pl[1])
237
237
238 def branch(self):
238 def branch(self):
239 return encoding.tolocal(self._branch)
239 return encoding.tolocal(self._branch)
240
240
241 def setparents(self, p1, p2=nullid):
241 def setparents(self, p1, p2=nullid):
242 """Set dirstate parents to p1 and p2.
242 """Set dirstate parents to p1 and p2.
243
243
244 When moving from two parents to one, 'm' merged entries a
244 When moving from two parents to one, 'm' merged entries a
245 adjusted to normal and previous copy records discarded and
245 adjusted to normal and previous copy records discarded and
246 returned by the call.
246 returned by the call.
247
247
248 See localrepo.setparents()
248 See localrepo.setparents()
249 """
249 """
250 self._dirty = self._dirtypl = True
250 self._dirty = self._dirtypl = True
251 oldp2 = self._pl[1]
251 oldp2 = self._pl[1]
252 self._pl = p1, p2
252 self._pl = p1, p2
253 copies = {}
253 copies = {}
254 if oldp2 != nullid and p2 == nullid:
254 if oldp2 != nullid and p2 == nullid:
255 # Discard 'm' markers when moving away from a merge state
255 # Discard 'm' markers when moving away from a merge state
256 for f, s in self._map.iteritems():
256 for f, s in self._map.iteritems():
257 if s[0] == 'm':
257 if s[0] == 'm':
258 if f in self._copymap:
258 if f in self._copymap:
259 copies[f] = self._copymap[f]
259 copies[f] = self._copymap[f]
260 self.normallookup(f)
260 self.normallookup(f)
261 return copies
261 return copies
262
262
263 def setbranch(self, branch):
263 def setbranch(self, branch):
264 self._branch = encoding.fromlocal(branch)
264 self._branch = encoding.fromlocal(branch)
265 f = self._opener('branch', 'w', atomictemp=True)
265 f = self._opener('branch', 'w', atomictemp=True)
266 try:
266 try:
267 f.write(self._branch + '\n')
267 f.write(self._branch + '\n')
268 f.close()
268 f.close()
269
269
270 # make sure filecache has the correct stat info for _branch after
270 # make sure filecache has the correct stat info for _branch after
271 # replacing the underlying file
271 # replacing the underlying file
272 ce = self._filecache['_branch']
272 ce = self._filecache['_branch']
273 if ce:
273 if ce:
274 ce.refresh()
274 ce.refresh()
275 except: # re-raises
275 except: # re-raises
276 f.discard()
276 f.discard()
277 raise
277 raise
278
278
279 def _read(self):
279 def _read(self):
280 self._map = {}
280 self._map = {}
281 self._copymap = {}
281 self._copymap = {}
282 try:
282 try:
283 st = self._opener.read("dirstate")
283 st = self._opener.read("dirstate")
284 except IOError, err:
284 except IOError, err:
285 if err.errno != errno.ENOENT:
285 if err.errno != errno.ENOENT:
286 raise
286 raise
287 return
287 return
288 if not st:
288 if not st:
289 return
289 return
290
290
291 # Python's garbage collector triggers a GC each time a certain number
291 # Python's garbage collector triggers a GC each time a certain number
292 # of container objects (the number being defined by
292 # of container objects (the number being defined by
293 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
293 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
294 # for each file in the dirstate. The C version then immediately marks
294 # for each file in the dirstate. The C version then immediately marks
295 # them as not to be tracked by the collector. However, this has no
295 # them as not to be tracked by the collector. However, this has no
296 # effect on when GCs are triggered, only on what objects the GC looks
296 # effect on when GCs are triggered, only on what objects the GC looks
297 # into. This means that O(number of files) GCs are unavoidable.
297 # into. This means that O(number of files) GCs are unavoidable.
298 # Depending on when in the process's lifetime the dirstate is parsed,
298 # Depending on when in the process's lifetime the dirstate is parsed,
299 # this can get very expensive. As a workaround, disable GC while
299 # this can get very expensive. As a workaround, disable GC while
300 # parsing the dirstate.
300 # parsing the dirstate.
301 gcenabled = gc.isenabled()
301 gcenabled = gc.isenabled()
302 gc.disable()
302 gc.disable()
303 try:
303 try:
304 p = parsers.parse_dirstate(self._map, self._copymap, st)
304 p = parsers.parse_dirstate(self._map, self._copymap, st)
305 finally:
305 finally:
306 if gcenabled:
306 if gcenabled:
307 gc.enable()
307 gc.enable()
308 if not self._dirtypl:
308 if not self._dirtypl:
309 self._pl = p
309 self._pl = p
310
310
311 def invalidate(self):
311 def invalidate(self):
312 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
312 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
313 "_ignore"):
313 "_ignore"):
314 if a in self.__dict__:
314 if a in self.__dict__:
315 delattr(self, a)
315 delattr(self, a)
316 self._lastnormaltime = 0
316 self._lastnormaltime = 0
317 self._dirty = False
317 self._dirty = False
318
318
319 def copy(self, source, dest):
319 def copy(self, source, dest):
320 """Mark dest as a copy of source. Unmark dest if source is None."""
320 """Mark dest as a copy of source. Unmark dest if source is None."""
321 if source == dest:
321 if source == dest:
322 return
322 return
323 self._dirty = True
323 self._dirty = True
324 if source is not None:
324 if source is not None:
325 self._copymap[dest] = source
325 self._copymap[dest] = source
326 elif dest in self._copymap:
326 elif dest in self._copymap:
327 del self._copymap[dest]
327 del self._copymap[dest]
328
328
329 def copied(self, file):
329 def copied(self, file):
330 return self._copymap.get(file, None)
330 return self._copymap.get(file, None)
331
331
332 def copies(self):
332 def copies(self):
333 return self._copymap
333 return self._copymap
334
334
335 def _droppath(self, f):
335 def _droppath(self, f):
336 if self[f] not in "?r" and "_dirs" in self.__dict__:
336 if self[f] not in "?r" and "_dirs" in self.__dict__:
337 _decdirs(self._dirs, f)
337 _decdirs(self._dirs, f)
338
338
339 def _addpath(self, f, state, mode, size, mtime):
339 def _addpath(self, f, state, mode, size, mtime):
340 oldstate = self[f]
340 oldstate = self[f]
341 if state == 'a' or oldstate == 'r':
341 if state == 'a' or oldstate == 'r':
342 scmutil.checkfilename(f)
342 scmutil.checkfilename(f)
343 if f in self._dirs:
343 if f in self._dirs:
344 raise util.Abort(_('directory %r already in dirstate') % f)
344 raise util.Abort(_('directory %r already in dirstate') % f)
345 # shadows
345 # shadows
346 for d in _finddirs(f):
346 for d in _finddirs(f):
347 if d in self._dirs:
347 if d in self._dirs:
348 break
348 break
349 if d in self._map and self[d] != 'r':
349 if d in self._map and self[d] != 'r':
350 raise util.Abort(
350 raise util.Abort(
351 _('file %r in dirstate clashes with %r') % (d, f))
351 _('file %r in dirstate clashes with %r') % (d, f))
352 if oldstate in "?r" and "_dirs" in self.__dict__:
352 if oldstate in "?r" and "_dirs" in self.__dict__:
353 _incdirs(self._dirs, f)
353 _incdirs(self._dirs, f)
354 self._dirty = True
354 self._dirty = True
355 self._map[f] = (state, mode, size, mtime)
355 self._map[f] = (state, mode, size, mtime)
356
356
357 def normal(self, f):
357 def normal(self, f):
358 '''Mark a file normal and clean.'''
358 '''Mark a file normal and clean.'''
359 s = os.lstat(self._join(f))
359 s = os.lstat(self._join(f))
360 mtime = int(s.st_mtime)
360 mtime = int(s.st_mtime)
361 self._addpath(f, 'n', s.st_mode,
361 self._addpath(f, 'n', s.st_mode,
362 s.st_size & _rangemask, mtime & _rangemask)
362 s.st_size & _rangemask, mtime & _rangemask)
363 if f in self._copymap:
363 if f in self._copymap:
364 del self._copymap[f]
364 del self._copymap[f]
365 if mtime > self._lastnormaltime:
365 if mtime > self._lastnormaltime:
366 # Remember the most recent modification timeslot for status(),
366 # Remember the most recent modification timeslot for status(),
367 # to make sure we won't miss future size-preserving file content
367 # to make sure we won't miss future size-preserving file content
368 # modifications that happen within the same timeslot.
368 # modifications that happen within the same timeslot.
369 self._lastnormaltime = mtime
369 self._lastnormaltime = mtime
370
370
371 def normallookup(self, f):
371 def normallookup(self, f):
372 '''Mark a file normal, but possibly dirty.'''
372 '''Mark a file normal, but possibly dirty.'''
373 if self._pl[1] != nullid and f in self._map:
373 if self._pl[1] != nullid and f in self._map:
374 # if there is a merge going on and the file was either
374 # if there is a merge going on and the file was either
375 # in state 'm' (-1) or coming from other parent (-2) before
375 # in state 'm' (-1) or coming from other parent (-2) before
376 # being removed, restore that state.
376 # being removed, restore that state.
377 entry = self._map[f]
377 entry = self._map[f]
378 if entry[0] == 'r' and entry[2] in (-1, -2):
378 if entry[0] == 'r' and entry[2] in (-1, -2):
379 source = self._copymap.get(f)
379 source = self._copymap.get(f)
380 if entry[2] == -1:
380 if entry[2] == -1:
381 self.merge(f)
381 self.merge(f)
382 elif entry[2] == -2:
382 elif entry[2] == -2:
383 self.otherparent(f)
383 self.otherparent(f)
384 if source:
384 if source:
385 self.copy(source, f)
385 self.copy(source, f)
386 return
386 return
387 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
387 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
388 return
388 return
389 self._addpath(f, 'n', 0, -1, -1)
389 self._addpath(f, 'n', 0, -1, -1)
390 if f in self._copymap:
390 if f in self._copymap:
391 del self._copymap[f]
391 del self._copymap[f]
392
392
393 def otherparent(self, f):
393 def otherparent(self, f):
394 '''Mark as coming from the other parent, always dirty.'''
394 '''Mark as coming from the other parent, always dirty.'''
395 if self._pl[1] == nullid:
395 if self._pl[1] == nullid:
396 raise util.Abort(_("setting %r to other parent "
396 raise util.Abort(_("setting %r to other parent "
397 "only allowed in merges") % f)
397 "only allowed in merges") % f)
398 self._addpath(f, 'n', 0, -2, -1)
398 self._addpath(f, 'n', 0, -2, -1)
399 if f in self._copymap:
399 if f in self._copymap:
400 del self._copymap[f]
400 del self._copymap[f]
401
401
402 def add(self, f):
402 def add(self, f):
403 '''Mark a file added.'''
403 '''Mark a file added.'''
404 self._addpath(f, 'a', 0, -1, -1)
404 self._addpath(f, 'a', 0, -1, -1)
405 if f in self._copymap:
405 if f in self._copymap:
406 del self._copymap[f]
406 del self._copymap[f]
407
407
408 def remove(self, f):
408 def remove(self, f):
409 '''Mark a file removed.'''
409 '''Mark a file removed.'''
410 self._dirty = True
410 self._dirty = True
411 self._droppath(f)
411 self._droppath(f)
412 size = 0
412 size = 0
413 if self._pl[1] != nullid and f in self._map:
413 if self._pl[1] != nullid and f in self._map:
414 # backup the previous state
414 # backup the previous state
415 entry = self._map[f]
415 entry = self._map[f]
416 if entry[0] == 'm': # merge
416 if entry[0] == 'm': # merge
417 size = -1
417 size = -1
418 elif entry[0] == 'n' and entry[2] == -2: # other parent
418 elif entry[0] == 'n' and entry[2] == -2: # other parent
419 size = -2
419 size = -2
420 self._map[f] = ('r', 0, size, 0)
420 self._map[f] = ('r', 0, size, 0)
421 if size == 0 and f in self._copymap:
421 if size == 0 and f in self._copymap:
422 del self._copymap[f]
422 del self._copymap[f]
423
423
424 def merge(self, f):
424 def merge(self, f):
425 '''Mark a file merged.'''
425 '''Mark a file merged.'''
426 if self._pl[1] == nullid:
426 if self._pl[1] == nullid:
427 return self.normallookup(f)
427 return self.normallookup(f)
428 s = os.lstat(self._join(f))
428 s = os.lstat(self._join(f))
429 self._addpath(f, 'm', s.st_mode,
429 self._addpath(f, 'm', s.st_mode,
430 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
430 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
431 if f in self._copymap:
431 if f in self._copymap:
432 del self._copymap[f]
432 del self._copymap[f]
433
433
434 def drop(self, f):
434 def drop(self, f):
435 '''Drop a file from the dirstate'''
435 '''Drop a file from the dirstate'''
436 if f in self._map:
436 if f in self._map:
437 self._dirty = True
437 self._dirty = True
438 self._droppath(f)
438 self._droppath(f)
439 del self._map[f]
439 del self._map[f]
440
440
441 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
441 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
442 normed = util.normcase(path)
442 normed = util.normcase(path)
443 folded = self._foldmap.get(normed, None)
443 folded = self._foldmap.get(normed, None)
444 if folded is None:
444 if folded is None:
445 if isknown:
445 if isknown:
446 folded = path
446 folded = path
447 else:
447 else:
448 if exists is None:
448 if exists is None:
449 exists = os.path.lexists(os.path.join(self._root, path))
449 exists = os.path.lexists(os.path.join(self._root, path))
450 if not exists:
450 if not exists:
451 # Maybe a path component exists
451 # Maybe a path component exists
452 if not ignoremissing and '/' in path:
452 if not ignoremissing and '/' in path:
453 d, f = path.rsplit('/', 1)
453 d, f = path.rsplit('/', 1)
454 d = self._normalize(d, isknown, ignoremissing, None)
454 d = self._normalize(d, isknown, ignoremissing, None)
455 folded = d + "/" + f
455 folded = d + "/" + f
456 else:
456 else:
457 # No path components, preserve original case
457 # No path components, preserve original case
458 folded = path
458 folded = path
459 else:
459 else:
460 # recursively normalize leading directory components
460 # recursively normalize leading directory components
461 # against dirstate
461 # against dirstate
462 if '/' in normed:
462 if '/' in normed:
463 d, f = normed.rsplit('/', 1)
463 d, f = normed.rsplit('/', 1)
464 d = self._normalize(d, isknown, ignoremissing, True)
464 d = self._normalize(d, isknown, ignoremissing, True)
465 r = self._root + "/" + d
465 r = self._root + "/" + d
466 folded = d + "/" + util.fspath(f, r)
466 folded = d + "/" + util.fspath(f, r)
467 else:
467 else:
468 folded = util.fspath(normed, self._root)
468 folded = util.fspath(normed, self._root)
469 self._foldmap[normed] = folded
469 self._foldmap[normed] = folded
470
470
471 return folded
471 return folded
472
472
473 def normalize(self, path, isknown=False, ignoremissing=False):
473 def normalize(self, path, isknown=False, ignoremissing=False):
474 '''
474 '''
475 normalize the case of a pathname when on a casefolding filesystem
475 normalize the case of a pathname when on a casefolding filesystem
476
476
477 isknown specifies whether the filename came from walking the
477 isknown specifies whether the filename came from walking the
478 disk, to avoid extra filesystem access.
478 disk, to avoid extra filesystem access.
479
479
480 If ignoremissing is True, missing path are returned
480 If ignoremissing is True, missing path are returned
481 unchanged. Otherwise, we try harder to normalize possibly
481 unchanged. Otherwise, we try harder to normalize possibly
482 existing path components.
482 existing path components.
483
483
484 The normalized case is determined based on the following precedence:
484 The normalized case is determined based on the following precedence:
485
485
486 - version of name already stored in the dirstate
486 - version of name already stored in the dirstate
487 - version of name stored on disk
487 - version of name stored on disk
488 - version provided via command arguments
488 - version provided via command arguments
489 '''
489 '''
490
490
491 if self._checkcase:
491 if self._checkcase:
492 return self._normalize(path, isknown, ignoremissing)
492 return self._normalize(path, isknown, ignoremissing)
493 return path
493 return path
494
494
495 def clear(self):
495 def clear(self):
496 self._map = {}
496 self._map = {}
497 if "_dirs" in self.__dict__:
497 if "_dirs" in self.__dict__:
498 delattr(self, "_dirs")
498 delattr(self, "_dirs")
499 self._copymap = {}
499 self._copymap = {}
500 self._pl = [nullid, nullid]
500 self._pl = [nullid, nullid]
501 self._lastnormaltime = 0
501 self._lastnormaltime = 0
502 self._dirty = True
502 self._dirty = True
503
503
504 def rebuild(self, parent, allfiles, changedfiles=None):
504 def rebuild(self, parent, allfiles, changedfiles=None):
505 changedfiles = changedfiles or allfiles
505 changedfiles = changedfiles or allfiles
506 oldmap = self._map
506 oldmap = self._map
507 self.clear()
507 self.clear()
508 for f in allfiles:
508 for f in allfiles:
509 if f not in changedfiles:
509 if f not in changedfiles:
510 self._map[f] = oldmap[f]
510 self._map[f] = oldmap[f]
511 else:
511 else:
512 if 'x' in allfiles.flags(f):
512 if 'x' in allfiles.flags(f):
513 self._map[f] = ('n', 0777, -1, 0)
513 self._map[f] = ('n', 0777, -1, 0)
514 else:
514 else:
515 self._map[f] = ('n', 0666, -1, 0)
515 self._map[f] = ('n', 0666, -1, 0)
516 self._pl = (parent, nullid)
516 self._pl = (parent, nullid)
517 self._dirty = True
517 self._dirty = True
518
518
519 def write(self):
519 def write(self):
520 if not self._dirty:
520 if not self._dirty:
521 return
521 return
522 st = self._opener("dirstate", "w", atomictemp=True)
522 st = self._opener("dirstate", "w", atomictemp=True)
523
523
524 def finish(s):
524 def finish(s):
525 st.write(s)
525 st.write(s)
526 st.close()
526 st.close()
527 self._lastnormaltime = 0
527 self._lastnormaltime = 0
528 self._dirty = self._dirtypl = False
528 self._dirty = self._dirtypl = False
529
529
530 # use the modification time of the newly created temporary file as the
530 # use the modification time of the newly created temporary file as the
531 # filesystem's notion of 'now'
531 # filesystem's notion of 'now'
532 now = util.fstat(st).st_mtime
532 now = util.fstat(st).st_mtime
533 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
533 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
534
534
535 def _dirignore(self, f):
535 def _dirignore(self, f):
536 if f == '.':
536 if f == '.':
537 return False
537 return False
538 if self._ignore(f):
538 if self._ignore(f):
539 return True
539 return True
540 for p in _finddirs(f):
540 for p in _finddirs(f):
541 if self._ignore(p):
541 if self._ignore(p):
542 return True
542 return True
543 return False
543 return False
544
544
545 def walk(self, match, subrepos, unknown, ignored):
545 def walk(self, match, subrepos, unknown, ignored):
546 '''
546 '''
547 Walk recursively through the directory tree, finding all files
547 Walk recursively through the directory tree, finding all files
548 matched by match.
548 matched by match.
549
549
550 Return a dict mapping filename to stat-like object (either
550 Return a dict mapping filename to stat-like object (either
551 mercurial.osutil.stat instance or return value of os.stat()).
551 mercurial.osutil.stat instance or return value of os.stat()).
552 '''
552 '''
553
553
554 def fwarn(f, msg):
554 def fwarn(f, msg):
555 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
555 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
556 return False
556 return False
557
557
558 def badtype(mode):
558 def badtype(mode):
559 kind = _('unknown')
559 kind = _('unknown')
560 if stat.S_ISCHR(mode):
560 if stat.S_ISCHR(mode):
561 kind = _('character device')
561 kind = _('character device')
562 elif stat.S_ISBLK(mode):
562 elif stat.S_ISBLK(mode):
563 kind = _('block device')
563 kind = _('block device')
564 elif stat.S_ISFIFO(mode):
564 elif stat.S_ISFIFO(mode):
565 kind = _('fifo')
565 kind = _('fifo')
566 elif stat.S_ISSOCK(mode):
566 elif stat.S_ISSOCK(mode):
567 kind = _('socket')
567 kind = _('socket')
568 elif stat.S_ISDIR(mode):
568 elif stat.S_ISDIR(mode):
569 kind = _('directory')
569 kind = _('directory')
570 return _('unsupported file type (type is %s)') % kind
570 return _('unsupported file type (type is %s)') % kind
571
571
572 ignore = self._ignore
572 ignore = self._ignore
573 dirignore = self._dirignore
573 dirignore = self._dirignore
574 if ignored:
574 if ignored:
575 ignore = util.never
575 ignore = util.never
576 dirignore = util.never
576 dirignore = util.never
577 elif not unknown:
577 elif not unknown:
578 # if unknown and ignored are False, skip step 2
578 # if unknown and ignored are False, skip step 2
579 ignore = util.always
579 ignore = util.always
580 dirignore = util.always
580 dirignore = util.always
581
581
582 matchfn = match.matchfn
582 matchfn = match.matchfn
583 matchalways = match.always()
583 badfn = match.bad
584 badfn = match.bad
584 dmap = self._map
585 dmap = self._map
585 normpath = util.normpath
586 normpath = util.normpath
586 listdir = osutil.listdir
587 listdir = osutil.listdir
587 lstat = os.lstat
588 lstat = os.lstat
588 getkind = stat.S_IFMT
589 getkind = stat.S_IFMT
589 dirkind = stat.S_IFDIR
590 dirkind = stat.S_IFDIR
590 regkind = stat.S_IFREG
591 regkind = stat.S_IFREG
591 lnkkind = stat.S_IFLNK
592 lnkkind = stat.S_IFLNK
592 join = self._join
593 join = self._join
593 work = []
594 work = []
594 wadd = work.append
595 wadd = work.append
595
596
596 exact = skipstep3 = False
597 exact = skipstep3 = False
597 if matchfn == match.exact: # match.exact
598 if matchfn == match.exact: # match.exact
598 exact = True
599 exact = True
599 dirignore = util.always # skip step 2
600 dirignore = util.always # skip step 2
600 elif match.files() and not match.anypats(): # match.match, no patterns
601 elif match.files() and not match.anypats(): # match.match, no patterns
601 skipstep3 = True
602 skipstep3 = True
602
603
603 if not exact and self._checkcase:
604 if not exact and self._checkcase:
604 normalize = self._normalize
605 normalize = self._normalize
605 skipstep3 = False
606 skipstep3 = False
606 else:
607 else:
607 normalize = None
608 normalize = None
608
609
609 files = sorted(match.files())
610 files = sorted(match.files())
610 subrepos.sort()
611 subrepos.sort()
611 i, j = 0, 0
612 i, j = 0, 0
612 while i < len(files) and j < len(subrepos):
613 while i < len(files) and j < len(subrepos):
613 subpath = subrepos[j] + "/"
614 subpath = subrepos[j] + "/"
614 if files[i] < subpath:
615 if files[i] < subpath:
615 i += 1
616 i += 1
616 continue
617 continue
617 while i < len(files) and files[i].startswith(subpath):
618 while i < len(files) and files[i].startswith(subpath):
618 del files[i]
619 del files[i]
619 j += 1
620 j += 1
620
621
621 if not files or '.' in files:
622 if not files or '.' in files:
622 files = ['']
623 files = ['']
623 results = dict.fromkeys(subrepos)
624 results = dict.fromkeys(subrepos)
624 results['.hg'] = None
625 results['.hg'] = None
625
626
626 # step 1: find all explicit files
627 # step 1: find all explicit files
627 for ff in files:
628 for ff in files:
628 if normalize:
629 if normalize:
629 nf = normalize(normpath(ff), False, True)
630 nf = normalize(normpath(ff), False, True)
630 else:
631 else:
631 nf = normpath(ff)
632 nf = normpath(ff)
632 if nf in results:
633 if nf in results:
633 continue
634 continue
634
635
635 try:
636 try:
636 st = lstat(join(nf))
637 st = lstat(join(nf))
637 kind = getkind(st.st_mode)
638 kind = getkind(st.st_mode)
638 if kind == dirkind:
639 if kind == dirkind:
639 skipstep3 = False
640 skipstep3 = False
640 if nf in dmap:
641 if nf in dmap:
641 #file deleted on disk but still in dirstate
642 #file deleted on disk but still in dirstate
642 results[nf] = None
643 results[nf] = None
643 match.dir(nf)
644 match.dir(nf)
644 if not dirignore(nf):
645 if not dirignore(nf):
645 wadd(nf)
646 wadd(nf)
646 elif kind == regkind or kind == lnkkind:
647 elif kind == regkind or kind == lnkkind:
647 results[nf] = st
648 results[nf] = st
648 else:
649 else:
649 badfn(ff, badtype(kind))
650 badfn(ff, badtype(kind))
650 if nf in dmap:
651 if nf in dmap:
651 results[nf] = None
652 results[nf] = None
652 except OSError, inst:
653 except OSError, inst:
653 if nf in dmap: # does it exactly match a file?
654 if nf in dmap: # does it exactly match a file?
654 results[nf] = None
655 results[nf] = None
655 else: # does it match a directory?
656 else: # does it match a directory?
656 prefix = nf + "/"
657 prefix = nf + "/"
657 for fn in dmap:
658 for fn in dmap:
658 if fn.startswith(prefix):
659 if fn.startswith(prefix):
659 match.dir(nf)
660 match.dir(nf)
660 skipstep3 = False
661 skipstep3 = False
661 break
662 break
662 else:
663 else:
663 badfn(ff, inst.strerror)
664 badfn(ff, inst.strerror)
664
665
665 # step 2: visit subdirectories
666 # step 2: visit subdirectories
666 while work:
667 while work:
667 nd = work.pop()
668 nd = work.pop()
668 skip = None
669 skip = None
669 if nd == '.':
670 if nd == '.':
670 nd = ''
671 nd = ''
671 else:
672 else:
672 skip = '.hg'
673 skip = '.hg'
673 try:
674 try:
674 entries = listdir(join(nd), stat=True, skip=skip)
675 entries = listdir(join(nd), stat=True, skip=skip)
675 except OSError, inst:
676 except OSError, inst:
676 if inst.errno in (errno.EACCES, errno.ENOENT):
677 if inst.errno in (errno.EACCES, errno.ENOENT):
677 fwarn(nd, inst.strerror)
678 fwarn(nd, inst.strerror)
678 continue
679 continue
679 raise
680 raise
680 for f, kind, st in entries:
681 for f, kind, st in entries:
681 if normalize:
682 if normalize:
682 nf = normalize(nd and (nd + "/" + f) or f, True, True)
683 nf = normalize(nd and (nd + "/" + f) or f, True, True)
683 else:
684 else:
684 nf = nd and (nd + "/" + f) or f
685 nf = nd and (nd + "/" + f) or f
685 if nf not in results:
686 if nf not in results:
686 if kind == dirkind:
687 if kind == dirkind:
687 if not ignore(nf):
688 if not ignore(nf):
688 match.dir(nf)
689 match.dir(nf)
689 wadd(nf)
690 wadd(nf)
690 if nf in dmap and matchfn(nf):
691 if nf in dmap and (matchalways or matchfn(nf)):
691 results[nf] = None
692 results[nf] = None
692 elif kind == regkind or kind == lnkkind:
693 elif kind == regkind or kind == lnkkind:
693 if nf in dmap:
694 if nf in dmap:
694 if matchfn(nf):
695 if matchalways or matchfn(nf):
695 results[nf] = st
696 results[nf] = st
696 elif matchfn(nf) and not ignore(nf):
697 elif (matchalways or matchfn(nf)) and not ignore(nf):
697 results[nf] = st
698 results[nf] = st
698 elif nf in dmap and matchfn(nf):
699 elif nf in dmap and (matchalways or matchfn(nf)):
699 results[nf] = None
700 results[nf] = None
700
701
701 for s in subrepos:
702 for s in subrepos:
702 del results[s]
703 del results[s]
703 del results['.hg']
704 del results['.hg']
704
705
705 # step 3: report unseen items in the dmap hash
706 # step 3: report unseen items in the dmap hash
706 if not skipstep3 and not exact:
707 if not skipstep3 and not exact:
707 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
708 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
708 if unknown:
709 if unknown:
709 # unknown == True means we walked the full directory tree above.
710 # unknown == True means we walked the full directory tree above.
710 # So if a file is not seen it was either a) not matching matchfn
711 # So if a file is not seen it was either a) not matching matchfn
711 # b) ignored, c) missing, or d) under a symlink directory.
712 # b) ignored, c) missing, or d) under a symlink directory.
712 audit_path = scmutil.pathauditor(self._root)
713 audit_path = scmutil.pathauditor(self._root)
713
714
714 for nf in iter(visit):
715 for nf in iter(visit):
715 # Report ignored items in the dmap as long as they are not
716 # Report ignored items in the dmap as long as they are not
716 # under a symlink directory.
717 # under a symlink directory.
717 if ignore(nf) and audit_path.check(nf):
718 if ignore(nf) and audit_path.check(nf):
718 try:
719 try:
719 results[nf] = lstat(join(nf))
720 results[nf] = lstat(join(nf))
720 except OSError:
721 except OSError:
721 # file doesn't exist
722 # file doesn't exist
722 results[nf] = None
723 results[nf] = None
723 else:
724 else:
724 # It's either missing or under a symlink directory
725 # It's either missing or under a symlink directory
725 results[nf] = None
726 results[nf] = None
726 else:
727 else:
727 # We may not have walked the full directory tree above,
728 # We may not have walked the full directory tree above,
728 # so stat everything we missed.
729 # so stat everything we missed.
729 nf = iter(visit).next
730 nf = iter(visit).next
730 for st in util.statfiles([join(i) for i in visit]):
731 for st in util.statfiles([join(i) for i in visit]):
731 results[nf()] = st
732 results[nf()] = st
732 return results
733 return results
733
734
734 def status(self, match, subrepos, ignored, clean, unknown):
735 def status(self, match, subrepos, ignored, clean, unknown):
735 '''Determine the status of the working copy relative to the
736 '''Determine the status of the working copy relative to the
736 dirstate and return a tuple of lists (unsure, modified, added,
737 dirstate and return a tuple of lists (unsure, modified, added,
737 removed, deleted, unknown, ignored, clean), where:
738 removed, deleted, unknown, ignored, clean), where:
738
739
739 unsure:
740 unsure:
740 files that might have been modified since the dirstate was
741 files that might have been modified since the dirstate was
741 written, but need to be read to be sure (size is the same
742 written, but need to be read to be sure (size is the same
742 but mtime differs)
743 but mtime differs)
743 modified:
744 modified:
744 files that have definitely been modified since the dirstate
745 files that have definitely been modified since the dirstate
745 was written (different size or mode)
746 was written (different size or mode)
746 added:
747 added:
747 files that have been explicitly added with hg add
748 files that have been explicitly added with hg add
748 removed:
749 removed:
749 files that have been explicitly removed with hg remove
750 files that have been explicitly removed with hg remove
750 deleted:
751 deleted:
751 files that have been deleted through other means ("missing")
752 files that have been deleted through other means ("missing")
752 unknown:
753 unknown:
753 files not in the dirstate that are not ignored
754 files not in the dirstate that are not ignored
754 ignored:
755 ignored:
755 files not in the dirstate that are ignored
756 files not in the dirstate that are ignored
756 (by _dirignore())
757 (by _dirignore())
757 clean:
758 clean:
758 files that have definitely not been modified since the
759 files that have definitely not been modified since the
759 dirstate was written
760 dirstate was written
760 '''
761 '''
761 listignored, listclean, listunknown = ignored, clean, unknown
762 listignored, listclean, listunknown = ignored, clean, unknown
762 lookup, modified, added, unknown, ignored = [], [], [], [], []
763 lookup, modified, added, unknown, ignored = [], [], [], [], []
763 removed, deleted, clean = [], [], []
764 removed, deleted, clean = [], [], []
764
765
765 dmap = self._map
766 dmap = self._map
766 ladd = lookup.append # aka "unsure"
767 ladd = lookup.append # aka "unsure"
767 madd = modified.append
768 madd = modified.append
768 aadd = added.append
769 aadd = added.append
769 uadd = unknown.append
770 uadd = unknown.append
770 iadd = ignored.append
771 iadd = ignored.append
771 radd = removed.append
772 radd = removed.append
772 dadd = deleted.append
773 dadd = deleted.append
773 cadd = clean.append
774 cadd = clean.append
774 mexact = match.exact
775 mexact = match.exact
775 dirignore = self._dirignore
776 dirignore = self._dirignore
776 checkexec = self._checkexec
777 checkexec = self._checkexec
777 checklink = self._checklink
778 checklink = self._checklink
778 copymap = self._copymap
779 copymap = self._copymap
779 lastnormaltime = self._lastnormaltime
780 lastnormaltime = self._lastnormaltime
780
781
781 lnkkind = stat.S_IFLNK
782 lnkkind = stat.S_IFLNK
782
783
783 for fn, st in self.walk(match, subrepos, listunknown,
784 for fn, st in self.walk(match, subrepos, listunknown,
784 listignored).iteritems():
785 listignored).iteritems():
785 if fn not in dmap:
786 if fn not in dmap:
786 if (listignored or mexact(fn)) and dirignore(fn):
787 if (listignored or mexact(fn)) and dirignore(fn):
787 if listignored:
788 if listignored:
788 iadd(fn)
789 iadd(fn)
789 elif listunknown:
790 elif listunknown:
790 uadd(fn)
791 uadd(fn)
791 continue
792 continue
792
793
793 state, mode, size, time = dmap[fn]
794 state, mode, size, time = dmap[fn]
794
795
795 if not st and state in "nma":
796 if not st and state in "nma":
796 dadd(fn)
797 dadd(fn)
797 elif state == 'n':
798 elif state == 'n':
798 # The "mode & lnkkind != lnkkind or self._checklink"
799 # The "mode & lnkkind != lnkkind or self._checklink"
799 # lines are an expansion of "islink => checklink"
800 # lines are an expansion of "islink => checklink"
800 # where islink means "is this a link?" and checklink
801 # where islink means "is this a link?" and checklink
801 # means "can we check links?".
802 # means "can we check links?".
802 mtime = int(st.st_mtime)
803 mtime = int(st.st_mtime)
803 if (size >= 0 and
804 if (size >= 0 and
804 ((size != st.st_size and size != st.st_size & _rangemask)
805 ((size != st.st_size and size != st.st_size & _rangemask)
805 or ((mode ^ st.st_mode) & 0100 and checkexec))
806 or ((mode ^ st.st_mode) & 0100 and checkexec))
806 and (mode & lnkkind != lnkkind or checklink)
807 and (mode & lnkkind != lnkkind or checklink)
807 or size == -2 # other parent
808 or size == -2 # other parent
808 or fn in copymap):
809 or fn in copymap):
809 madd(fn)
810 madd(fn)
810 elif ((time != mtime and time != mtime & _rangemask)
811 elif ((time != mtime and time != mtime & _rangemask)
811 and (mode & lnkkind != lnkkind or checklink)):
812 and (mode & lnkkind != lnkkind or checklink)):
812 ladd(fn)
813 ladd(fn)
813 elif mtime == lastnormaltime:
814 elif mtime == lastnormaltime:
814 # fn may have been changed in the same timeslot without
815 # fn may have been changed in the same timeslot without
815 # changing its size. This can happen if we quickly do
816 # changing its size. This can happen if we quickly do
816 # multiple commits in a single transaction.
817 # multiple commits in a single transaction.
817 # Force lookup, so we don't miss such a racy file change.
818 # Force lookup, so we don't miss such a racy file change.
818 ladd(fn)
819 ladd(fn)
819 elif listclean:
820 elif listclean:
820 cadd(fn)
821 cadd(fn)
821 elif state == 'm':
822 elif state == 'm':
822 madd(fn)
823 madd(fn)
823 elif state == 'a':
824 elif state == 'a':
824 aadd(fn)
825 aadd(fn)
825 elif state == 'r':
826 elif state == 'r':
826 radd(fn)
827 radd(fn)
827
828
828 return (lookup, modified, added, removed, deleted, unknown, ignored,
829 return (lookup, modified, added, removed, deleted, unknown, ignored,
829 clean)
830 clean)
General Comments 0
You need to be logged in to leave comments. Login now