##// END OF EJS Templates
copies: re-include root directory in directory rename detection (issue3511)
Matt Mackall -
r17055:8b7cd9a9 default
parent child Browse files
Show More
@@ -1,1279 +1,1279
1 # context.py - changeset and file context objects for mercurial
1 # context.py - changeset and file context objects for mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 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
7
8 from node import nullid, nullrev, short, hex, bin
8 from node import nullid, nullrev, short, hex, bin
9 from i18n import _
9 from i18n import _
10 import ancestor, mdiff, error, util, scmutil, subrepo, patch, encoding, phases
10 import ancestor, mdiff, error, util, scmutil, subrepo, patch, encoding, phases
11 import copies
11 import copies
12 import match as matchmod
12 import match as matchmod
13 import os, errno, stat
13 import os, errno, stat
14
14
15 propertycache = util.propertycache
15 propertycache = util.propertycache
16
16
17 class changectx(object):
17 class changectx(object):
18 """A changecontext object makes access to data related to a particular
18 """A changecontext object makes access to data related to a particular
19 changeset convenient."""
19 changeset convenient."""
20 def __init__(self, repo, changeid=''):
20 def __init__(self, repo, changeid=''):
21 """changeid is a revision number, node, or tag"""
21 """changeid is a revision number, node, or tag"""
22 if changeid == '':
22 if changeid == '':
23 changeid = '.'
23 changeid = '.'
24 self._repo = repo
24 self._repo = repo
25
25
26 if isinstance(changeid, int):
26 if isinstance(changeid, int):
27 self._rev = changeid
27 self._rev = changeid
28 self._node = repo.changelog.node(changeid)
28 self._node = repo.changelog.node(changeid)
29 return
29 return
30 if isinstance(changeid, long):
30 if isinstance(changeid, long):
31 changeid = str(changeid)
31 changeid = str(changeid)
32 if changeid == '.':
32 if changeid == '.':
33 self._node = repo.dirstate.p1()
33 self._node = repo.dirstate.p1()
34 self._rev = repo.changelog.rev(self._node)
34 self._rev = repo.changelog.rev(self._node)
35 return
35 return
36 if changeid == 'null':
36 if changeid == 'null':
37 self._node = nullid
37 self._node = nullid
38 self._rev = nullrev
38 self._rev = nullrev
39 return
39 return
40 if changeid == 'tip':
40 if changeid == 'tip':
41 self._rev = len(repo.changelog) - 1
41 self._rev = len(repo.changelog) - 1
42 self._node = repo.changelog.node(self._rev)
42 self._node = repo.changelog.node(self._rev)
43 return
43 return
44 if len(changeid) == 20:
44 if len(changeid) == 20:
45 try:
45 try:
46 self._node = changeid
46 self._node = changeid
47 self._rev = repo.changelog.rev(changeid)
47 self._rev = repo.changelog.rev(changeid)
48 return
48 return
49 except LookupError:
49 except LookupError:
50 pass
50 pass
51
51
52 try:
52 try:
53 r = int(changeid)
53 r = int(changeid)
54 if str(r) != changeid:
54 if str(r) != changeid:
55 raise ValueError
55 raise ValueError
56 l = len(repo.changelog)
56 l = len(repo.changelog)
57 if r < 0:
57 if r < 0:
58 r += l
58 r += l
59 if r < 0 or r >= l:
59 if r < 0 or r >= l:
60 raise ValueError
60 raise ValueError
61 self._rev = r
61 self._rev = r
62 self._node = repo.changelog.node(r)
62 self._node = repo.changelog.node(r)
63 return
63 return
64 except (ValueError, OverflowError):
64 except (ValueError, OverflowError):
65 pass
65 pass
66
66
67 if len(changeid) == 40:
67 if len(changeid) == 40:
68 try:
68 try:
69 self._node = bin(changeid)
69 self._node = bin(changeid)
70 self._rev = repo.changelog.rev(self._node)
70 self._rev = repo.changelog.rev(self._node)
71 return
71 return
72 except (TypeError, LookupError):
72 except (TypeError, LookupError):
73 pass
73 pass
74
74
75 if changeid in repo._bookmarks:
75 if changeid in repo._bookmarks:
76 self._node = repo._bookmarks[changeid]
76 self._node = repo._bookmarks[changeid]
77 self._rev = repo.changelog.rev(self._node)
77 self._rev = repo.changelog.rev(self._node)
78 return
78 return
79 if changeid in repo._tagscache.tags:
79 if changeid in repo._tagscache.tags:
80 self._node = repo._tagscache.tags[changeid]
80 self._node = repo._tagscache.tags[changeid]
81 self._rev = repo.changelog.rev(self._node)
81 self._rev = repo.changelog.rev(self._node)
82 return
82 return
83 try:
83 try:
84 self._node = repo.branchtip(changeid)
84 self._node = repo.branchtip(changeid)
85 self._rev = repo.changelog.rev(self._node)
85 self._rev = repo.changelog.rev(self._node)
86 return
86 return
87 except error.RepoLookupError:
87 except error.RepoLookupError:
88 pass
88 pass
89
89
90 self._node = repo.changelog._partialmatch(changeid)
90 self._node = repo.changelog._partialmatch(changeid)
91 if self._node is not None:
91 if self._node is not None:
92 self._rev = repo.changelog.rev(self._node)
92 self._rev = repo.changelog.rev(self._node)
93 return
93 return
94
94
95 # lookup failed
95 # lookup failed
96 # check if it might have come from damaged dirstate
96 # check if it might have come from damaged dirstate
97 if changeid in repo.dirstate.parents():
97 if changeid in repo.dirstate.parents():
98 raise error.Abort(_("working directory has unknown parent '%s'!")
98 raise error.Abort(_("working directory has unknown parent '%s'!")
99 % short(changeid))
99 % short(changeid))
100 try:
100 try:
101 if len(changeid) == 20:
101 if len(changeid) == 20:
102 changeid = hex(changeid)
102 changeid = hex(changeid)
103 except TypeError:
103 except TypeError:
104 pass
104 pass
105 raise error.RepoLookupError(
105 raise error.RepoLookupError(
106 _("unknown revision '%s'") % changeid)
106 _("unknown revision '%s'") % changeid)
107
107
108 def __str__(self):
108 def __str__(self):
109 return short(self.node())
109 return short(self.node())
110
110
111 def __int__(self):
111 def __int__(self):
112 return self.rev()
112 return self.rev()
113
113
114 def __repr__(self):
114 def __repr__(self):
115 return "<changectx %s>" % str(self)
115 return "<changectx %s>" % str(self)
116
116
117 def __hash__(self):
117 def __hash__(self):
118 try:
118 try:
119 return hash(self._rev)
119 return hash(self._rev)
120 except AttributeError:
120 except AttributeError:
121 return id(self)
121 return id(self)
122
122
123 def __eq__(self, other):
123 def __eq__(self, other):
124 try:
124 try:
125 return self._rev == other._rev
125 return self._rev == other._rev
126 except AttributeError:
126 except AttributeError:
127 return False
127 return False
128
128
129 def __ne__(self, other):
129 def __ne__(self, other):
130 return not (self == other)
130 return not (self == other)
131
131
132 def __nonzero__(self):
132 def __nonzero__(self):
133 return self._rev != nullrev
133 return self._rev != nullrev
134
134
135 @propertycache
135 @propertycache
136 def _changeset(self):
136 def _changeset(self):
137 return self._repo.changelog.read(self.rev())
137 return self._repo.changelog.read(self.rev())
138
138
139 @propertycache
139 @propertycache
140 def _manifest(self):
140 def _manifest(self):
141 return self._repo.manifest.read(self._changeset[0])
141 return self._repo.manifest.read(self._changeset[0])
142
142
143 @propertycache
143 @propertycache
144 def _manifestdelta(self):
144 def _manifestdelta(self):
145 return self._repo.manifest.readdelta(self._changeset[0])
145 return self._repo.manifest.readdelta(self._changeset[0])
146
146
147 @propertycache
147 @propertycache
148 def _parents(self):
148 def _parents(self):
149 p = self._repo.changelog.parentrevs(self._rev)
149 p = self._repo.changelog.parentrevs(self._rev)
150 if p[1] == nullrev:
150 if p[1] == nullrev:
151 p = p[:-1]
151 p = p[:-1]
152 return [changectx(self._repo, x) for x in p]
152 return [changectx(self._repo, x) for x in p]
153
153
154 @propertycache
154 @propertycache
155 def substate(self):
155 def substate(self):
156 return subrepo.state(self, self._repo.ui)
156 return subrepo.state(self, self._repo.ui)
157
157
158 def __contains__(self, key):
158 def __contains__(self, key):
159 return key in self._manifest
159 return key in self._manifest
160
160
161 def __getitem__(self, key):
161 def __getitem__(self, key):
162 return self.filectx(key)
162 return self.filectx(key)
163
163
164 def __iter__(self):
164 def __iter__(self):
165 for f in sorted(self._manifest):
165 for f in sorted(self._manifest):
166 yield f
166 yield f
167
167
168 def changeset(self):
168 def changeset(self):
169 return self._changeset
169 return self._changeset
170 def manifest(self):
170 def manifest(self):
171 return self._manifest
171 return self._manifest
172 def manifestnode(self):
172 def manifestnode(self):
173 return self._changeset[0]
173 return self._changeset[0]
174
174
175 def rev(self):
175 def rev(self):
176 return self._rev
176 return self._rev
177 def node(self):
177 def node(self):
178 return self._node
178 return self._node
179 def hex(self):
179 def hex(self):
180 return hex(self._node)
180 return hex(self._node)
181 def user(self):
181 def user(self):
182 return self._changeset[1]
182 return self._changeset[1]
183 def date(self):
183 def date(self):
184 return self._changeset[2]
184 return self._changeset[2]
185 def files(self):
185 def files(self):
186 return self._changeset[3]
186 return self._changeset[3]
187 def description(self):
187 def description(self):
188 return self._changeset[4]
188 return self._changeset[4]
189 def branch(self):
189 def branch(self):
190 return encoding.tolocal(self._changeset[5].get("branch"))
190 return encoding.tolocal(self._changeset[5].get("branch"))
191 def closesbranch(self):
191 def closesbranch(self):
192 return 'close' in self._changeset[5]
192 return 'close' in self._changeset[5]
193 def extra(self):
193 def extra(self):
194 return self._changeset[5]
194 return self._changeset[5]
195 def tags(self):
195 def tags(self):
196 return self._repo.nodetags(self._node)
196 return self._repo.nodetags(self._node)
197 def bookmarks(self):
197 def bookmarks(self):
198 return self._repo.nodebookmarks(self._node)
198 return self._repo.nodebookmarks(self._node)
199 def phase(self):
199 def phase(self):
200 return self._repo._phasecache.phase(self._repo, self._rev)
200 return self._repo._phasecache.phase(self._repo, self._rev)
201 def phasestr(self):
201 def phasestr(self):
202 return phases.phasenames[self.phase()]
202 return phases.phasenames[self.phase()]
203 def mutable(self):
203 def mutable(self):
204 return self.phase() > phases.public
204 return self.phase() > phases.public
205 def hidden(self):
205 def hidden(self):
206 return self._rev in self._repo.changelog.hiddenrevs
206 return self._rev in self._repo.changelog.hiddenrevs
207
207
208 def parents(self):
208 def parents(self):
209 """return contexts for each parent changeset"""
209 """return contexts for each parent changeset"""
210 return self._parents
210 return self._parents
211
211
212 def p1(self):
212 def p1(self):
213 return self._parents[0]
213 return self._parents[0]
214
214
215 def p2(self):
215 def p2(self):
216 if len(self._parents) == 2:
216 if len(self._parents) == 2:
217 return self._parents[1]
217 return self._parents[1]
218 return changectx(self._repo, -1)
218 return changectx(self._repo, -1)
219
219
220 def children(self):
220 def children(self):
221 """return contexts for each child changeset"""
221 """return contexts for each child changeset"""
222 c = self._repo.changelog.children(self._node)
222 c = self._repo.changelog.children(self._node)
223 return [changectx(self._repo, x) for x in c]
223 return [changectx(self._repo, x) for x in c]
224
224
225 def ancestors(self):
225 def ancestors(self):
226 for a in self._repo.changelog.ancestors([self._rev]):
226 for a in self._repo.changelog.ancestors([self._rev]):
227 yield changectx(self._repo, a)
227 yield changectx(self._repo, a)
228
228
229 def descendants(self):
229 def descendants(self):
230 for d in self._repo.changelog.descendants([self._rev]):
230 for d in self._repo.changelog.descendants([self._rev]):
231 yield changectx(self._repo, d)
231 yield changectx(self._repo, d)
232
232
233 def _fileinfo(self, path):
233 def _fileinfo(self, path):
234 if '_manifest' in self.__dict__:
234 if '_manifest' in self.__dict__:
235 try:
235 try:
236 return self._manifest[path], self._manifest.flags(path)
236 return self._manifest[path], self._manifest.flags(path)
237 except KeyError:
237 except KeyError:
238 raise error.LookupError(self._node, path,
238 raise error.LookupError(self._node, path,
239 _('not found in manifest'))
239 _('not found in manifest'))
240 if '_manifestdelta' in self.__dict__ or path in self.files():
240 if '_manifestdelta' in self.__dict__ or path in self.files():
241 if path in self._manifestdelta:
241 if path in self._manifestdelta:
242 return (self._manifestdelta[path],
242 return (self._manifestdelta[path],
243 self._manifestdelta.flags(path))
243 self._manifestdelta.flags(path))
244 node, flag = self._repo.manifest.find(self._changeset[0], path)
244 node, flag = self._repo.manifest.find(self._changeset[0], path)
245 if not node:
245 if not node:
246 raise error.LookupError(self._node, path,
246 raise error.LookupError(self._node, path,
247 _('not found in manifest'))
247 _('not found in manifest'))
248
248
249 return node, flag
249 return node, flag
250
250
251 def filenode(self, path):
251 def filenode(self, path):
252 return self._fileinfo(path)[0]
252 return self._fileinfo(path)[0]
253
253
254 def flags(self, path):
254 def flags(self, path):
255 try:
255 try:
256 return self._fileinfo(path)[1]
256 return self._fileinfo(path)[1]
257 except error.LookupError:
257 except error.LookupError:
258 return ''
258 return ''
259
259
260 def filectx(self, path, fileid=None, filelog=None):
260 def filectx(self, path, fileid=None, filelog=None):
261 """get a file context from this changeset"""
261 """get a file context from this changeset"""
262 if fileid is None:
262 if fileid is None:
263 fileid = self.filenode(path)
263 fileid = self.filenode(path)
264 return filectx(self._repo, path, fileid=fileid,
264 return filectx(self._repo, path, fileid=fileid,
265 changectx=self, filelog=filelog)
265 changectx=self, filelog=filelog)
266
266
267 def ancestor(self, c2):
267 def ancestor(self, c2):
268 """
268 """
269 return the ancestor context of self and c2
269 return the ancestor context of self and c2
270 """
270 """
271 # deal with workingctxs
271 # deal with workingctxs
272 n2 = c2._node
272 n2 = c2._node
273 if n2 is None:
273 if n2 is None:
274 n2 = c2._parents[0]._node
274 n2 = c2._parents[0]._node
275 n = self._repo.changelog.ancestor(self._node, n2)
275 n = self._repo.changelog.ancestor(self._node, n2)
276 return changectx(self._repo, n)
276 return changectx(self._repo, n)
277
277
278 def walk(self, match):
278 def walk(self, match):
279 fset = set(match.files())
279 fset = set(match.files())
280 # for dirstate.walk, files=['.'] means "walk the whole tree".
280 # for dirstate.walk, files=['.'] means "walk the whole tree".
281 # follow that here, too
281 # follow that here, too
282 fset.discard('.')
282 fset.discard('.')
283 for fn in self:
283 for fn in self:
284 if fn in fset:
284 if fn in fset:
285 # specified pattern is the exact name
285 # specified pattern is the exact name
286 fset.remove(fn)
286 fset.remove(fn)
287 if match(fn):
287 if match(fn):
288 yield fn
288 yield fn
289 for fn in sorted(fset):
289 for fn in sorted(fset):
290 if fn in self._dirs:
290 if fn in self._dirs:
291 # specified pattern is a directory
291 # specified pattern is a directory
292 continue
292 continue
293 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
293 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
294 yield fn
294 yield fn
295
295
296 def sub(self, path):
296 def sub(self, path):
297 return subrepo.subrepo(self, path)
297 return subrepo.subrepo(self, path)
298
298
299 def match(self, pats=[], include=None, exclude=None, default='glob'):
299 def match(self, pats=[], include=None, exclude=None, default='glob'):
300 r = self._repo
300 r = self._repo
301 return matchmod.match(r.root, r.getcwd(), pats,
301 return matchmod.match(r.root, r.getcwd(), pats,
302 include, exclude, default,
302 include, exclude, default,
303 auditor=r.auditor, ctx=self)
303 auditor=r.auditor, ctx=self)
304
304
305 def diff(self, ctx2=None, match=None, **opts):
305 def diff(self, ctx2=None, match=None, **opts):
306 """Returns a diff generator for the given contexts and matcher"""
306 """Returns a diff generator for the given contexts and matcher"""
307 if ctx2 is None:
307 if ctx2 is None:
308 ctx2 = self.p1()
308 ctx2 = self.p1()
309 if ctx2 is not None and not isinstance(ctx2, changectx):
309 if ctx2 is not None and not isinstance(ctx2, changectx):
310 ctx2 = self._repo[ctx2]
310 ctx2 = self._repo[ctx2]
311 diffopts = patch.diffopts(self._repo.ui, opts)
311 diffopts = patch.diffopts(self._repo.ui, opts)
312 return patch.diff(self._repo, ctx2.node(), self.node(),
312 return patch.diff(self._repo, ctx2.node(), self.node(),
313 match=match, opts=diffopts)
313 match=match, opts=diffopts)
314
314
315 @propertycache
315 @propertycache
316 def _dirs(self):
316 def _dirs(self):
317 dirs = set()
317 dirs = set()
318 for f in self._manifest:
318 for f in self._manifest:
319 pos = f.rfind('/')
319 pos = f.rfind('/')
320 while pos != -1:
320 while pos != -1:
321 f = f[:pos]
321 f = f[:pos]
322 if f in dirs:
322 if f in dirs:
323 break # dirs already contains this and above
323 break # dirs already contains this and above
324 dirs.add(f)
324 dirs.add(f)
325 pos = f.rfind('/')
325 pos = f.rfind('/')
326 return dirs
326 return dirs
327
327
328 def dirs(self):
328 def dirs(self):
329 return self._dirs
329 return self._dirs
330
330
331 class filectx(object):
331 class filectx(object):
332 """A filecontext object makes access to data related to a particular
332 """A filecontext object makes access to data related to a particular
333 filerevision convenient."""
333 filerevision convenient."""
334 def __init__(self, repo, path, changeid=None, fileid=None,
334 def __init__(self, repo, path, changeid=None, fileid=None,
335 filelog=None, changectx=None):
335 filelog=None, changectx=None):
336 """changeid can be a changeset revision, node, or tag.
336 """changeid can be a changeset revision, node, or tag.
337 fileid can be a file revision or node."""
337 fileid can be a file revision or node."""
338 self._repo = repo
338 self._repo = repo
339 self._path = path
339 self._path = path
340
340
341 assert (changeid is not None
341 assert (changeid is not None
342 or fileid is not None
342 or fileid is not None
343 or changectx is not None), \
343 or changectx is not None), \
344 ("bad args: changeid=%r, fileid=%r, changectx=%r"
344 ("bad args: changeid=%r, fileid=%r, changectx=%r"
345 % (changeid, fileid, changectx))
345 % (changeid, fileid, changectx))
346
346
347 if filelog:
347 if filelog:
348 self._filelog = filelog
348 self._filelog = filelog
349
349
350 if changeid is not None:
350 if changeid is not None:
351 self._changeid = changeid
351 self._changeid = changeid
352 if changectx is not None:
352 if changectx is not None:
353 self._changectx = changectx
353 self._changectx = changectx
354 if fileid is not None:
354 if fileid is not None:
355 self._fileid = fileid
355 self._fileid = fileid
356
356
357 @propertycache
357 @propertycache
358 def _changectx(self):
358 def _changectx(self):
359 return changectx(self._repo, self._changeid)
359 return changectx(self._repo, self._changeid)
360
360
361 @propertycache
361 @propertycache
362 def _filelog(self):
362 def _filelog(self):
363 return self._repo.file(self._path)
363 return self._repo.file(self._path)
364
364
365 @propertycache
365 @propertycache
366 def _changeid(self):
366 def _changeid(self):
367 if '_changectx' in self.__dict__:
367 if '_changectx' in self.__dict__:
368 return self._changectx.rev()
368 return self._changectx.rev()
369 else:
369 else:
370 return self._filelog.linkrev(self._filerev)
370 return self._filelog.linkrev(self._filerev)
371
371
372 @propertycache
372 @propertycache
373 def _filenode(self):
373 def _filenode(self):
374 if '_fileid' in self.__dict__:
374 if '_fileid' in self.__dict__:
375 return self._filelog.lookup(self._fileid)
375 return self._filelog.lookup(self._fileid)
376 else:
376 else:
377 return self._changectx.filenode(self._path)
377 return self._changectx.filenode(self._path)
378
378
379 @propertycache
379 @propertycache
380 def _filerev(self):
380 def _filerev(self):
381 return self._filelog.rev(self._filenode)
381 return self._filelog.rev(self._filenode)
382
382
383 @propertycache
383 @propertycache
384 def _repopath(self):
384 def _repopath(self):
385 return self._path
385 return self._path
386
386
387 def __nonzero__(self):
387 def __nonzero__(self):
388 try:
388 try:
389 self._filenode
389 self._filenode
390 return True
390 return True
391 except error.LookupError:
391 except error.LookupError:
392 # file is missing
392 # file is missing
393 return False
393 return False
394
394
395 def __str__(self):
395 def __str__(self):
396 return "%s@%s" % (self.path(), short(self.node()))
396 return "%s@%s" % (self.path(), short(self.node()))
397
397
398 def __repr__(self):
398 def __repr__(self):
399 return "<filectx %s>" % str(self)
399 return "<filectx %s>" % str(self)
400
400
401 def __hash__(self):
401 def __hash__(self):
402 try:
402 try:
403 return hash((self._path, self._filenode))
403 return hash((self._path, self._filenode))
404 except AttributeError:
404 except AttributeError:
405 return id(self)
405 return id(self)
406
406
407 def __eq__(self, other):
407 def __eq__(self, other):
408 try:
408 try:
409 return (self._path == other._path
409 return (self._path == other._path
410 and self._filenode == other._filenode)
410 and self._filenode == other._filenode)
411 except AttributeError:
411 except AttributeError:
412 return False
412 return False
413
413
414 def __ne__(self, other):
414 def __ne__(self, other):
415 return not (self == other)
415 return not (self == other)
416
416
417 def filectx(self, fileid):
417 def filectx(self, fileid):
418 '''opens an arbitrary revision of the file without
418 '''opens an arbitrary revision of the file without
419 opening a new filelog'''
419 opening a new filelog'''
420 return filectx(self._repo, self._path, fileid=fileid,
420 return filectx(self._repo, self._path, fileid=fileid,
421 filelog=self._filelog)
421 filelog=self._filelog)
422
422
423 def filerev(self):
423 def filerev(self):
424 return self._filerev
424 return self._filerev
425 def filenode(self):
425 def filenode(self):
426 return self._filenode
426 return self._filenode
427 def flags(self):
427 def flags(self):
428 return self._changectx.flags(self._path)
428 return self._changectx.flags(self._path)
429 def filelog(self):
429 def filelog(self):
430 return self._filelog
430 return self._filelog
431
431
432 def rev(self):
432 def rev(self):
433 if '_changectx' in self.__dict__:
433 if '_changectx' in self.__dict__:
434 return self._changectx.rev()
434 return self._changectx.rev()
435 if '_changeid' in self.__dict__:
435 if '_changeid' in self.__dict__:
436 return self._changectx.rev()
436 return self._changectx.rev()
437 return self._filelog.linkrev(self._filerev)
437 return self._filelog.linkrev(self._filerev)
438
438
439 def linkrev(self):
439 def linkrev(self):
440 return self._filelog.linkrev(self._filerev)
440 return self._filelog.linkrev(self._filerev)
441 def node(self):
441 def node(self):
442 return self._changectx.node()
442 return self._changectx.node()
443 def hex(self):
443 def hex(self):
444 return hex(self.node())
444 return hex(self.node())
445 def user(self):
445 def user(self):
446 return self._changectx.user()
446 return self._changectx.user()
447 def date(self):
447 def date(self):
448 return self._changectx.date()
448 return self._changectx.date()
449 def files(self):
449 def files(self):
450 return self._changectx.files()
450 return self._changectx.files()
451 def description(self):
451 def description(self):
452 return self._changectx.description()
452 return self._changectx.description()
453 def branch(self):
453 def branch(self):
454 return self._changectx.branch()
454 return self._changectx.branch()
455 def extra(self):
455 def extra(self):
456 return self._changectx.extra()
456 return self._changectx.extra()
457 def manifest(self):
457 def manifest(self):
458 return self._changectx.manifest()
458 return self._changectx.manifest()
459 def changectx(self):
459 def changectx(self):
460 return self._changectx
460 return self._changectx
461
461
462 def data(self):
462 def data(self):
463 return self._filelog.read(self._filenode)
463 return self._filelog.read(self._filenode)
464 def path(self):
464 def path(self):
465 return self._path
465 return self._path
466 def size(self):
466 def size(self):
467 return self._filelog.size(self._filerev)
467 return self._filelog.size(self._filerev)
468
468
469 def isbinary(self):
469 def isbinary(self):
470 try:
470 try:
471 return util.binary(self.data())
471 return util.binary(self.data())
472 except IOError:
472 except IOError:
473 return False
473 return False
474
474
475 def cmp(self, fctx):
475 def cmp(self, fctx):
476 """compare with other file context
476 """compare with other file context
477
477
478 returns True if different than fctx.
478 returns True if different than fctx.
479 """
479 """
480 if (fctx._filerev is None
480 if (fctx._filerev is None
481 and (self._repo._encodefilterpats
481 and (self._repo._encodefilterpats
482 # if file data starts with '\1\n', empty metadata block is
482 # if file data starts with '\1\n', empty metadata block is
483 # prepended, which adds 4 bytes to filelog.size().
483 # prepended, which adds 4 bytes to filelog.size().
484 or self.size() - 4 == fctx.size())
484 or self.size() - 4 == fctx.size())
485 or self.size() == fctx.size()):
485 or self.size() == fctx.size()):
486 return self._filelog.cmp(self._filenode, fctx.data())
486 return self._filelog.cmp(self._filenode, fctx.data())
487
487
488 return True
488 return True
489
489
490 def renamed(self):
490 def renamed(self):
491 """check if file was actually renamed in this changeset revision
491 """check if file was actually renamed in this changeset revision
492
492
493 If rename logged in file revision, we report copy for changeset only
493 If rename logged in file revision, we report copy for changeset only
494 if file revisions linkrev points back to the changeset in question
494 if file revisions linkrev points back to the changeset in question
495 or both changeset parents contain different file revisions.
495 or both changeset parents contain different file revisions.
496 """
496 """
497
497
498 renamed = self._filelog.renamed(self._filenode)
498 renamed = self._filelog.renamed(self._filenode)
499 if not renamed:
499 if not renamed:
500 return renamed
500 return renamed
501
501
502 if self.rev() == self.linkrev():
502 if self.rev() == self.linkrev():
503 return renamed
503 return renamed
504
504
505 name = self.path()
505 name = self.path()
506 fnode = self._filenode
506 fnode = self._filenode
507 for p in self._changectx.parents():
507 for p in self._changectx.parents():
508 try:
508 try:
509 if fnode == p.filenode(name):
509 if fnode == p.filenode(name):
510 return None
510 return None
511 except error.LookupError:
511 except error.LookupError:
512 pass
512 pass
513 return renamed
513 return renamed
514
514
515 def parents(self):
515 def parents(self):
516 p = self._path
516 p = self._path
517 fl = self._filelog
517 fl = self._filelog
518 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
518 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
519
519
520 r = self._filelog.renamed(self._filenode)
520 r = self._filelog.renamed(self._filenode)
521 if r:
521 if r:
522 pl[0] = (r[0], r[1], None)
522 pl[0] = (r[0], r[1], None)
523
523
524 return [filectx(self._repo, p, fileid=n, filelog=l)
524 return [filectx(self._repo, p, fileid=n, filelog=l)
525 for p, n, l in pl if n != nullid]
525 for p, n, l in pl if n != nullid]
526
526
527 def p1(self):
527 def p1(self):
528 return self.parents()[0]
528 return self.parents()[0]
529
529
530 def p2(self):
530 def p2(self):
531 p = self.parents()
531 p = self.parents()
532 if len(p) == 2:
532 if len(p) == 2:
533 return p[1]
533 return p[1]
534 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
534 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
535
535
536 def children(self):
536 def children(self):
537 # hard for renames
537 # hard for renames
538 c = self._filelog.children(self._filenode)
538 c = self._filelog.children(self._filenode)
539 return [filectx(self._repo, self._path, fileid=x,
539 return [filectx(self._repo, self._path, fileid=x,
540 filelog=self._filelog) for x in c]
540 filelog=self._filelog) for x in c]
541
541
542 def annotate(self, follow=False, linenumber=None, diffopts=None):
542 def annotate(self, follow=False, linenumber=None, diffopts=None):
543 '''returns a list of tuples of (ctx, line) for each line
543 '''returns a list of tuples of (ctx, line) for each line
544 in the file, where ctx is the filectx of the node where
544 in the file, where ctx is the filectx of the node where
545 that line was last changed.
545 that line was last changed.
546 This returns tuples of ((ctx, linenumber), line) for each line,
546 This returns tuples of ((ctx, linenumber), line) for each line,
547 if "linenumber" parameter is NOT "None".
547 if "linenumber" parameter is NOT "None".
548 In such tuples, linenumber means one at the first appearance
548 In such tuples, linenumber means one at the first appearance
549 in the managed file.
549 in the managed file.
550 To reduce annotation cost,
550 To reduce annotation cost,
551 this returns fixed value(False is used) as linenumber,
551 this returns fixed value(False is used) as linenumber,
552 if "linenumber" parameter is "False".'''
552 if "linenumber" parameter is "False".'''
553
553
554 def decorate_compat(text, rev):
554 def decorate_compat(text, rev):
555 return ([rev] * len(text.splitlines()), text)
555 return ([rev] * len(text.splitlines()), text)
556
556
557 def without_linenumber(text, rev):
557 def without_linenumber(text, rev):
558 return ([(rev, False)] * len(text.splitlines()), text)
558 return ([(rev, False)] * len(text.splitlines()), text)
559
559
560 def with_linenumber(text, rev):
560 def with_linenumber(text, rev):
561 size = len(text.splitlines())
561 size = len(text.splitlines())
562 return ([(rev, i) for i in xrange(1, size + 1)], text)
562 return ([(rev, i) for i in xrange(1, size + 1)], text)
563
563
564 decorate = (((linenumber is None) and decorate_compat) or
564 decorate = (((linenumber is None) and decorate_compat) or
565 (linenumber and with_linenumber) or
565 (linenumber and with_linenumber) or
566 without_linenumber)
566 without_linenumber)
567
567
568 def pair(parent, child):
568 def pair(parent, child):
569 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
569 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
570 refine=True)
570 refine=True)
571 for (a1, a2, b1, b2), t in blocks:
571 for (a1, a2, b1, b2), t in blocks:
572 # Changed blocks ('!') or blocks made only of blank lines ('~')
572 # Changed blocks ('!') or blocks made only of blank lines ('~')
573 # belong to the child.
573 # belong to the child.
574 if t == '=':
574 if t == '=':
575 child[0][b1:b2] = parent[0][a1:a2]
575 child[0][b1:b2] = parent[0][a1:a2]
576 return child
576 return child
577
577
578 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
578 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
579 def getctx(path, fileid):
579 def getctx(path, fileid):
580 log = path == self._path and self._filelog or getlog(path)
580 log = path == self._path and self._filelog or getlog(path)
581 return filectx(self._repo, path, fileid=fileid, filelog=log)
581 return filectx(self._repo, path, fileid=fileid, filelog=log)
582 getctx = util.lrucachefunc(getctx)
582 getctx = util.lrucachefunc(getctx)
583
583
584 def parents(f):
584 def parents(f):
585 # we want to reuse filectx objects as much as possible
585 # we want to reuse filectx objects as much as possible
586 p = f._path
586 p = f._path
587 if f._filerev is None: # working dir
587 if f._filerev is None: # working dir
588 pl = [(n.path(), n.filerev()) for n in f.parents()]
588 pl = [(n.path(), n.filerev()) for n in f.parents()]
589 else:
589 else:
590 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
590 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
591
591
592 if follow:
592 if follow:
593 r = f.renamed()
593 r = f.renamed()
594 if r:
594 if r:
595 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
595 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
596
596
597 return [getctx(p, n) for p, n in pl if n != nullrev]
597 return [getctx(p, n) for p, n in pl if n != nullrev]
598
598
599 # use linkrev to find the first changeset where self appeared
599 # use linkrev to find the first changeset where self appeared
600 if self.rev() != self.linkrev():
600 if self.rev() != self.linkrev():
601 base = self.filectx(self.filerev())
601 base = self.filectx(self.filerev())
602 else:
602 else:
603 base = self
603 base = self
604
604
605 # This algorithm would prefer to be recursive, but Python is a
605 # This algorithm would prefer to be recursive, but Python is a
606 # bit recursion-hostile. Instead we do an iterative
606 # bit recursion-hostile. Instead we do an iterative
607 # depth-first search.
607 # depth-first search.
608
608
609 visit = [base]
609 visit = [base]
610 hist = {}
610 hist = {}
611 pcache = {}
611 pcache = {}
612 needed = {base: 1}
612 needed = {base: 1}
613 while visit:
613 while visit:
614 f = visit[-1]
614 f = visit[-1]
615 if f not in pcache:
615 if f not in pcache:
616 pcache[f] = parents(f)
616 pcache[f] = parents(f)
617
617
618 ready = True
618 ready = True
619 pl = pcache[f]
619 pl = pcache[f]
620 for p in pl:
620 for p in pl:
621 if p not in hist:
621 if p not in hist:
622 ready = False
622 ready = False
623 visit.append(p)
623 visit.append(p)
624 needed[p] = needed.get(p, 0) + 1
624 needed[p] = needed.get(p, 0) + 1
625 if ready:
625 if ready:
626 visit.pop()
626 visit.pop()
627 curr = decorate(f.data(), f)
627 curr = decorate(f.data(), f)
628 for p in pl:
628 for p in pl:
629 curr = pair(hist[p], curr)
629 curr = pair(hist[p], curr)
630 if needed[p] == 1:
630 if needed[p] == 1:
631 del hist[p]
631 del hist[p]
632 else:
632 else:
633 needed[p] -= 1
633 needed[p] -= 1
634
634
635 hist[f] = curr
635 hist[f] = curr
636 pcache[f] = []
636 pcache[f] = []
637
637
638 return zip(hist[base][0], hist[base][1].splitlines(True))
638 return zip(hist[base][0], hist[base][1].splitlines(True))
639
639
640 def ancestor(self, fc2, actx):
640 def ancestor(self, fc2, actx):
641 """
641 """
642 find the common ancestor file context, if any, of self, and fc2
642 find the common ancestor file context, if any, of self, and fc2
643
643
644 actx must be the changectx of the common ancestor
644 actx must be the changectx of the common ancestor
645 of self's and fc2's respective changesets.
645 of self's and fc2's respective changesets.
646 """
646 """
647
647
648 # the easy case: no (relevant) renames
648 # the easy case: no (relevant) renames
649 if fc2.path() == self.path() and self.path() in actx:
649 if fc2.path() == self.path() and self.path() in actx:
650 return actx[self.path()]
650 return actx[self.path()]
651
651
652 # the next easiest cases: unambiguous predecessor (name trumps
652 # the next easiest cases: unambiguous predecessor (name trumps
653 # history)
653 # history)
654 if self.path() in actx and fc2.path() not in actx:
654 if self.path() in actx and fc2.path() not in actx:
655 return actx[self.path()]
655 return actx[self.path()]
656 if fc2.path() in actx and self.path() not in actx:
656 if fc2.path() in actx and self.path() not in actx:
657 return actx[fc2.path()]
657 return actx[fc2.path()]
658
658
659 # prime the ancestor cache for the working directory
659 # prime the ancestor cache for the working directory
660 acache = {}
660 acache = {}
661 for c in (self, fc2):
661 for c in (self, fc2):
662 if c._filerev is None:
662 if c._filerev is None:
663 pl = [(n.path(), n.filenode()) for n in c.parents()]
663 pl = [(n.path(), n.filenode()) for n in c.parents()]
664 acache[(c._path, None)] = pl
664 acache[(c._path, None)] = pl
665
665
666 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
666 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
667 def parents(vertex):
667 def parents(vertex):
668 if vertex in acache:
668 if vertex in acache:
669 return acache[vertex]
669 return acache[vertex]
670 f, n = vertex
670 f, n = vertex
671 if f not in flcache:
671 if f not in flcache:
672 flcache[f] = self._repo.file(f)
672 flcache[f] = self._repo.file(f)
673 fl = flcache[f]
673 fl = flcache[f]
674 pl = [(f, p) for p in fl.parents(n) if p != nullid]
674 pl = [(f, p) for p in fl.parents(n) if p != nullid]
675 re = fl.renamed(n)
675 re = fl.renamed(n)
676 if re:
676 if re:
677 pl.append(re)
677 pl.append(re)
678 acache[vertex] = pl
678 acache[vertex] = pl
679 return pl
679 return pl
680
680
681 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
681 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
682 v = ancestor.ancestor(a, b, parents)
682 v = ancestor.ancestor(a, b, parents)
683 if v:
683 if v:
684 f, n = v
684 f, n = v
685 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
685 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
686
686
687 return None
687 return None
688
688
689 def ancestors(self, followfirst=False):
689 def ancestors(self, followfirst=False):
690 visit = {}
690 visit = {}
691 c = self
691 c = self
692 cut = followfirst and 1 or None
692 cut = followfirst and 1 or None
693 while True:
693 while True:
694 for parent in c.parents()[:cut]:
694 for parent in c.parents()[:cut]:
695 visit[(parent.rev(), parent.node())] = parent
695 visit[(parent.rev(), parent.node())] = parent
696 if not visit:
696 if not visit:
697 break
697 break
698 c = visit.pop(max(visit))
698 c = visit.pop(max(visit))
699 yield c
699 yield c
700
700
701 def copies(self, c2):
701 def copies(self, c2):
702 if not util.safehasattr(self, "_copycache"):
702 if not util.safehasattr(self, "_copycache"):
703 self._copycache = {}
703 self._copycache = {}
704 sc2 = str(c2)
704 sc2 = str(c2)
705 if sc2 not in self._copycache:
705 if sc2 not in self._copycache:
706 self._copycache[sc2] = copies.pathcopies(c2)
706 self._copycache[sc2] = copies.pathcopies(c2)
707 return self._copycache[sc2]
707 return self._copycache[sc2]
708
708
709 class workingctx(changectx):
709 class workingctx(changectx):
710 """A workingctx object makes access to data related to
710 """A workingctx object makes access to data related to
711 the current working directory convenient.
711 the current working directory convenient.
712 date - any valid date string or (unixtime, offset), or None.
712 date - any valid date string or (unixtime, offset), or None.
713 user - username string, or None.
713 user - username string, or None.
714 extra - a dictionary of extra values, or None.
714 extra - a dictionary of extra values, or None.
715 changes - a list of file lists as returned by localrepo.status()
715 changes - a list of file lists as returned by localrepo.status()
716 or None to use the repository status.
716 or None to use the repository status.
717 """
717 """
718 def __init__(self, repo, text="", user=None, date=None, extra=None,
718 def __init__(self, repo, text="", user=None, date=None, extra=None,
719 changes=None):
719 changes=None):
720 self._repo = repo
720 self._repo = repo
721 self._rev = None
721 self._rev = None
722 self._node = None
722 self._node = None
723 self._text = text
723 self._text = text
724 if date:
724 if date:
725 self._date = util.parsedate(date)
725 self._date = util.parsedate(date)
726 if user:
726 if user:
727 self._user = user
727 self._user = user
728 if changes:
728 if changes:
729 self._status = list(changes[:4])
729 self._status = list(changes[:4])
730 self._unknown = changes[4]
730 self._unknown = changes[4]
731 self._ignored = changes[5]
731 self._ignored = changes[5]
732 self._clean = changes[6]
732 self._clean = changes[6]
733 else:
733 else:
734 self._unknown = None
734 self._unknown = None
735 self._ignored = None
735 self._ignored = None
736 self._clean = None
736 self._clean = None
737
737
738 self._extra = {}
738 self._extra = {}
739 if extra:
739 if extra:
740 self._extra = extra.copy()
740 self._extra = extra.copy()
741 if 'branch' not in self._extra:
741 if 'branch' not in self._extra:
742 try:
742 try:
743 branch = encoding.fromlocal(self._repo.dirstate.branch())
743 branch = encoding.fromlocal(self._repo.dirstate.branch())
744 except UnicodeDecodeError:
744 except UnicodeDecodeError:
745 raise util.Abort(_('branch name not in UTF-8!'))
745 raise util.Abort(_('branch name not in UTF-8!'))
746 self._extra['branch'] = branch
746 self._extra['branch'] = branch
747 if self._extra['branch'] == '':
747 if self._extra['branch'] == '':
748 self._extra['branch'] = 'default'
748 self._extra['branch'] = 'default'
749
749
750 def __str__(self):
750 def __str__(self):
751 return str(self._parents[0]) + "+"
751 return str(self._parents[0]) + "+"
752
752
753 def __repr__(self):
753 def __repr__(self):
754 return "<workingctx %s>" % str(self)
754 return "<workingctx %s>" % str(self)
755
755
756 def __nonzero__(self):
756 def __nonzero__(self):
757 return True
757 return True
758
758
759 def __contains__(self, key):
759 def __contains__(self, key):
760 return self._repo.dirstate[key] not in "?r"
760 return self._repo.dirstate[key] not in "?r"
761
761
762 def _buildflagfunc(self):
762 def _buildflagfunc(self):
763 # Create a fallback function for getting file flags when the
763 # Create a fallback function for getting file flags when the
764 # filesystem doesn't support them
764 # filesystem doesn't support them
765
765
766 copiesget = self._repo.dirstate.copies().get
766 copiesget = self._repo.dirstate.copies().get
767
767
768 if len(self._parents) < 2:
768 if len(self._parents) < 2:
769 # when we have one parent, it's easy: copy from parent
769 # when we have one parent, it's easy: copy from parent
770 man = self._parents[0].manifest()
770 man = self._parents[0].manifest()
771 def func(f):
771 def func(f):
772 f = copiesget(f, f)
772 f = copiesget(f, f)
773 return man.flags(f)
773 return man.flags(f)
774 else:
774 else:
775 # merges are tricky: we try to reconstruct the unstored
775 # merges are tricky: we try to reconstruct the unstored
776 # result from the merge (issue1802)
776 # result from the merge (issue1802)
777 p1, p2 = self._parents
777 p1, p2 = self._parents
778 pa = p1.ancestor(p2)
778 pa = p1.ancestor(p2)
779 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
779 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
780
780
781 def func(f):
781 def func(f):
782 f = copiesget(f, f) # may be wrong for merges with copies
782 f = copiesget(f, f) # may be wrong for merges with copies
783 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
783 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
784 if fl1 == fl2:
784 if fl1 == fl2:
785 return fl1
785 return fl1
786 if fl1 == fla:
786 if fl1 == fla:
787 return fl2
787 return fl2
788 if fl2 == fla:
788 if fl2 == fla:
789 return fl1
789 return fl1
790 return '' # punt for conflicts
790 return '' # punt for conflicts
791
791
792 return func
792 return func
793
793
794 @propertycache
794 @propertycache
795 def _flagfunc(self):
795 def _flagfunc(self):
796 return self._repo.dirstate.flagfunc(self._buildflagfunc)
796 return self._repo.dirstate.flagfunc(self._buildflagfunc)
797
797
798 @propertycache
798 @propertycache
799 def _manifest(self):
799 def _manifest(self):
800 """generate a manifest corresponding to the working directory"""
800 """generate a manifest corresponding to the working directory"""
801
801
802 man = self._parents[0].manifest().copy()
802 man = self._parents[0].manifest().copy()
803 if len(self._parents) > 1:
803 if len(self._parents) > 1:
804 man2 = self.p2().manifest()
804 man2 = self.p2().manifest()
805 def getman(f):
805 def getman(f):
806 if f in man:
806 if f in man:
807 return man
807 return man
808 return man2
808 return man2
809 else:
809 else:
810 getman = lambda f: man
810 getman = lambda f: man
811
811
812 copied = self._repo.dirstate.copies()
812 copied = self._repo.dirstate.copies()
813 ff = self._flagfunc
813 ff = self._flagfunc
814 modified, added, removed, deleted = self._status
814 modified, added, removed, deleted = self._status
815 for i, l in (("a", added), ("m", modified)):
815 for i, l in (("a", added), ("m", modified)):
816 for f in l:
816 for f in l:
817 orig = copied.get(f, f)
817 orig = copied.get(f, f)
818 man[f] = getman(orig).get(orig, nullid) + i
818 man[f] = getman(orig).get(orig, nullid) + i
819 try:
819 try:
820 man.set(f, ff(f))
820 man.set(f, ff(f))
821 except OSError:
821 except OSError:
822 pass
822 pass
823
823
824 for f in deleted + removed:
824 for f in deleted + removed:
825 if f in man:
825 if f in man:
826 del man[f]
826 del man[f]
827
827
828 return man
828 return man
829
829
830 def __iter__(self):
830 def __iter__(self):
831 d = self._repo.dirstate
831 d = self._repo.dirstate
832 for f in d:
832 for f in d:
833 if d[f] != 'r':
833 if d[f] != 'r':
834 yield f
834 yield f
835
835
836 @propertycache
836 @propertycache
837 def _status(self):
837 def _status(self):
838 return self._repo.status()[:4]
838 return self._repo.status()[:4]
839
839
840 @propertycache
840 @propertycache
841 def _user(self):
841 def _user(self):
842 return self._repo.ui.username()
842 return self._repo.ui.username()
843
843
844 @propertycache
844 @propertycache
845 def _date(self):
845 def _date(self):
846 return util.makedate()
846 return util.makedate()
847
847
848 @propertycache
848 @propertycache
849 def _parents(self):
849 def _parents(self):
850 p = self._repo.dirstate.parents()
850 p = self._repo.dirstate.parents()
851 if p[1] == nullid:
851 if p[1] == nullid:
852 p = p[:-1]
852 p = p[:-1]
853 self._parents = [changectx(self._repo, x) for x in p]
853 self._parents = [changectx(self._repo, x) for x in p]
854 return self._parents
854 return self._parents
855
855
856 def status(self, ignored=False, clean=False, unknown=False):
856 def status(self, ignored=False, clean=False, unknown=False):
857 """Explicit status query
857 """Explicit status query
858 Unless this method is used to query the working copy status, the
858 Unless this method is used to query the working copy status, the
859 _status property will implicitly read the status using its default
859 _status property will implicitly read the status using its default
860 arguments."""
860 arguments."""
861 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
861 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
862 self._unknown = self._ignored = self._clean = None
862 self._unknown = self._ignored = self._clean = None
863 if unknown:
863 if unknown:
864 self._unknown = stat[4]
864 self._unknown = stat[4]
865 if ignored:
865 if ignored:
866 self._ignored = stat[5]
866 self._ignored = stat[5]
867 if clean:
867 if clean:
868 self._clean = stat[6]
868 self._clean = stat[6]
869 self._status = stat[:4]
869 self._status = stat[:4]
870 return stat
870 return stat
871
871
872 def manifest(self):
872 def manifest(self):
873 return self._manifest
873 return self._manifest
874 def user(self):
874 def user(self):
875 return self._user or self._repo.ui.username()
875 return self._user or self._repo.ui.username()
876 def date(self):
876 def date(self):
877 return self._date
877 return self._date
878 def description(self):
878 def description(self):
879 return self._text
879 return self._text
880 def files(self):
880 def files(self):
881 return sorted(self._status[0] + self._status[1] + self._status[2])
881 return sorted(self._status[0] + self._status[1] + self._status[2])
882
882
883 def modified(self):
883 def modified(self):
884 return self._status[0]
884 return self._status[0]
885 def added(self):
885 def added(self):
886 return self._status[1]
886 return self._status[1]
887 def removed(self):
887 def removed(self):
888 return self._status[2]
888 return self._status[2]
889 def deleted(self):
889 def deleted(self):
890 return self._status[3]
890 return self._status[3]
891 def unknown(self):
891 def unknown(self):
892 assert self._unknown is not None # must call status first
892 assert self._unknown is not None # must call status first
893 return self._unknown
893 return self._unknown
894 def ignored(self):
894 def ignored(self):
895 assert self._ignored is not None # must call status first
895 assert self._ignored is not None # must call status first
896 return self._ignored
896 return self._ignored
897 def clean(self):
897 def clean(self):
898 assert self._clean is not None # must call status first
898 assert self._clean is not None # must call status first
899 return self._clean
899 return self._clean
900 def branch(self):
900 def branch(self):
901 return encoding.tolocal(self._extra['branch'])
901 return encoding.tolocal(self._extra['branch'])
902 def closesbranch(self):
902 def closesbranch(self):
903 return 'close' in self._extra
903 return 'close' in self._extra
904 def extra(self):
904 def extra(self):
905 return self._extra
905 return self._extra
906
906
907 def tags(self):
907 def tags(self):
908 t = []
908 t = []
909 for p in self.parents():
909 for p in self.parents():
910 t.extend(p.tags())
910 t.extend(p.tags())
911 return t
911 return t
912
912
913 def bookmarks(self):
913 def bookmarks(self):
914 b = []
914 b = []
915 for p in self.parents():
915 for p in self.parents():
916 b.extend(p.bookmarks())
916 b.extend(p.bookmarks())
917 return b
917 return b
918
918
919 def phase(self):
919 def phase(self):
920 phase = phases.draft # default phase to draft
920 phase = phases.draft # default phase to draft
921 for p in self.parents():
921 for p in self.parents():
922 phase = max(phase, p.phase())
922 phase = max(phase, p.phase())
923 return phase
923 return phase
924
924
925 def hidden(self):
925 def hidden(self):
926 return False
926 return False
927
927
928 def children(self):
928 def children(self):
929 return []
929 return []
930
930
931 def flags(self, path):
931 def flags(self, path):
932 if '_manifest' in self.__dict__:
932 if '_manifest' in self.__dict__:
933 try:
933 try:
934 return self._manifest.flags(path)
934 return self._manifest.flags(path)
935 except KeyError:
935 except KeyError:
936 return ''
936 return ''
937
937
938 try:
938 try:
939 return self._flagfunc(path)
939 return self._flagfunc(path)
940 except OSError:
940 except OSError:
941 return ''
941 return ''
942
942
943 def filectx(self, path, filelog=None):
943 def filectx(self, path, filelog=None):
944 """get a file context from the working directory"""
944 """get a file context from the working directory"""
945 return workingfilectx(self._repo, path, workingctx=self,
945 return workingfilectx(self._repo, path, workingctx=self,
946 filelog=filelog)
946 filelog=filelog)
947
947
948 def ancestor(self, c2):
948 def ancestor(self, c2):
949 """return the ancestor context of self and c2"""
949 """return the ancestor context of self and c2"""
950 return self._parents[0].ancestor(c2) # punt on two parents for now
950 return self._parents[0].ancestor(c2) # punt on two parents for now
951
951
952 def walk(self, match):
952 def walk(self, match):
953 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
953 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
954 True, False))
954 True, False))
955
955
956 def dirty(self, missing=False, merge=True, branch=True):
956 def dirty(self, missing=False, merge=True, branch=True):
957 "check whether a working directory is modified"
957 "check whether a working directory is modified"
958 # check subrepos first
958 # check subrepos first
959 for s in self.substate:
959 for s in self.substate:
960 if self.sub(s).dirty():
960 if self.sub(s).dirty():
961 return True
961 return True
962 # check current working dir
962 # check current working dir
963 return ((merge and self.p2()) or
963 return ((merge and self.p2()) or
964 (branch and self.branch() != self.p1().branch()) or
964 (branch and self.branch() != self.p1().branch()) or
965 self.modified() or self.added() or self.removed() or
965 self.modified() or self.added() or self.removed() or
966 (missing and self.deleted()))
966 (missing and self.deleted()))
967
967
968 def add(self, list, prefix=""):
968 def add(self, list, prefix=""):
969 join = lambda f: os.path.join(prefix, f)
969 join = lambda f: os.path.join(prefix, f)
970 wlock = self._repo.wlock()
970 wlock = self._repo.wlock()
971 ui, ds = self._repo.ui, self._repo.dirstate
971 ui, ds = self._repo.ui, self._repo.dirstate
972 try:
972 try:
973 rejected = []
973 rejected = []
974 for f in list:
974 for f in list:
975 scmutil.checkportable(ui, join(f))
975 scmutil.checkportable(ui, join(f))
976 p = self._repo.wjoin(f)
976 p = self._repo.wjoin(f)
977 try:
977 try:
978 st = os.lstat(p)
978 st = os.lstat(p)
979 except OSError:
979 except OSError:
980 ui.warn(_("%s does not exist!\n") % join(f))
980 ui.warn(_("%s does not exist!\n") % join(f))
981 rejected.append(f)
981 rejected.append(f)
982 continue
982 continue
983 if st.st_size > 10000000:
983 if st.st_size > 10000000:
984 ui.warn(_("%s: up to %d MB of RAM may be required "
984 ui.warn(_("%s: up to %d MB of RAM may be required "
985 "to manage this file\n"
985 "to manage this file\n"
986 "(use 'hg revert %s' to cancel the "
986 "(use 'hg revert %s' to cancel the "
987 "pending addition)\n")
987 "pending addition)\n")
988 % (f, 3 * st.st_size // 1000000, join(f)))
988 % (f, 3 * st.st_size // 1000000, join(f)))
989 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
989 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
990 ui.warn(_("%s not added: only files and symlinks "
990 ui.warn(_("%s not added: only files and symlinks "
991 "supported currently\n") % join(f))
991 "supported currently\n") % join(f))
992 rejected.append(p)
992 rejected.append(p)
993 elif ds[f] in 'amn':
993 elif ds[f] in 'amn':
994 ui.warn(_("%s already tracked!\n") % join(f))
994 ui.warn(_("%s already tracked!\n") % join(f))
995 elif ds[f] == 'r':
995 elif ds[f] == 'r':
996 ds.normallookup(f)
996 ds.normallookup(f)
997 else:
997 else:
998 ds.add(f)
998 ds.add(f)
999 return rejected
999 return rejected
1000 finally:
1000 finally:
1001 wlock.release()
1001 wlock.release()
1002
1002
1003 def forget(self, files, prefix=""):
1003 def forget(self, files, prefix=""):
1004 join = lambda f: os.path.join(prefix, f)
1004 join = lambda f: os.path.join(prefix, f)
1005 wlock = self._repo.wlock()
1005 wlock = self._repo.wlock()
1006 try:
1006 try:
1007 rejected = []
1007 rejected = []
1008 for f in files:
1008 for f in files:
1009 if f not in self._repo.dirstate:
1009 if f not in self._repo.dirstate:
1010 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1010 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1011 rejected.append(f)
1011 rejected.append(f)
1012 elif self._repo.dirstate[f] != 'a':
1012 elif self._repo.dirstate[f] != 'a':
1013 self._repo.dirstate.remove(f)
1013 self._repo.dirstate.remove(f)
1014 else:
1014 else:
1015 self._repo.dirstate.drop(f)
1015 self._repo.dirstate.drop(f)
1016 return rejected
1016 return rejected
1017 finally:
1017 finally:
1018 wlock.release()
1018 wlock.release()
1019
1019
1020 def ancestors(self):
1020 def ancestors(self):
1021 for a in self._repo.changelog.ancestors(
1021 for a in self._repo.changelog.ancestors(
1022 [p.rev() for p in self._parents]):
1022 [p.rev() for p in self._parents]):
1023 yield changectx(self._repo, a)
1023 yield changectx(self._repo, a)
1024
1024
1025 def undelete(self, list):
1025 def undelete(self, list):
1026 pctxs = self.parents()
1026 pctxs = self.parents()
1027 wlock = self._repo.wlock()
1027 wlock = self._repo.wlock()
1028 try:
1028 try:
1029 for f in list:
1029 for f in list:
1030 if self._repo.dirstate[f] != 'r':
1030 if self._repo.dirstate[f] != 'r':
1031 self._repo.ui.warn(_("%s not removed!\n") % f)
1031 self._repo.ui.warn(_("%s not removed!\n") % f)
1032 else:
1032 else:
1033 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1033 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1034 t = fctx.data()
1034 t = fctx.data()
1035 self._repo.wwrite(f, t, fctx.flags())
1035 self._repo.wwrite(f, t, fctx.flags())
1036 self._repo.dirstate.normal(f)
1036 self._repo.dirstate.normal(f)
1037 finally:
1037 finally:
1038 wlock.release()
1038 wlock.release()
1039
1039
1040 def copy(self, source, dest):
1040 def copy(self, source, dest):
1041 p = self._repo.wjoin(dest)
1041 p = self._repo.wjoin(dest)
1042 if not os.path.lexists(p):
1042 if not os.path.lexists(p):
1043 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1043 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1044 elif not (os.path.isfile(p) or os.path.islink(p)):
1044 elif not (os.path.isfile(p) or os.path.islink(p)):
1045 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1045 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1046 "symbolic link\n") % dest)
1046 "symbolic link\n") % dest)
1047 else:
1047 else:
1048 wlock = self._repo.wlock()
1048 wlock = self._repo.wlock()
1049 try:
1049 try:
1050 if self._repo.dirstate[dest] in '?r':
1050 if self._repo.dirstate[dest] in '?r':
1051 self._repo.dirstate.add(dest)
1051 self._repo.dirstate.add(dest)
1052 self._repo.dirstate.copy(source, dest)
1052 self._repo.dirstate.copy(source, dest)
1053 finally:
1053 finally:
1054 wlock.release()
1054 wlock.release()
1055
1055
1056 def dirs(self):
1056 def dirs(self):
1057 return self._repo.dirstate.dirs()
1057 return set(self._repo.dirstate.dirs())
1058
1058
1059 class workingfilectx(filectx):
1059 class workingfilectx(filectx):
1060 """A workingfilectx object makes access to data related to a particular
1060 """A workingfilectx object makes access to data related to a particular
1061 file in the working directory convenient."""
1061 file in the working directory convenient."""
1062 def __init__(self, repo, path, filelog=None, workingctx=None):
1062 def __init__(self, repo, path, filelog=None, workingctx=None):
1063 """changeid can be a changeset revision, node, or tag.
1063 """changeid can be a changeset revision, node, or tag.
1064 fileid can be a file revision or node."""
1064 fileid can be a file revision or node."""
1065 self._repo = repo
1065 self._repo = repo
1066 self._path = path
1066 self._path = path
1067 self._changeid = None
1067 self._changeid = None
1068 self._filerev = self._filenode = None
1068 self._filerev = self._filenode = None
1069
1069
1070 if filelog:
1070 if filelog:
1071 self._filelog = filelog
1071 self._filelog = filelog
1072 if workingctx:
1072 if workingctx:
1073 self._changectx = workingctx
1073 self._changectx = workingctx
1074
1074
1075 @propertycache
1075 @propertycache
1076 def _changectx(self):
1076 def _changectx(self):
1077 return workingctx(self._repo)
1077 return workingctx(self._repo)
1078
1078
1079 def __nonzero__(self):
1079 def __nonzero__(self):
1080 return True
1080 return True
1081
1081
1082 def __str__(self):
1082 def __str__(self):
1083 return "%s@%s" % (self.path(), self._changectx)
1083 return "%s@%s" % (self.path(), self._changectx)
1084
1084
1085 def __repr__(self):
1085 def __repr__(self):
1086 return "<workingfilectx %s>" % str(self)
1086 return "<workingfilectx %s>" % str(self)
1087
1087
1088 def data(self):
1088 def data(self):
1089 return self._repo.wread(self._path)
1089 return self._repo.wread(self._path)
1090 def renamed(self):
1090 def renamed(self):
1091 rp = self._repo.dirstate.copied(self._path)
1091 rp = self._repo.dirstate.copied(self._path)
1092 if not rp:
1092 if not rp:
1093 return None
1093 return None
1094 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1094 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1095
1095
1096 def parents(self):
1096 def parents(self):
1097 '''return parent filectxs, following copies if necessary'''
1097 '''return parent filectxs, following copies if necessary'''
1098 def filenode(ctx, path):
1098 def filenode(ctx, path):
1099 return ctx._manifest.get(path, nullid)
1099 return ctx._manifest.get(path, nullid)
1100
1100
1101 path = self._path
1101 path = self._path
1102 fl = self._filelog
1102 fl = self._filelog
1103 pcl = self._changectx._parents
1103 pcl = self._changectx._parents
1104 renamed = self.renamed()
1104 renamed = self.renamed()
1105
1105
1106 if renamed:
1106 if renamed:
1107 pl = [renamed + (None,)]
1107 pl = [renamed + (None,)]
1108 else:
1108 else:
1109 pl = [(path, filenode(pcl[0], path), fl)]
1109 pl = [(path, filenode(pcl[0], path), fl)]
1110
1110
1111 for pc in pcl[1:]:
1111 for pc in pcl[1:]:
1112 pl.append((path, filenode(pc, path), fl))
1112 pl.append((path, filenode(pc, path), fl))
1113
1113
1114 return [filectx(self._repo, p, fileid=n, filelog=l)
1114 return [filectx(self._repo, p, fileid=n, filelog=l)
1115 for p, n, l in pl if n != nullid]
1115 for p, n, l in pl if n != nullid]
1116
1116
1117 def children(self):
1117 def children(self):
1118 return []
1118 return []
1119
1119
1120 def size(self):
1120 def size(self):
1121 return os.lstat(self._repo.wjoin(self._path)).st_size
1121 return os.lstat(self._repo.wjoin(self._path)).st_size
1122 def date(self):
1122 def date(self):
1123 t, tz = self._changectx.date()
1123 t, tz = self._changectx.date()
1124 try:
1124 try:
1125 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
1125 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
1126 except OSError, err:
1126 except OSError, err:
1127 if err.errno != errno.ENOENT:
1127 if err.errno != errno.ENOENT:
1128 raise
1128 raise
1129 return (t, tz)
1129 return (t, tz)
1130
1130
1131 def cmp(self, fctx):
1131 def cmp(self, fctx):
1132 """compare with other file context
1132 """compare with other file context
1133
1133
1134 returns True if different than fctx.
1134 returns True if different than fctx.
1135 """
1135 """
1136 # fctx should be a filectx (not a wfctx)
1136 # fctx should be a filectx (not a wfctx)
1137 # invert comparison to reuse the same code path
1137 # invert comparison to reuse the same code path
1138 return fctx.cmp(self)
1138 return fctx.cmp(self)
1139
1139
1140 class memctx(object):
1140 class memctx(object):
1141 """Use memctx to perform in-memory commits via localrepo.commitctx().
1141 """Use memctx to perform in-memory commits via localrepo.commitctx().
1142
1142
1143 Revision information is supplied at initialization time while
1143 Revision information is supplied at initialization time while
1144 related files data and is made available through a callback
1144 related files data and is made available through a callback
1145 mechanism. 'repo' is the current localrepo, 'parents' is a
1145 mechanism. 'repo' is the current localrepo, 'parents' is a
1146 sequence of two parent revisions identifiers (pass None for every
1146 sequence of two parent revisions identifiers (pass None for every
1147 missing parent), 'text' is the commit message and 'files' lists
1147 missing parent), 'text' is the commit message and 'files' lists
1148 names of files touched by the revision (normalized and relative to
1148 names of files touched by the revision (normalized and relative to
1149 repository root).
1149 repository root).
1150
1150
1151 filectxfn(repo, memctx, path) is a callable receiving the
1151 filectxfn(repo, memctx, path) is a callable receiving the
1152 repository, the current memctx object and the normalized path of
1152 repository, the current memctx object and the normalized path of
1153 requested file, relative to repository root. It is fired by the
1153 requested file, relative to repository root. It is fired by the
1154 commit function for every file in 'files', but calls order is
1154 commit function for every file in 'files', but calls order is
1155 undefined. If the file is available in the revision being
1155 undefined. If the file is available in the revision being
1156 committed (updated or added), filectxfn returns a memfilectx
1156 committed (updated or added), filectxfn returns a memfilectx
1157 object. If the file was removed, filectxfn raises an
1157 object. If the file was removed, filectxfn raises an
1158 IOError. Moved files are represented by marking the source file
1158 IOError. Moved files are represented by marking the source file
1159 removed and the new file added with copy information (see
1159 removed and the new file added with copy information (see
1160 memfilectx).
1160 memfilectx).
1161
1161
1162 user receives the committer name and defaults to current
1162 user receives the committer name and defaults to current
1163 repository username, date is the commit date in any format
1163 repository username, date is the commit date in any format
1164 supported by util.parsedate() and defaults to current date, extra
1164 supported by util.parsedate() and defaults to current date, extra
1165 is a dictionary of metadata or is left empty.
1165 is a dictionary of metadata or is left empty.
1166 """
1166 """
1167 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1167 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1168 date=None, extra=None):
1168 date=None, extra=None):
1169 self._repo = repo
1169 self._repo = repo
1170 self._rev = None
1170 self._rev = None
1171 self._node = None
1171 self._node = None
1172 self._text = text
1172 self._text = text
1173 self._date = date and util.parsedate(date) or util.makedate()
1173 self._date = date and util.parsedate(date) or util.makedate()
1174 self._user = user
1174 self._user = user
1175 parents = [(p or nullid) for p in parents]
1175 parents = [(p or nullid) for p in parents]
1176 p1, p2 = parents
1176 p1, p2 = parents
1177 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1177 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1178 files = sorted(set(files))
1178 files = sorted(set(files))
1179 self._status = [files, [], [], [], []]
1179 self._status = [files, [], [], [], []]
1180 self._filectxfn = filectxfn
1180 self._filectxfn = filectxfn
1181
1181
1182 self._extra = extra and extra.copy() or {}
1182 self._extra = extra and extra.copy() or {}
1183 if self._extra.get('branch', '') == '':
1183 if self._extra.get('branch', '') == '':
1184 self._extra['branch'] = 'default'
1184 self._extra['branch'] = 'default'
1185
1185
1186 def __str__(self):
1186 def __str__(self):
1187 return str(self._parents[0]) + "+"
1187 return str(self._parents[0]) + "+"
1188
1188
1189 def __int__(self):
1189 def __int__(self):
1190 return self._rev
1190 return self._rev
1191
1191
1192 def __nonzero__(self):
1192 def __nonzero__(self):
1193 return True
1193 return True
1194
1194
1195 def __getitem__(self, key):
1195 def __getitem__(self, key):
1196 return self.filectx(key)
1196 return self.filectx(key)
1197
1197
1198 def p1(self):
1198 def p1(self):
1199 return self._parents[0]
1199 return self._parents[0]
1200 def p2(self):
1200 def p2(self):
1201 return self._parents[1]
1201 return self._parents[1]
1202
1202
1203 def user(self):
1203 def user(self):
1204 return self._user or self._repo.ui.username()
1204 return self._user or self._repo.ui.username()
1205 def date(self):
1205 def date(self):
1206 return self._date
1206 return self._date
1207 def description(self):
1207 def description(self):
1208 return self._text
1208 return self._text
1209 def files(self):
1209 def files(self):
1210 return self.modified()
1210 return self.modified()
1211 def modified(self):
1211 def modified(self):
1212 return self._status[0]
1212 return self._status[0]
1213 def added(self):
1213 def added(self):
1214 return self._status[1]
1214 return self._status[1]
1215 def removed(self):
1215 def removed(self):
1216 return self._status[2]
1216 return self._status[2]
1217 def deleted(self):
1217 def deleted(self):
1218 return self._status[3]
1218 return self._status[3]
1219 def unknown(self):
1219 def unknown(self):
1220 return self._status[4]
1220 return self._status[4]
1221 def ignored(self):
1221 def ignored(self):
1222 return self._status[5]
1222 return self._status[5]
1223 def clean(self):
1223 def clean(self):
1224 return self._status[6]
1224 return self._status[6]
1225 def branch(self):
1225 def branch(self):
1226 return encoding.tolocal(self._extra['branch'])
1226 return encoding.tolocal(self._extra['branch'])
1227 def extra(self):
1227 def extra(self):
1228 return self._extra
1228 return self._extra
1229 def flags(self, f):
1229 def flags(self, f):
1230 return self[f].flags()
1230 return self[f].flags()
1231
1231
1232 def parents(self):
1232 def parents(self):
1233 """return contexts for each parent changeset"""
1233 """return contexts for each parent changeset"""
1234 return self._parents
1234 return self._parents
1235
1235
1236 def filectx(self, path, filelog=None):
1236 def filectx(self, path, filelog=None):
1237 """get a file context from the working directory"""
1237 """get a file context from the working directory"""
1238 return self._filectxfn(self._repo, self, path)
1238 return self._filectxfn(self._repo, self, path)
1239
1239
1240 def commit(self):
1240 def commit(self):
1241 """commit context to the repo"""
1241 """commit context to the repo"""
1242 return self._repo.commitctx(self)
1242 return self._repo.commitctx(self)
1243
1243
1244 class memfilectx(object):
1244 class memfilectx(object):
1245 """memfilectx represents an in-memory file to commit.
1245 """memfilectx represents an in-memory file to commit.
1246
1246
1247 See memctx for more details.
1247 See memctx for more details.
1248 """
1248 """
1249 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1249 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1250 """
1250 """
1251 path is the normalized file path relative to repository root.
1251 path is the normalized file path relative to repository root.
1252 data is the file content as a string.
1252 data is the file content as a string.
1253 islink is True if the file is a symbolic link.
1253 islink is True if the file is a symbolic link.
1254 isexec is True if the file is executable.
1254 isexec is True if the file is executable.
1255 copied is the source file path if current file was copied in the
1255 copied is the source file path if current file was copied in the
1256 revision being committed, or None."""
1256 revision being committed, or None."""
1257 self._path = path
1257 self._path = path
1258 self._data = data
1258 self._data = data
1259 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1259 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1260 self._copied = None
1260 self._copied = None
1261 if copied:
1261 if copied:
1262 self._copied = (copied, nullid)
1262 self._copied = (copied, nullid)
1263
1263
1264 def __nonzero__(self):
1264 def __nonzero__(self):
1265 return True
1265 return True
1266 def __str__(self):
1266 def __str__(self):
1267 return "%s@%s" % (self.path(), self._changectx)
1267 return "%s@%s" % (self.path(), self._changectx)
1268 def path(self):
1268 def path(self):
1269 return self._path
1269 return self._path
1270 def data(self):
1270 def data(self):
1271 return self._data
1271 return self._data
1272 def flags(self):
1272 def flags(self):
1273 return self._flags
1273 return self._flags
1274 def isexec(self):
1274 def isexec(self):
1275 return 'x' in self._flags
1275 return 'x' in self._flags
1276 def islink(self):
1276 def islink(self):
1277 return 'l' in self._flags
1277 return 'l' in self._flags
1278 def renamed(self):
1278 def renamed(self):
1279 return self._copied
1279 return self._copied
@@ -1,370 +1,372
1 # copies.py - copy detection for Mercurial
1 # copies.py - copy detection for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 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 import util
8 import util
9 import heapq
9 import heapq
10
10
11 def _nonoverlap(d1, d2, d3):
11 def _nonoverlap(d1, d2, d3):
12 "Return list of elements in d1 not in d2 or d3"
12 "Return list of elements in d1 not in d2 or d3"
13 return sorted([d for d in d1 if d not in d3 and d not in d2])
13 return sorted([d for d in d1 if d not in d3 and d not in d2])
14
14
15 def _dirname(f):
15 def _dirname(f):
16 s = f.rfind("/")
16 s = f.rfind("/")
17 if s == -1:
17 if s == -1:
18 return ""
18 return ""
19 return f[:s]
19 return f[:s]
20
20
21 def _findlimit(repo, a, b):
21 def _findlimit(repo, a, b):
22 """Find the earliest revision that's an ancestor of a or b but not both,
22 """Find the earliest revision that's an ancestor of a or b but not both,
23 None if no such revision exists.
23 None if no such revision exists.
24 """
24 """
25 # basic idea:
25 # basic idea:
26 # - mark a and b with different sides
26 # - mark a and b with different sides
27 # - if a parent's children are all on the same side, the parent is
27 # - if a parent's children are all on the same side, the parent is
28 # on that side, otherwise it is on no side
28 # on that side, otherwise it is on no side
29 # - walk the graph in topological order with the help of a heap;
29 # - walk the graph in topological order with the help of a heap;
30 # - add unseen parents to side map
30 # - add unseen parents to side map
31 # - clear side of any parent that has children on different sides
31 # - clear side of any parent that has children on different sides
32 # - track number of interesting revs that might still be on a side
32 # - track number of interesting revs that might still be on a side
33 # - track the lowest interesting rev seen
33 # - track the lowest interesting rev seen
34 # - quit when interesting revs is zero
34 # - quit when interesting revs is zero
35
35
36 cl = repo.changelog
36 cl = repo.changelog
37 working = len(cl) # pseudo rev for the working directory
37 working = len(cl) # pseudo rev for the working directory
38 if a is None:
38 if a is None:
39 a = working
39 a = working
40 if b is None:
40 if b is None:
41 b = working
41 b = working
42
42
43 side = {a: -1, b: 1}
43 side = {a: -1, b: 1}
44 visit = [-a, -b]
44 visit = [-a, -b]
45 heapq.heapify(visit)
45 heapq.heapify(visit)
46 interesting = len(visit)
46 interesting = len(visit)
47 hascommonancestor = False
47 hascommonancestor = False
48 limit = working
48 limit = working
49
49
50 while interesting:
50 while interesting:
51 r = -heapq.heappop(visit)
51 r = -heapq.heappop(visit)
52 if r == working:
52 if r == working:
53 parents = [cl.rev(p) for p in repo.dirstate.parents()]
53 parents = [cl.rev(p) for p in repo.dirstate.parents()]
54 else:
54 else:
55 parents = cl.parentrevs(r)
55 parents = cl.parentrevs(r)
56 for p in parents:
56 for p in parents:
57 if p < 0:
57 if p < 0:
58 continue
58 continue
59 if p not in side:
59 if p not in side:
60 # first time we see p; add it to visit
60 # first time we see p; add it to visit
61 side[p] = side[r]
61 side[p] = side[r]
62 if side[p]:
62 if side[p]:
63 interesting += 1
63 interesting += 1
64 heapq.heappush(visit, -p)
64 heapq.heappush(visit, -p)
65 elif side[p] and side[p] != side[r]:
65 elif side[p] and side[p] != side[r]:
66 # p was interesting but now we know better
66 # p was interesting but now we know better
67 side[p] = 0
67 side[p] = 0
68 interesting -= 1
68 interesting -= 1
69 hascommonancestor = True
69 hascommonancestor = True
70 if side[r]:
70 if side[r]:
71 limit = r # lowest rev visited
71 limit = r # lowest rev visited
72 interesting -= 1
72 interesting -= 1
73
73
74 if not hascommonancestor:
74 if not hascommonancestor:
75 return None
75 return None
76 return limit
76 return limit
77
77
78 def _chain(src, dst, a, b):
78 def _chain(src, dst, a, b):
79 '''chain two sets of copies a->b'''
79 '''chain two sets of copies a->b'''
80 t = a.copy()
80 t = a.copy()
81 for k, v in b.iteritems():
81 for k, v in b.iteritems():
82 if v in t:
82 if v in t:
83 # found a chain
83 # found a chain
84 if t[v] != k:
84 if t[v] != k:
85 # file wasn't renamed back to itself
85 # file wasn't renamed back to itself
86 t[k] = t[v]
86 t[k] = t[v]
87 if v not in dst:
87 if v not in dst:
88 # chain was a rename, not a copy
88 # chain was a rename, not a copy
89 del t[v]
89 del t[v]
90 if v in src:
90 if v in src:
91 # file is a copy of an existing file
91 # file is a copy of an existing file
92 t[k] = v
92 t[k] = v
93
93
94 # remove criss-crossed copies
94 # remove criss-crossed copies
95 for k, v in t.items():
95 for k, v in t.items():
96 if k in src and v in dst:
96 if k in src and v in dst:
97 del t[k]
97 del t[k]
98
98
99 return t
99 return t
100
100
101 def _tracefile(fctx, actx):
101 def _tracefile(fctx, actx):
102 '''return file context that is the ancestor of fctx present in actx'''
102 '''return file context that is the ancestor of fctx present in actx'''
103 stop = actx.rev()
103 stop = actx.rev()
104 am = actx.manifest()
104 am = actx.manifest()
105
105
106 for f in fctx.ancestors():
106 for f in fctx.ancestors():
107 if am.get(f.path(), None) == f.filenode():
107 if am.get(f.path(), None) == f.filenode():
108 return f
108 return f
109 if f.rev() < stop:
109 if f.rev() < stop:
110 return None
110 return None
111
111
112 def _dirstatecopies(d):
112 def _dirstatecopies(d):
113 ds = d._repo.dirstate
113 ds = d._repo.dirstate
114 c = ds.copies().copy()
114 c = ds.copies().copy()
115 for k in c.keys():
115 for k in c.keys():
116 if ds[k] not in 'anm':
116 if ds[k] not in 'anm':
117 del c[k]
117 del c[k]
118 return c
118 return c
119
119
120 def _forwardcopies(a, b):
120 def _forwardcopies(a, b):
121 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
121 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
122
122
123 # check for working copy
123 # check for working copy
124 w = None
124 w = None
125 if b.rev() is None:
125 if b.rev() is None:
126 w = b
126 w = b
127 b = w.p1()
127 b = w.p1()
128 if a == b:
128 if a == b:
129 # short-circuit to avoid issues with merge states
129 # short-circuit to avoid issues with merge states
130 return _dirstatecopies(w)
130 return _dirstatecopies(w)
131
131
132 # find where new files came from
132 # find where new files came from
133 # we currently don't try to find where old files went, too expensive
133 # we currently don't try to find where old files went, too expensive
134 # this means we can miss a case like 'hg rm b; hg cp a b'
134 # this means we can miss a case like 'hg rm b; hg cp a b'
135 cm = {}
135 cm = {}
136 for f in b:
136 for f in b:
137 if f not in a:
137 if f not in a:
138 ofctx = _tracefile(b[f], a)
138 ofctx = _tracefile(b[f], a)
139 if ofctx:
139 if ofctx:
140 cm[f] = ofctx.path()
140 cm[f] = ofctx.path()
141
141
142 # combine copies from dirstate if necessary
142 # combine copies from dirstate if necessary
143 if w is not None:
143 if w is not None:
144 cm = _chain(a, w, cm, _dirstatecopies(w))
144 cm = _chain(a, w, cm, _dirstatecopies(w))
145
145
146 return cm
146 return cm
147
147
148 def _backwardcopies(a, b):
148 def _backwardcopies(a, b):
149 # because the forward mapping is 1:n, we can lose renames here
149 # because the forward mapping is 1:n, we can lose renames here
150 # in particular, we find renames better than copies
150 # in particular, we find renames better than copies
151 f = _forwardcopies(b, a)
151 f = _forwardcopies(b, a)
152 r = {}
152 r = {}
153 for k, v in f.iteritems():
153 for k, v in f.iteritems():
154 r[v] = k
154 r[v] = k
155 return r
155 return r
156
156
157 def pathcopies(x, y):
157 def pathcopies(x, y):
158 '''find {dst@y: src@x} copy mapping for directed compare'''
158 '''find {dst@y: src@x} copy mapping for directed compare'''
159 if x == y or not x or not y:
159 if x == y or not x or not y:
160 return {}
160 return {}
161 a = y.ancestor(x)
161 a = y.ancestor(x)
162 if a == x:
162 if a == x:
163 return _forwardcopies(x, y)
163 return _forwardcopies(x, y)
164 if a == y:
164 if a == y:
165 return _backwardcopies(x, y)
165 return _backwardcopies(x, y)
166 return _chain(x, y, _backwardcopies(x, a), _forwardcopies(a, y))
166 return _chain(x, y, _backwardcopies(x, a), _forwardcopies(a, y))
167
167
168 def mergecopies(repo, c1, c2, ca):
168 def mergecopies(repo, c1, c2, ca):
169 """
169 """
170 Find moves and copies between context c1 and c2 that are relevant
170 Find moves and copies between context c1 and c2 that are relevant
171 for merging.
171 for merging.
172
172
173 Returns two dicts, "copy" and "diverge".
173 Returns two dicts, "copy" and "diverge".
174
174
175 "copy" is a mapping from destination name -> source name,
175 "copy" is a mapping from destination name -> source name,
176 where source is in c1 and destination is in c2 or vice-versa.
176 where source is in c1 and destination is in c2 or vice-versa.
177
177
178 "diverge" is a mapping of source name -> list of destination names
178 "diverge" is a mapping of source name -> list of destination names
179 for divergent renames.
179 for divergent renames.
180
180
181 "renamedelete" is a mapping of source name -> list of destination
181 "renamedelete" is a mapping of source name -> list of destination
182 names for files deleted in c1 that were renamed in c2 or vice-versa.
182 names for files deleted in c1 that were renamed in c2 or vice-versa.
183 """
183 """
184 # avoid silly behavior for update from empty dir
184 # avoid silly behavior for update from empty dir
185 if not c1 or not c2 or c1 == c2:
185 if not c1 or not c2 or c1 == c2:
186 return {}, {}, {}
186 return {}, {}, {}
187
187
188 # avoid silly behavior for parent -> working dir
188 # avoid silly behavior for parent -> working dir
189 if c2.node() is None and c1.node() == repo.dirstate.p1():
189 if c2.node() is None and c1.node() == repo.dirstate.p1():
190 return repo.dirstate.copies(), {}, {}
190 return repo.dirstate.copies(), {}, {}
191
191
192 limit = _findlimit(repo, c1.rev(), c2.rev())
192 limit = _findlimit(repo, c1.rev(), c2.rev())
193 if limit is None:
193 if limit is None:
194 # no common ancestor, no copies
194 # no common ancestor, no copies
195 return {}, {}, {}
195 return {}, {}, {}
196 m1 = c1.manifest()
196 m1 = c1.manifest()
197 m2 = c2.manifest()
197 m2 = c2.manifest()
198 ma = ca.manifest()
198 ma = ca.manifest()
199
199
200 def makectx(f, n):
200 def makectx(f, n):
201 if len(n) != 20: # in a working context?
201 if len(n) != 20: # in a working context?
202 if c1.rev() is None:
202 if c1.rev() is None:
203 return c1.filectx(f)
203 return c1.filectx(f)
204 return c2.filectx(f)
204 return c2.filectx(f)
205 return repo.filectx(f, fileid=n)
205 return repo.filectx(f, fileid=n)
206
206
207 ctx = util.lrucachefunc(makectx)
207 ctx = util.lrucachefunc(makectx)
208 copy = {}
208 copy = {}
209 fullcopy = {}
209 fullcopy = {}
210 diverge = {}
210 diverge = {}
211
211
212 def related(f1, f2, limit):
212 def related(f1, f2, limit):
213 # Walk back to common ancestor to see if the two files originate
213 # Walk back to common ancestor to see if the two files originate
214 # from the same file. Since workingfilectx's rev() is None it messes
214 # from the same file. Since workingfilectx's rev() is None it messes
215 # up the integer comparison logic, hence the pre-step check for
215 # up the integer comparison logic, hence the pre-step check for
216 # None (f1 and f2 can only be workingfilectx's initially).
216 # None (f1 and f2 can only be workingfilectx's initially).
217
217
218 if f1 == f2:
218 if f1 == f2:
219 return f1 # a match
219 return f1 # a match
220
220
221 g1, g2 = f1.ancestors(), f2.ancestors()
221 g1, g2 = f1.ancestors(), f2.ancestors()
222 try:
222 try:
223 f1r, f2r = f1.rev(), f2.rev()
223 f1r, f2r = f1.rev(), f2.rev()
224
224
225 if f1r is None:
225 if f1r is None:
226 f1 = g1.next()
226 f1 = g1.next()
227 if f2r is None:
227 if f2r is None:
228 f2 = g2.next()
228 f2 = g2.next()
229
229
230 while True:
230 while True:
231 f1r, f2r = f1.rev(), f2.rev()
231 f1r, f2r = f1.rev(), f2.rev()
232 if f1r > f2r:
232 if f1r > f2r:
233 f1 = g1.next()
233 f1 = g1.next()
234 elif f2r > f1r:
234 elif f2r > f1r:
235 f2 = g2.next()
235 f2 = g2.next()
236 elif f1 == f2:
236 elif f1 == f2:
237 return f1 # a match
237 return f1 # a match
238 elif f1r == f2r or f1r < limit or f2r < limit:
238 elif f1r == f2r or f1r < limit or f2r < limit:
239 return False # copy no longer relevant
239 return False # copy no longer relevant
240 except StopIteration:
240 except StopIteration:
241 return False
241 return False
242
242
243 def checkcopies(f, m1, m2):
243 def checkcopies(f, m1, m2):
244 '''check possible copies of f from m1 to m2'''
244 '''check possible copies of f from m1 to m2'''
245 of = None
245 of = None
246 seen = set([f])
246 seen = set([f])
247 for oc in ctx(f, m1[f]).ancestors():
247 for oc in ctx(f, m1[f]).ancestors():
248 ocr = oc.rev()
248 ocr = oc.rev()
249 of = oc.path()
249 of = oc.path()
250 if of in seen:
250 if of in seen:
251 # check limit late - grab last rename before
251 # check limit late - grab last rename before
252 if ocr < limit:
252 if ocr < limit:
253 break
253 break
254 continue
254 continue
255 seen.add(of)
255 seen.add(of)
256
256
257 fullcopy[f] = of # remember for dir rename detection
257 fullcopy[f] = of # remember for dir rename detection
258 if of not in m2:
258 if of not in m2:
259 continue # no match, keep looking
259 continue # no match, keep looking
260 if m2[of] == ma.get(of):
260 if m2[of] == ma.get(of):
261 break # no merge needed, quit early
261 break # no merge needed, quit early
262 c2 = ctx(of, m2[of])
262 c2 = ctx(of, m2[of])
263 cr = related(oc, c2, ca.rev())
263 cr = related(oc, c2, ca.rev())
264 if cr and (of == f or of == c2.path()): # non-divergent
264 if cr and (of == f or of == c2.path()): # non-divergent
265 copy[f] = of
265 copy[f] = of
266 of = None
266 of = None
267 break
267 break
268
268
269 if of in ma:
269 if of in ma:
270 diverge.setdefault(of, []).append(f)
270 diverge.setdefault(of, []).append(f)
271
271
272 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
272 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
273
273
274 u1 = _nonoverlap(m1, m2, ma)
274 u1 = _nonoverlap(m1, m2, ma)
275 u2 = _nonoverlap(m2, m1, ma)
275 u2 = _nonoverlap(m2, m1, ma)
276
276
277 if u1:
277 if u1:
278 repo.ui.debug(" unmatched files in local:\n %s\n"
278 repo.ui.debug(" unmatched files in local:\n %s\n"
279 % "\n ".join(u1))
279 % "\n ".join(u1))
280 if u2:
280 if u2:
281 repo.ui.debug(" unmatched files in other:\n %s\n"
281 repo.ui.debug(" unmatched files in other:\n %s\n"
282 % "\n ".join(u2))
282 % "\n ".join(u2))
283
283
284 for f in u1:
284 for f in u1:
285 checkcopies(f, m1, m2)
285 checkcopies(f, m1, m2)
286 for f in u2:
286 for f in u2:
287 checkcopies(f, m2, m1)
287 checkcopies(f, m2, m1)
288
288
289 renamedelete = {}
289 renamedelete = {}
290 renamedelete2 = set()
290 renamedelete2 = set()
291 diverge2 = set()
291 diverge2 = set()
292 for of, fl in diverge.items():
292 for of, fl in diverge.items():
293 if len(fl) == 1 or of in c1 or of in c2:
293 if len(fl) == 1 or of in c1 or of in c2:
294 del diverge[of] # not actually divergent, or not a rename
294 del diverge[of] # not actually divergent, or not a rename
295 if of not in c1 and of not in c2:
295 if of not in c1 and of not in c2:
296 # renamed on one side, deleted on the other side, but filter
296 # renamed on one side, deleted on the other side, but filter
297 # out files that have been renamed and then deleted
297 # out files that have been renamed and then deleted
298 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
298 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
299 renamedelete2.update(fl) # reverse map for below
299 renamedelete2.update(fl) # reverse map for below
300 else:
300 else:
301 diverge2.update(fl) # reverse map for below
301 diverge2.update(fl) # reverse map for below
302
302
303 if fullcopy:
303 if fullcopy:
304 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
304 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
305 "% = renamed and deleted):\n")
305 "% = renamed and deleted):\n")
306 for f in fullcopy:
306 for f in fullcopy:
307 note = ""
307 note = ""
308 if f in copy:
308 if f in copy:
309 note += "*"
309 note += "*"
310 if f in diverge2:
310 if f in diverge2:
311 note += "!"
311 note += "!"
312 if f in renamedelete2:
312 if f in renamedelete2:
313 note += "%"
313 note += "%"
314 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
314 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
315 del diverge2
315 del diverge2
316
316
317 if not fullcopy:
317 if not fullcopy:
318 return copy, diverge, renamedelete
318 return copy, diverge, renamedelete
319
319
320 repo.ui.debug(" checking for directory renames\n")
320 repo.ui.debug(" checking for directory renames\n")
321
321
322 # generate a directory move map
322 # generate a directory move map
323 d1, d2 = c1.dirs(), c2.dirs()
323 d1, d2 = c1.dirs(), c2.dirs()
324 invalid = set([""])
324 d1.add('')
325 d2.add('')
326 invalid = set()
325 dirmove = {}
327 dirmove = {}
326
328
327 # examine each file copy for a potential directory move, which is
329 # examine each file copy for a potential directory move, which is
328 # when all the files in a directory are moved to a new directory
330 # when all the files in a directory are moved to a new directory
329 for dst, src in fullcopy.iteritems():
331 for dst, src in fullcopy.iteritems():
330 dsrc, ddst = _dirname(src), _dirname(dst)
332 dsrc, ddst = _dirname(src), _dirname(dst)
331 if dsrc in invalid:
333 if dsrc in invalid:
332 # already seen to be uninteresting
334 # already seen to be uninteresting
333 continue
335 continue
334 elif dsrc in d1 and ddst in d1:
336 elif dsrc in d1 and ddst in d1:
335 # directory wasn't entirely moved locally
337 # directory wasn't entirely moved locally
336 invalid.add(dsrc)
338 invalid.add(dsrc)
337 elif dsrc in d2 and ddst in d2:
339 elif dsrc in d2 and ddst in d2:
338 # directory wasn't entirely moved remotely
340 # directory wasn't entirely moved remotely
339 invalid.add(dsrc)
341 invalid.add(dsrc)
340 elif dsrc in dirmove and dirmove[dsrc] != ddst:
342 elif dsrc in dirmove and dirmove[dsrc] != ddst:
341 # files from the same directory moved to two different places
343 # files from the same directory moved to two different places
342 invalid.add(dsrc)
344 invalid.add(dsrc)
343 else:
345 else:
344 # looks good so far
346 # looks good so far
345 dirmove[dsrc + "/"] = ddst + "/"
347 dirmove[dsrc + "/"] = ddst + "/"
346
348
347 for i in invalid:
349 for i in invalid:
348 if i in dirmove:
350 if i in dirmove:
349 del dirmove[i]
351 del dirmove[i]
350 del d1, d2, invalid
352 del d1, d2, invalid
351
353
352 if not dirmove:
354 if not dirmove:
353 return copy, diverge, renamedelete
355 return copy, diverge, renamedelete
354
356
355 for d in dirmove:
357 for d in dirmove:
356 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
358 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
357
359
358 # check unaccounted nonoverlapping files against directory moves
360 # check unaccounted nonoverlapping files against directory moves
359 for f in u1 + u2:
361 for f in u1 + u2:
360 if f not in fullcopy:
362 if f not in fullcopy:
361 for d in dirmove:
363 for d in dirmove:
362 if f.startswith(d):
364 if f.startswith(d):
363 # new file added in a directory that was moved, move it
365 # new file added in a directory that was moved, move it
364 df = dirmove[d] + f[len(d):]
366 df = dirmove[d] + f[len(d):]
365 if df not in copy:
367 if df not in copy:
366 copy[f] = df
368 copy[f] = df
367 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
369 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
368 break
370 break
369
371
370 return copy, diverge, renamedelete
372 return copy, diverge, renamedelete
General Comments 0
You need to be logged in to leave comments. Login now