##// END OF EJS Templates
copies: speed up copy detection...
Matt Mackall -
r10262:eb243551 stable
parent child Browse files
Show More
@@ -1,832 +1,843
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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from node import nullid, nullrev, short, hex
8 from node import nullid, nullrev, short, hex
9 from i18n import _
9 from i18n import _
10 import ancestor, bdiff, error, util, subrepo
10 import ancestor, bdiff, error, util, subrepo
11 import os, errno
11 import os, errno
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 class changectx(object):
15 class changectx(object):
16 """A changecontext object makes access to data related to a particular
16 """A changecontext object makes access to data related to a particular
17 changeset convenient."""
17 changeset convenient."""
18 def __init__(self, repo, changeid=''):
18 def __init__(self, repo, changeid=''):
19 """changeid is a revision number, node, or tag"""
19 """changeid is a revision number, node, or tag"""
20 if changeid == '':
20 if changeid == '':
21 changeid = '.'
21 changeid = '.'
22 self._repo = repo
22 self._repo = repo
23 if isinstance(changeid, (long, int)):
23 if isinstance(changeid, (long, int)):
24 self._rev = changeid
24 self._rev = changeid
25 self._node = self._repo.changelog.node(changeid)
25 self._node = self._repo.changelog.node(changeid)
26 else:
26 else:
27 self._node = self._repo.lookup(changeid)
27 self._node = self._repo.lookup(changeid)
28 self._rev = self._repo.changelog.rev(self._node)
28 self._rev = self._repo.changelog.rev(self._node)
29
29
30 def __str__(self):
30 def __str__(self):
31 return short(self.node())
31 return short(self.node())
32
32
33 def __int__(self):
33 def __int__(self):
34 return self.rev()
34 return self.rev()
35
35
36 def __repr__(self):
36 def __repr__(self):
37 return "<changectx %s>" % str(self)
37 return "<changectx %s>" % str(self)
38
38
39 def __hash__(self):
39 def __hash__(self):
40 try:
40 try:
41 return hash(self._rev)
41 return hash(self._rev)
42 except AttributeError:
42 except AttributeError:
43 return id(self)
43 return id(self)
44
44
45 def __eq__(self, other):
45 def __eq__(self, other):
46 try:
46 try:
47 return self._rev == other._rev
47 return self._rev == other._rev
48 except AttributeError:
48 except AttributeError:
49 return False
49 return False
50
50
51 def __ne__(self, other):
51 def __ne__(self, other):
52 return not (self == other)
52 return not (self == other)
53
53
54 def __nonzero__(self):
54 def __nonzero__(self):
55 return self._rev != nullrev
55 return self._rev != nullrev
56
56
57 @propertycache
57 @propertycache
58 def _changeset(self):
58 def _changeset(self):
59 return self._repo.changelog.read(self.node())
59 return self._repo.changelog.read(self.node())
60
60
61 @propertycache
61 @propertycache
62 def _manifest(self):
62 def _manifest(self):
63 return self._repo.manifest.read(self._changeset[0])
63 return self._repo.manifest.read(self._changeset[0])
64
64
65 @propertycache
65 @propertycache
66 def _manifestdelta(self):
66 def _manifestdelta(self):
67 return self._repo.manifest.readdelta(self._changeset[0])
67 return self._repo.manifest.readdelta(self._changeset[0])
68
68
69 @propertycache
69 @propertycache
70 def _parents(self):
70 def _parents(self):
71 p = self._repo.changelog.parentrevs(self._rev)
71 p = self._repo.changelog.parentrevs(self._rev)
72 if p[1] == nullrev:
72 if p[1] == nullrev:
73 p = p[:-1]
73 p = p[:-1]
74 return [changectx(self._repo, x) for x in p]
74 return [changectx(self._repo, x) for x in p]
75
75
76 @propertycache
76 @propertycache
77 def substate(self):
77 def substate(self):
78 return subrepo.state(self)
78 return subrepo.state(self)
79
79
80 def __contains__(self, key):
80 def __contains__(self, key):
81 return key in self._manifest
81 return key in self._manifest
82
82
83 def __getitem__(self, key):
83 def __getitem__(self, key):
84 return self.filectx(key)
84 return self.filectx(key)
85
85
86 def __iter__(self):
86 def __iter__(self):
87 for f in sorted(self._manifest):
87 for f in sorted(self._manifest):
88 yield f
88 yield f
89
89
90 def changeset(self): return self._changeset
90 def changeset(self): return self._changeset
91 def manifest(self): return self._manifest
91 def manifest(self): return self._manifest
92 def manifestnode(self): return self._changeset[0]
92 def manifestnode(self): return self._changeset[0]
93
93
94 def rev(self): return self._rev
94 def rev(self): return self._rev
95 def node(self): return self._node
95 def node(self): return self._node
96 def hex(self): return hex(self._node)
96 def hex(self): return hex(self._node)
97 def user(self): return self._changeset[1]
97 def user(self): return self._changeset[1]
98 def date(self): return self._changeset[2]
98 def date(self): return self._changeset[2]
99 def files(self): return self._changeset[3]
99 def files(self): return self._changeset[3]
100 def description(self): return self._changeset[4]
100 def description(self): return self._changeset[4]
101 def branch(self): return self._changeset[5].get("branch")
101 def branch(self): return self._changeset[5].get("branch")
102 def extra(self): return self._changeset[5]
102 def extra(self): return self._changeset[5]
103 def tags(self): return self._repo.nodetags(self._node)
103 def tags(self): return self._repo.nodetags(self._node)
104
104
105 def parents(self):
105 def parents(self):
106 """return contexts for each parent changeset"""
106 """return contexts for each parent changeset"""
107 return self._parents
107 return self._parents
108
108
109 def p1(self):
109 def p1(self):
110 return self._parents[0]
110 return self._parents[0]
111
111
112 def p2(self):
112 def p2(self):
113 if len(self._parents) == 2:
113 if len(self._parents) == 2:
114 return self._parents[1]
114 return self._parents[1]
115 return changectx(self._repo, -1)
115 return changectx(self._repo, -1)
116
116
117 def children(self):
117 def children(self):
118 """return contexts for each child changeset"""
118 """return contexts for each child changeset"""
119 c = self._repo.changelog.children(self._node)
119 c = self._repo.changelog.children(self._node)
120 return [changectx(self._repo, x) for x in c]
120 return [changectx(self._repo, x) for x in c]
121
121
122 def ancestors(self):
122 def ancestors(self):
123 for a in self._repo.changelog.ancestors(self._rev):
123 for a in self._repo.changelog.ancestors(self._rev):
124 yield changectx(self._repo, a)
124 yield changectx(self._repo, a)
125
125
126 def descendants(self):
126 def descendants(self):
127 for d in self._repo.changelog.descendants(self._rev):
127 for d in self._repo.changelog.descendants(self._rev):
128 yield changectx(self._repo, d)
128 yield changectx(self._repo, d)
129
129
130 def _fileinfo(self, path):
130 def _fileinfo(self, path):
131 if '_manifest' in self.__dict__:
131 if '_manifest' in self.__dict__:
132 try:
132 try:
133 return self._manifest[path], self._manifest.flags(path)
133 return self._manifest[path], self._manifest.flags(path)
134 except KeyError:
134 except KeyError:
135 raise error.LookupError(self._node, path,
135 raise error.LookupError(self._node, path,
136 _('not found in manifest'))
136 _('not found in manifest'))
137 if '_manifestdelta' in self.__dict__ or path in self.files():
137 if '_manifestdelta' in self.__dict__ or path in self.files():
138 if path in self._manifestdelta:
138 if path in self._manifestdelta:
139 return self._manifestdelta[path], self._manifestdelta.flags(path)
139 return self._manifestdelta[path], self._manifestdelta.flags(path)
140 node, flag = self._repo.manifest.find(self._changeset[0], path)
140 node, flag = self._repo.manifest.find(self._changeset[0], path)
141 if not node:
141 if not node:
142 raise error.LookupError(self._node, path,
142 raise error.LookupError(self._node, path,
143 _('not found in manifest'))
143 _('not found in manifest'))
144
144
145 return node, flag
145 return node, flag
146
146
147 def filenode(self, path):
147 def filenode(self, path):
148 return self._fileinfo(path)[0]
148 return self._fileinfo(path)[0]
149
149
150 def flags(self, path):
150 def flags(self, path):
151 try:
151 try:
152 return self._fileinfo(path)[1]
152 return self._fileinfo(path)[1]
153 except error.LookupError:
153 except error.LookupError:
154 return ''
154 return ''
155
155
156 def filectx(self, path, fileid=None, filelog=None):
156 def filectx(self, path, fileid=None, filelog=None):
157 """get a file context from this changeset"""
157 """get a file context from this changeset"""
158 if fileid is None:
158 if fileid is None:
159 fileid = self.filenode(path)
159 fileid = self.filenode(path)
160 return filectx(self._repo, path, fileid=fileid,
160 return filectx(self._repo, path, fileid=fileid,
161 changectx=self, filelog=filelog)
161 changectx=self, filelog=filelog)
162
162
163 def ancestor(self, c2):
163 def ancestor(self, c2):
164 """
164 """
165 return the ancestor context of self and c2
165 return the ancestor context of self and c2
166 """
166 """
167 # deal with workingctxs
167 # deal with workingctxs
168 n2 = c2._node
168 n2 = c2._node
169 if n2 == None:
169 if n2 == None:
170 n2 = c2._parents[0]._node
170 n2 = c2._parents[0]._node
171 n = self._repo.changelog.ancestor(self._node, n2)
171 n = self._repo.changelog.ancestor(self._node, n2)
172 return changectx(self._repo, n)
172 return changectx(self._repo, n)
173
173
174 def walk(self, match):
174 def walk(self, match):
175 fset = set(match.files())
175 fset = set(match.files())
176 # for dirstate.walk, files=['.'] means "walk the whole tree".
176 # for dirstate.walk, files=['.'] means "walk the whole tree".
177 # follow that here, too
177 # follow that here, too
178 fset.discard('.')
178 fset.discard('.')
179 for fn in self:
179 for fn in self:
180 for ffn in fset:
180 for ffn in fset:
181 # match if the file is the exact name or a directory
181 # match if the file is the exact name or a directory
182 if ffn == fn or fn.startswith("%s/" % ffn):
182 if ffn == fn or fn.startswith("%s/" % ffn):
183 fset.remove(ffn)
183 fset.remove(ffn)
184 break
184 break
185 if match(fn):
185 if match(fn):
186 yield fn
186 yield fn
187 for fn in sorted(fset):
187 for fn in sorted(fset):
188 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
188 if match.bad(fn, 'No such file in rev ' + str(self)) and match(fn):
189 yield fn
189 yield fn
190
190
191 def sub(self, path):
191 def sub(self, path):
192 return subrepo.subrepo(self, path)
192 return subrepo.subrepo(self, path)
193
193
194 class filectx(object):
194 class filectx(object):
195 """A filecontext object makes access to data related to a particular
195 """A filecontext object makes access to data related to a particular
196 filerevision convenient."""
196 filerevision convenient."""
197 def __init__(self, repo, path, changeid=None, fileid=None,
197 def __init__(self, repo, path, changeid=None, fileid=None,
198 filelog=None, changectx=None):
198 filelog=None, changectx=None):
199 """changeid can be a changeset revision, node, or tag.
199 """changeid can be a changeset revision, node, or tag.
200 fileid can be a file revision or node."""
200 fileid can be a file revision or node."""
201 self._repo = repo
201 self._repo = repo
202 self._path = path
202 self._path = path
203
203
204 assert (changeid is not None
204 assert (changeid is not None
205 or fileid is not None
205 or fileid is not None
206 or changectx is not None), \
206 or changectx is not None), \
207 ("bad args: changeid=%r, fileid=%r, changectx=%r"
207 ("bad args: changeid=%r, fileid=%r, changectx=%r"
208 % (changeid, fileid, changectx))
208 % (changeid, fileid, changectx))
209
209
210 if filelog:
210 if filelog:
211 self._filelog = filelog
211 self._filelog = filelog
212
212
213 if changeid is not None:
213 if changeid is not None:
214 self._changeid = changeid
214 self._changeid = changeid
215 if changectx is not None:
215 if changectx is not None:
216 self._changectx = changectx
216 self._changectx = changectx
217 if fileid is not None:
217 if fileid is not None:
218 self._fileid = fileid
218 self._fileid = fileid
219
219
220 @propertycache
220 @propertycache
221 def _changectx(self):
221 def _changectx(self):
222 return changectx(self._repo, self._changeid)
222 return changectx(self._repo, self._changeid)
223
223
224 @propertycache
224 @propertycache
225 def _filelog(self):
225 def _filelog(self):
226 return self._repo.file(self._path)
226 return self._repo.file(self._path)
227
227
228 @propertycache
228 @propertycache
229 def _changeid(self):
229 def _changeid(self):
230 if '_changectx' in self.__dict__:
230 if '_changectx' in self.__dict__:
231 return self._changectx.rev()
231 return self._changectx.rev()
232 else:
232 else:
233 return self._filelog.linkrev(self._filerev)
233 return self._filelog.linkrev(self._filerev)
234
234
235 @propertycache
235 @propertycache
236 def _filenode(self):
236 def _filenode(self):
237 if '_fileid' in self.__dict__:
237 if '_fileid' in self.__dict__:
238 return self._filelog.lookup(self._fileid)
238 return self._filelog.lookup(self._fileid)
239 else:
239 else:
240 return self._changectx.filenode(self._path)
240 return self._changectx.filenode(self._path)
241
241
242 @propertycache
242 @propertycache
243 def _filerev(self):
243 def _filerev(self):
244 return self._filelog.rev(self._filenode)
244 return self._filelog.rev(self._filenode)
245
245
246 @propertycache
246 @propertycache
247 def _repopath(self):
247 def _repopath(self):
248 return self._path
248 return self._path
249
249
250 def __nonzero__(self):
250 def __nonzero__(self):
251 try:
251 try:
252 self._filenode
252 self._filenode
253 return True
253 return True
254 except error.LookupError:
254 except error.LookupError:
255 # file is missing
255 # file is missing
256 return False
256 return False
257
257
258 def __str__(self):
258 def __str__(self):
259 return "%s@%s" % (self.path(), short(self.node()))
259 return "%s@%s" % (self.path(), short(self.node()))
260
260
261 def __repr__(self):
261 def __repr__(self):
262 return "<filectx %s>" % str(self)
262 return "<filectx %s>" % str(self)
263
263
264 def __hash__(self):
264 def __hash__(self):
265 try:
265 try:
266 return hash((self._path, self._fileid))
266 return hash((self._path, self._fileid))
267 except AttributeError:
267 except AttributeError:
268 return id(self)
268 return id(self)
269
269
270 def __eq__(self, other):
270 def __eq__(self, other):
271 try:
271 try:
272 return (self._path == other._path
272 return (self._path == other._path
273 and self._fileid == other._fileid)
273 and self._fileid == other._fileid)
274 except AttributeError:
274 except AttributeError:
275 return False
275 return False
276
276
277 def __ne__(self, other):
277 def __ne__(self, other):
278 return not (self == other)
278 return not (self == other)
279
279
280 def filectx(self, fileid):
280 def filectx(self, fileid):
281 '''opens an arbitrary revision of the file without
281 '''opens an arbitrary revision of the file without
282 opening a new filelog'''
282 opening a new filelog'''
283 return filectx(self._repo, self._path, fileid=fileid,
283 return filectx(self._repo, self._path, fileid=fileid,
284 filelog=self._filelog)
284 filelog=self._filelog)
285
285
286 def filerev(self): return self._filerev
286 def filerev(self): return self._filerev
287 def filenode(self): return self._filenode
287 def filenode(self): return self._filenode
288 def flags(self): return self._changectx.flags(self._path)
288 def flags(self): return self._changectx.flags(self._path)
289 def filelog(self): return self._filelog
289 def filelog(self): return self._filelog
290
290
291 def rev(self):
291 def rev(self):
292 if '_changectx' in self.__dict__:
292 if '_changectx' in self.__dict__:
293 return self._changectx.rev()
293 return self._changectx.rev()
294 if '_changeid' in self.__dict__:
294 if '_changeid' in self.__dict__:
295 return self._changectx.rev()
295 return self._changectx.rev()
296 return self._filelog.linkrev(self._filerev)
296 return self._filelog.linkrev(self._filerev)
297
297
298 def linkrev(self): return self._filelog.linkrev(self._filerev)
298 def linkrev(self): return self._filelog.linkrev(self._filerev)
299 def node(self): return self._changectx.node()
299 def node(self): return self._changectx.node()
300 def hex(self): return hex(self.node())
300 def hex(self): return hex(self.node())
301 def user(self): return self._changectx.user()
301 def user(self): return self._changectx.user()
302 def date(self): return self._changectx.date()
302 def date(self): return self._changectx.date()
303 def files(self): return self._changectx.files()
303 def files(self): return self._changectx.files()
304 def description(self): return self._changectx.description()
304 def description(self): return self._changectx.description()
305 def branch(self): return self._changectx.branch()
305 def branch(self): return self._changectx.branch()
306 def extra(self): return self._changectx.extra()
306 def extra(self): return self._changectx.extra()
307 def manifest(self): return self._changectx.manifest()
307 def manifest(self): return self._changectx.manifest()
308 def changectx(self): return self._changectx
308 def changectx(self): return self._changectx
309
309
310 def data(self): return self._filelog.read(self._filenode)
310 def data(self): return self._filelog.read(self._filenode)
311 def path(self): return self._path
311 def path(self): return self._path
312 def size(self): return self._filelog.size(self._filerev)
312 def size(self): return self._filelog.size(self._filerev)
313
313
314 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
314 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
315
315
316 def renamed(self):
316 def renamed(self):
317 """check if file was actually renamed in this changeset revision
317 """check if file was actually renamed in this changeset revision
318
318
319 If rename logged in file revision, we report copy for changeset only
319 If rename logged in file revision, we report copy for changeset only
320 if file revisions linkrev points back to the changeset in question
320 if file revisions linkrev points back to the changeset in question
321 or both changeset parents contain different file revisions.
321 or both changeset parents contain different file revisions.
322 """
322 """
323
323
324 renamed = self._filelog.renamed(self._filenode)
324 renamed = self._filelog.renamed(self._filenode)
325 if not renamed:
325 if not renamed:
326 return renamed
326 return renamed
327
327
328 if self.rev() == self.linkrev():
328 if self.rev() == self.linkrev():
329 return renamed
329 return renamed
330
330
331 name = self.path()
331 name = self.path()
332 fnode = self._filenode
332 fnode = self._filenode
333 for p in self._changectx.parents():
333 for p in self._changectx.parents():
334 try:
334 try:
335 if fnode == p.filenode(name):
335 if fnode == p.filenode(name):
336 return None
336 return None
337 except error.LookupError:
337 except error.LookupError:
338 pass
338 pass
339 return renamed
339 return renamed
340
340
341 def parents(self):
341 def parents(self):
342 p = self._path
342 p = self._path
343 fl = self._filelog
343 fl = self._filelog
344 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
344 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
345
345
346 r = self._filelog.renamed(self._filenode)
346 r = self._filelog.renamed(self._filenode)
347 if r:
347 if r:
348 pl[0] = (r[0], r[1], None)
348 pl[0] = (r[0], r[1], None)
349
349
350 return [filectx(self._repo, p, fileid=n, filelog=l)
350 return [filectx(self._repo, p, fileid=n, filelog=l)
351 for p,n,l in pl if n != nullid]
351 for p,n,l in pl if n != nullid]
352
352
353 def children(self):
353 def children(self):
354 # hard for renames
354 # hard for renames
355 c = self._filelog.children(self._filenode)
355 c = self._filelog.children(self._filenode)
356 return [filectx(self._repo, self._path, fileid=x,
356 return [filectx(self._repo, self._path, fileid=x,
357 filelog=self._filelog) for x in c]
357 filelog=self._filelog) for x in c]
358
358
359 def annotate(self, follow=False, linenumber=None):
359 def annotate(self, follow=False, linenumber=None):
360 '''returns a list of tuples of (ctx, line) for each line
360 '''returns a list of tuples of (ctx, line) for each line
361 in the file, where ctx is the filectx of the node where
361 in the file, where ctx is the filectx of the node where
362 that line was last changed.
362 that line was last changed.
363 This returns tuples of ((ctx, linenumber), line) for each line,
363 This returns tuples of ((ctx, linenumber), line) for each line,
364 if "linenumber" parameter is NOT "None".
364 if "linenumber" parameter is NOT "None".
365 In such tuples, linenumber means one at the first appearance
365 In such tuples, linenumber means one at the first appearance
366 in the managed file.
366 in the managed file.
367 To reduce annotation cost,
367 To reduce annotation cost,
368 this returns fixed value(False is used) as linenumber,
368 this returns fixed value(False is used) as linenumber,
369 if "linenumber" parameter is "False".'''
369 if "linenumber" parameter is "False".'''
370
370
371 def decorate_compat(text, rev):
371 def decorate_compat(text, rev):
372 return ([rev] * len(text.splitlines()), text)
372 return ([rev] * len(text.splitlines()), text)
373
373
374 def without_linenumber(text, rev):
374 def without_linenumber(text, rev):
375 return ([(rev, False)] * len(text.splitlines()), text)
375 return ([(rev, False)] * len(text.splitlines()), text)
376
376
377 def with_linenumber(text, rev):
377 def with_linenumber(text, rev):
378 size = len(text.splitlines())
378 size = len(text.splitlines())
379 return ([(rev, i) for i in xrange(1, size + 1)], text)
379 return ([(rev, i) for i in xrange(1, size + 1)], text)
380
380
381 decorate = (((linenumber is None) and decorate_compat) or
381 decorate = (((linenumber is None) and decorate_compat) or
382 (linenumber and with_linenumber) or
382 (linenumber and with_linenumber) or
383 without_linenumber)
383 without_linenumber)
384
384
385 def pair(parent, child):
385 def pair(parent, child):
386 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
386 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
387 child[0][b1:b2] = parent[0][a1:a2]
387 child[0][b1:b2] = parent[0][a1:a2]
388 return child
388 return child
389
389
390 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
390 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
391 def getctx(path, fileid):
391 def getctx(path, fileid):
392 log = path == self._path and self._filelog or getlog(path)
392 log = path == self._path and self._filelog or getlog(path)
393 return filectx(self._repo, path, fileid=fileid, filelog=log)
393 return filectx(self._repo, path, fileid=fileid, filelog=log)
394 getctx = util.lrucachefunc(getctx)
394 getctx = util.lrucachefunc(getctx)
395
395
396 def parents(f):
396 def parents(f):
397 # we want to reuse filectx objects as much as possible
397 # we want to reuse filectx objects as much as possible
398 p = f._path
398 p = f._path
399 if f._filerev is None: # working dir
399 if f._filerev is None: # working dir
400 pl = [(n.path(), n.filerev()) for n in f.parents()]
400 pl = [(n.path(), n.filerev()) for n in f.parents()]
401 else:
401 else:
402 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
402 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
403
403
404 if follow:
404 if follow:
405 r = f.renamed()
405 r = f.renamed()
406 if r:
406 if r:
407 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
407 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
408
408
409 return [getctx(p, n) for p, n in pl if n != nullrev]
409 return [getctx(p, n) for p, n in pl if n != nullrev]
410
410
411 # use linkrev to find the first changeset where self appeared
411 # use linkrev to find the first changeset where self appeared
412 if self.rev() != self.linkrev():
412 if self.rev() != self.linkrev():
413 base = self.filectx(self.filerev())
413 base = self.filectx(self.filerev())
414 else:
414 else:
415 base = self
415 base = self
416
416
417 # find all ancestors
417 # find all ancestors
418 needed = {base: 1}
418 needed = {base: 1}
419 visit = [base]
419 visit = [base]
420 files = [base._path]
420 files = [base._path]
421 while visit:
421 while visit:
422 f = visit.pop(0)
422 f = visit.pop(0)
423 for p in parents(f):
423 for p in parents(f):
424 if p not in needed:
424 if p not in needed:
425 needed[p] = 1
425 needed[p] = 1
426 visit.append(p)
426 visit.append(p)
427 if p._path not in files:
427 if p._path not in files:
428 files.append(p._path)
428 files.append(p._path)
429 else:
429 else:
430 # count how many times we'll use this
430 # count how many times we'll use this
431 needed[p] += 1
431 needed[p] += 1
432
432
433 # sort by revision (per file) which is a topological order
433 # sort by revision (per file) which is a topological order
434 visit = []
434 visit = []
435 for f in files:
435 for f in files:
436 fn = [(n.rev(), n) for n in needed if n._path == f]
436 fn = [(n.rev(), n) for n in needed if n._path == f]
437 visit.extend(fn)
437 visit.extend(fn)
438
438
439 hist = {}
439 hist = {}
440 for r, f in sorted(visit):
440 for r, f in sorted(visit):
441 curr = decorate(f.data(), f)
441 curr = decorate(f.data(), f)
442 for p in parents(f):
442 for p in parents(f):
443 if p != nullid:
443 if p != nullid:
444 curr = pair(hist[p], curr)
444 curr = pair(hist[p], curr)
445 # trim the history of unneeded revs
445 # trim the history of unneeded revs
446 needed[p] -= 1
446 needed[p] -= 1
447 if not needed[p]:
447 if not needed[p]:
448 del hist[p]
448 del hist[p]
449 hist[f] = curr
449 hist[f] = curr
450
450
451 return zip(hist[f][0], hist[f][1].splitlines(True))
451 return zip(hist[f][0], hist[f][1].splitlines(True))
452
452
453 def ancestor(self, fc2):
453 def ancestor(self, fc2):
454 """
454 """
455 find the common ancestor file context, if any, of self, and fc2
455 find the common ancestor file context, if any, of self, and fc2
456 """
456 """
457
457
458 actx = self.changectx().ancestor(fc2.changectx())
458 actx = self.changectx().ancestor(fc2.changectx())
459
459
460 # the trivial case: changesets are unrelated, files must be too
460 # the trivial case: changesets are unrelated, files must be too
461 if not actx:
461 if not actx:
462 return None
462 return None
463
463
464 # the easy case: no (relevant) renames
464 # the easy case: no (relevant) renames
465 if fc2.path() == self.path() and self.path() in actx:
465 if fc2.path() == self.path() and self.path() in actx:
466 return actx[self.path()]
466 return actx[self.path()]
467 acache = {}
467 acache = {}
468
468
469 # prime the ancestor cache for the working directory
469 # prime the ancestor cache for the working directory
470 for c in (self, fc2):
470 for c in (self, fc2):
471 if c._filerev is None:
471 if c._filerev is None:
472 pl = [(n.path(), n.filenode()) for n in c.parents()]
472 pl = [(n.path(), n.filenode()) for n in c.parents()]
473 acache[(c._path, None)] = pl
473 acache[(c._path, None)] = pl
474
474
475 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
475 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
476 def parents(vertex):
476 def parents(vertex):
477 if vertex in acache:
477 if vertex in acache:
478 return acache[vertex]
478 return acache[vertex]
479 f, n = vertex
479 f, n = vertex
480 if f not in flcache:
480 if f not in flcache:
481 flcache[f] = self._repo.file(f)
481 flcache[f] = self._repo.file(f)
482 fl = flcache[f]
482 fl = flcache[f]
483 pl = [(f, p) for p in fl.parents(n) if p != nullid]
483 pl = [(f, p) for p in fl.parents(n) if p != nullid]
484 re = fl.renamed(n)
484 re = fl.renamed(n)
485 if re:
485 if re:
486 pl.append(re)
486 pl.append(re)
487 acache[vertex] = pl
487 acache[vertex] = pl
488 return pl
488 return pl
489
489
490 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
490 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
491 v = ancestor.ancestor(a, b, parents)
491 v = ancestor.ancestor(a, b, parents)
492 if v:
492 if v:
493 f, n = v
493 f, n = v
494 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
494 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
495
495
496 return None
496 return None
497
497
498 def ancestors(self):
499 seen = set(str(self))
500 visit = [self]
501 while visit:
502 for parent in visit.pop(0).parents():
503 s = str(parent)
504 if s not in seen:
505 visit.append(parent)
506 seen.add(s)
507 yield parent
508
498 class workingctx(changectx):
509 class workingctx(changectx):
499 """A workingctx object makes access to data related to
510 """A workingctx object makes access to data related to
500 the current working directory convenient.
511 the current working directory convenient.
501 parents - a pair of parent nodeids, or None to use the dirstate.
512 parents - a pair of parent nodeids, or None to use the dirstate.
502 date - any valid date string or (unixtime, offset), or None.
513 date - any valid date string or (unixtime, offset), or None.
503 user - username string, or None.
514 user - username string, or None.
504 extra - a dictionary of extra values, or None.
515 extra - a dictionary of extra values, or None.
505 changes - a list of file lists as returned by localrepo.status()
516 changes - a list of file lists as returned by localrepo.status()
506 or None to use the repository status.
517 or None to use the repository status.
507 """
518 """
508 def __init__(self, repo, parents=None, text="", user=None, date=None,
519 def __init__(self, repo, parents=None, text="", user=None, date=None,
509 extra=None, changes=None):
520 extra=None, changes=None):
510 self._repo = repo
521 self._repo = repo
511 self._rev = None
522 self._rev = None
512 self._node = None
523 self._node = None
513 self._text = text
524 self._text = text
514 if date:
525 if date:
515 self._date = util.parsedate(date)
526 self._date = util.parsedate(date)
516 if user:
527 if user:
517 self._user = user
528 self._user = user
518 if parents:
529 if parents:
519 self._parents = [changectx(self._repo, p) for p in parents]
530 self._parents = [changectx(self._repo, p) for p in parents]
520 if changes:
531 if changes:
521 self._status = list(changes)
532 self._status = list(changes)
522
533
523 self._extra = {}
534 self._extra = {}
524 if extra:
535 if extra:
525 self._extra = extra.copy()
536 self._extra = extra.copy()
526 if 'branch' not in self._extra:
537 if 'branch' not in self._extra:
527 branch = self._repo.dirstate.branch()
538 branch = self._repo.dirstate.branch()
528 try:
539 try:
529 branch = branch.decode('UTF-8').encode('UTF-8')
540 branch = branch.decode('UTF-8').encode('UTF-8')
530 except UnicodeDecodeError:
541 except UnicodeDecodeError:
531 raise util.Abort(_('branch name not in UTF-8!'))
542 raise util.Abort(_('branch name not in UTF-8!'))
532 self._extra['branch'] = branch
543 self._extra['branch'] = branch
533 if self._extra['branch'] == '':
544 if self._extra['branch'] == '':
534 self._extra['branch'] = 'default'
545 self._extra['branch'] = 'default'
535
546
536 def __str__(self):
547 def __str__(self):
537 return str(self._parents[0]) + "+"
548 return str(self._parents[0]) + "+"
538
549
539 def __nonzero__(self):
550 def __nonzero__(self):
540 return True
551 return True
541
552
542 def __contains__(self, key):
553 def __contains__(self, key):
543 return self._repo.dirstate[key] not in "?r"
554 return self._repo.dirstate[key] not in "?r"
544
555
545 @propertycache
556 @propertycache
546 def _manifest(self):
557 def _manifest(self):
547 """generate a manifest corresponding to the working directory"""
558 """generate a manifest corresponding to the working directory"""
548
559
549 man = self._parents[0].manifest().copy()
560 man = self._parents[0].manifest().copy()
550 copied = self._repo.dirstate.copies()
561 copied = self._repo.dirstate.copies()
551 cf = lambda x: man.flags(copied.get(x, x))
562 cf = lambda x: man.flags(copied.get(x, x))
552 ff = self._repo.dirstate.flagfunc(cf)
563 ff = self._repo.dirstate.flagfunc(cf)
553 modified, added, removed, deleted, unknown = self._status[:5]
564 modified, added, removed, deleted, unknown = self._status[:5]
554 for i, l in (("a", added), ("m", modified), ("u", unknown)):
565 for i, l in (("a", added), ("m", modified), ("u", unknown)):
555 for f in l:
566 for f in l:
556 man[f] = man.get(copied.get(f, f), nullid) + i
567 man[f] = man.get(copied.get(f, f), nullid) + i
557 try:
568 try:
558 man.set(f, ff(f))
569 man.set(f, ff(f))
559 except OSError:
570 except OSError:
560 pass
571 pass
561
572
562 for f in deleted + removed:
573 for f in deleted + removed:
563 if f in man:
574 if f in man:
564 del man[f]
575 del man[f]
565
576
566 return man
577 return man
567
578
568 @propertycache
579 @propertycache
569 def _status(self):
580 def _status(self):
570 return self._repo.status(unknown=True)
581 return self._repo.status(unknown=True)
571
582
572 @propertycache
583 @propertycache
573 def _user(self):
584 def _user(self):
574 return self._repo.ui.username()
585 return self._repo.ui.username()
575
586
576 @propertycache
587 @propertycache
577 def _date(self):
588 def _date(self):
578 return util.makedate()
589 return util.makedate()
579
590
580 @propertycache
591 @propertycache
581 def _parents(self):
592 def _parents(self):
582 p = self._repo.dirstate.parents()
593 p = self._repo.dirstate.parents()
583 if p[1] == nullid:
594 if p[1] == nullid:
584 p = p[:-1]
595 p = p[:-1]
585 self._parents = [changectx(self._repo, x) for x in p]
596 self._parents = [changectx(self._repo, x) for x in p]
586 return self._parents
597 return self._parents
587
598
588 def manifest(self): return self._manifest
599 def manifest(self): return self._manifest
589
600
590 def user(self): return self._user or self._repo.ui.username()
601 def user(self): return self._user or self._repo.ui.username()
591 def date(self): return self._date
602 def date(self): return self._date
592 def description(self): return self._text
603 def description(self): return self._text
593 def files(self):
604 def files(self):
594 return sorted(self._status[0] + self._status[1] + self._status[2])
605 return sorted(self._status[0] + self._status[1] + self._status[2])
595
606
596 def modified(self): return self._status[0]
607 def modified(self): return self._status[0]
597 def added(self): return self._status[1]
608 def added(self): return self._status[1]
598 def removed(self): return self._status[2]
609 def removed(self): return self._status[2]
599 def deleted(self): return self._status[3]
610 def deleted(self): return self._status[3]
600 def unknown(self): return self._status[4]
611 def unknown(self): return self._status[4]
601 def clean(self): return self._status[5]
612 def clean(self): return self._status[5]
602 def branch(self): return self._extra['branch']
613 def branch(self): return self._extra['branch']
603 def extra(self): return self._extra
614 def extra(self): return self._extra
604
615
605 def tags(self):
616 def tags(self):
606 t = []
617 t = []
607 [t.extend(p.tags()) for p in self.parents()]
618 [t.extend(p.tags()) for p in self.parents()]
608 return t
619 return t
609
620
610 def children(self):
621 def children(self):
611 return []
622 return []
612
623
613 def flags(self, path):
624 def flags(self, path):
614 if '_manifest' in self.__dict__:
625 if '_manifest' in self.__dict__:
615 try:
626 try:
616 return self._manifest.flags(path)
627 return self._manifest.flags(path)
617 except KeyError:
628 except KeyError:
618 return ''
629 return ''
619
630
620 pnode = self._parents[0].changeset()[0]
631 pnode = self._parents[0].changeset()[0]
621 orig = self._repo.dirstate.copies().get(path, path)
632 orig = self._repo.dirstate.copies().get(path, path)
622 node, flag = self._repo.manifest.find(pnode, orig)
633 node, flag = self._repo.manifest.find(pnode, orig)
623 try:
634 try:
624 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
635 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
625 return ff(path)
636 return ff(path)
626 except OSError:
637 except OSError:
627 pass
638 pass
628
639
629 if not node or path in self.deleted() or path in self.removed():
640 if not node or path in self.deleted() or path in self.removed():
630 return ''
641 return ''
631 return flag
642 return flag
632
643
633 def filectx(self, path, filelog=None):
644 def filectx(self, path, filelog=None):
634 """get a file context from the working directory"""
645 """get a file context from the working directory"""
635 return workingfilectx(self._repo, path, workingctx=self,
646 return workingfilectx(self._repo, path, workingctx=self,
636 filelog=filelog)
647 filelog=filelog)
637
648
638 def ancestor(self, c2):
649 def ancestor(self, c2):
639 """return the ancestor context of self and c2"""
650 """return the ancestor context of self and c2"""
640 return self._parents[0].ancestor(c2) # punt on two parents for now
651 return self._parents[0].ancestor(c2) # punt on two parents for now
641
652
642 def walk(self, match):
653 def walk(self, match):
643 return sorted(self._repo.dirstate.walk(match, True, False))
654 return sorted(self._repo.dirstate.walk(match, True, False))
644
655
645 def dirty(self, missing=False):
656 def dirty(self, missing=False):
646 "check whether a working directory is modified"
657 "check whether a working directory is modified"
647
658
648 return (self.p2() or self.branch() != self.p1().branch() or
659 return (self.p2() or self.branch() != self.p1().branch() or
649 self.modified() or self.added() or self.removed() or
660 self.modified() or self.added() or self.removed() or
650 (missing and self.deleted()))
661 (missing and self.deleted()))
651
662
652 class workingfilectx(filectx):
663 class workingfilectx(filectx):
653 """A workingfilectx object makes access to data related to a particular
664 """A workingfilectx object makes access to data related to a particular
654 file in the working directory convenient."""
665 file in the working directory convenient."""
655 def __init__(self, repo, path, filelog=None, workingctx=None):
666 def __init__(self, repo, path, filelog=None, workingctx=None):
656 """changeid can be a changeset revision, node, or tag.
667 """changeid can be a changeset revision, node, or tag.
657 fileid can be a file revision or node."""
668 fileid can be a file revision or node."""
658 self._repo = repo
669 self._repo = repo
659 self._path = path
670 self._path = path
660 self._changeid = None
671 self._changeid = None
661 self._filerev = self._filenode = None
672 self._filerev = self._filenode = None
662
673
663 if filelog:
674 if filelog:
664 self._filelog = filelog
675 self._filelog = filelog
665 if workingctx:
676 if workingctx:
666 self._changectx = workingctx
677 self._changectx = workingctx
667
678
668 @propertycache
679 @propertycache
669 def _changectx(self):
680 def _changectx(self):
670 return workingctx(self._repo)
681 return workingctx(self._repo)
671
682
672 def __nonzero__(self):
683 def __nonzero__(self):
673 return True
684 return True
674
685
675 def __str__(self):
686 def __str__(self):
676 return "%s@%s" % (self.path(), self._changectx)
687 return "%s@%s" % (self.path(), self._changectx)
677
688
678 def data(self): return self._repo.wread(self._path)
689 def data(self): return self._repo.wread(self._path)
679 def renamed(self):
690 def renamed(self):
680 rp = self._repo.dirstate.copied(self._path)
691 rp = self._repo.dirstate.copied(self._path)
681 if not rp:
692 if not rp:
682 return None
693 return None
683 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
694 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
684
695
685 def parents(self):
696 def parents(self):
686 '''return parent filectxs, following copies if necessary'''
697 '''return parent filectxs, following copies if necessary'''
687 def filenode(ctx, path):
698 def filenode(ctx, path):
688 return ctx._manifest.get(path, nullid)
699 return ctx._manifest.get(path, nullid)
689
700
690 path = self._path
701 path = self._path
691 fl = self._filelog
702 fl = self._filelog
692 pcl = self._changectx._parents
703 pcl = self._changectx._parents
693 renamed = self.renamed()
704 renamed = self.renamed()
694
705
695 if renamed:
706 if renamed:
696 pl = [renamed + (None,)]
707 pl = [renamed + (None,)]
697 else:
708 else:
698 pl = [(path, filenode(pcl[0], path), fl)]
709 pl = [(path, filenode(pcl[0], path), fl)]
699
710
700 for pc in pcl[1:]:
711 for pc in pcl[1:]:
701 pl.append((path, filenode(pc, path), fl))
712 pl.append((path, filenode(pc, path), fl))
702
713
703 return [filectx(self._repo, p, fileid=n, filelog=l)
714 return [filectx(self._repo, p, fileid=n, filelog=l)
704 for p,n,l in pl if n != nullid]
715 for p,n,l in pl if n != nullid]
705
716
706 def children(self):
717 def children(self):
707 return []
718 return []
708
719
709 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
720 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
710 def date(self):
721 def date(self):
711 t, tz = self._changectx.date()
722 t, tz = self._changectx.date()
712 try:
723 try:
713 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
724 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
714 except OSError, err:
725 except OSError, err:
715 if err.errno != errno.ENOENT: raise
726 if err.errno != errno.ENOENT: raise
716 return (t, tz)
727 return (t, tz)
717
728
718 def cmp(self, text): return self._repo.wread(self._path) == text
729 def cmp(self, text): return self._repo.wread(self._path) == text
719
730
720 class memctx(object):
731 class memctx(object):
721 """Use memctx to perform in-memory commits via localrepo.commitctx().
732 """Use memctx to perform in-memory commits via localrepo.commitctx().
722
733
723 Revision information is supplied at initialization time while
734 Revision information is supplied at initialization time while
724 related files data and is made available through a callback
735 related files data and is made available through a callback
725 mechanism. 'repo' is the current localrepo, 'parents' is a
736 mechanism. 'repo' is the current localrepo, 'parents' is a
726 sequence of two parent revisions identifiers (pass None for every
737 sequence of two parent revisions identifiers (pass None for every
727 missing parent), 'text' is the commit message and 'files' lists
738 missing parent), 'text' is the commit message and 'files' lists
728 names of files touched by the revision (normalized and relative to
739 names of files touched by the revision (normalized and relative to
729 repository root).
740 repository root).
730
741
731 filectxfn(repo, memctx, path) is a callable receiving the
742 filectxfn(repo, memctx, path) is a callable receiving the
732 repository, the current memctx object and the normalized path of
743 repository, the current memctx object and the normalized path of
733 requested file, relative to repository root. It is fired by the
744 requested file, relative to repository root. It is fired by the
734 commit function for every file in 'files', but calls order is
745 commit function for every file in 'files', but calls order is
735 undefined. If the file is available in the revision being
746 undefined. If the file is available in the revision being
736 committed (updated or added), filectxfn returns a memfilectx
747 committed (updated or added), filectxfn returns a memfilectx
737 object. If the file was removed, filectxfn raises an
748 object. If the file was removed, filectxfn raises an
738 IOError. Moved files are represented by marking the source file
749 IOError. Moved files are represented by marking the source file
739 removed and the new file added with copy information (see
750 removed and the new file added with copy information (see
740 memfilectx).
751 memfilectx).
741
752
742 user receives the committer name and defaults to current
753 user receives the committer name and defaults to current
743 repository username, date is the commit date in any format
754 repository username, date is the commit date in any format
744 supported by util.parsedate() and defaults to current date, extra
755 supported by util.parsedate() and defaults to current date, extra
745 is a dictionary of metadata or is left empty.
756 is a dictionary of metadata or is left empty.
746 """
757 """
747 def __init__(self, repo, parents, text, files, filectxfn, user=None,
758 def __init__(self, repo, parents, text, files, filectxfn, user=None,
748 date=None, extra=None):
759 date=None, extra=None):
749 self._repo = repo
760 self._repo = repo
750 self._rev = None
761 self._rev = None
751 self._node = None
762 self._node = None
752 self._text = text
763 self._text = text
753 self._date = date and util.parsedate(date) or util.makedate()
764 self._date = date and util.parsedate(date) or util.makedate()
754 self._user = user
765 self._user = user
755 parents = [(p or nullid) for p in parents]
766 parents = [(p or nullid) for p in parents]
756 p1, p2 = parents
767 p1, p2 = parents
757 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
768 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
758 files = sorted(set(files))
769 files = sorted(set(files))
759 self._status = [files, [], [], [], []]
770 self._status = [files, [], [], [], []]
760 self._filectxfn = filectxfn
771 self._filectxfn = filectxfn
761
772
762 self._extra = extra and extra.copy() or {}
773 self._extra = extra and extra.copy() or {}
763 if 'branch' not in self._extra:
774 if 'branch' not in self._extra:
764 self._extra['branch'] = 'default'
775 self._extra['branch'] = 'default'
765 elif self._extra.get('branch') == '':
776 elif self._extra.get('branch') == '':
766 self._extra['branch'] = 'default'
777 self._extra['branch'] = 'default'
767
778
768 def __str__(self):
779 def __str__(self):
769 return str(self._parents[0]) + "+"
780 return str(self._parents[0]) + "+"
770
781
771 def __int__(self):
782 def __int__(self):
772 return self._rev
783 return self._rev
773
784
774 def __nonzero__(self):
785 def __nonzero__(self):
775 return True
786 return True
776
787
777 def __getitem__(self, key):
788 def __getitem__(self, key):
778 return self.filectx(key)
789 return self.filectx(key)
779
790
780 def p1(self): return self._parents[0]
791 def p1(self): return self._parents[0]
781 def p2(self): return self._parents[1]
792 def p2(self): return self._parents[1]
782
793
783 def user(self): return self._user or self._repo.ui.username()
794 def user(self): return self._user or self._repo.ui.username()
784 def date(self): return self._date
795 def date(self): return self._date
785 def description(self): return self._text
796 def description(self): return self._text
786 def files(self): return self.modified()
797 def files(self): return self.modified()
787 def modified(self): return self._status[0]
798 def modified(self): return self._status[0]
788 def added(self): return self._status[1]
799 def added(self): return self._status[1]
789 def removed(self): return self._status[2]
800 def removed(self): return self._status[2]
790 def deleted(self): return self._status[3]
801 def deleted(self): return self._status[3]
791 def unknown(self): return self._status[4]
802 def unknown(self): return self._status[4]
792 def clean(self): return self._status[5]
803 def clean(self): return self._status[5]
793 def branch(self): return self._extra['branch']
804 def branch(self): return self._extra['branch']
794 def extra(self): return self._extra
805 def extra(self): return self._extra
795 def flags(self, f): return self[f].flags()
806 def flags(self, f): return self[f].flags()
796
807
797 def parents(self):
808 def parents(self):
798 """return contexts for each parent changeset"""
809 """return contexts for each parent changeset"""
799 return self._parents
810 return self._parents
800
811
801 def filectx(self, path, filelog=None):
812 def filectx(self, path, filelog=None):
802 """get a file context from the working directory"""
813 """get a file context from the working directory"""
803 return self._filectxfn(self._repo, self, path)
814 return self._filectxfn(self._repo, self, path)
804
815
805 class memfilectx(object):
816 class memfilectx(object):
806 """memfilectx represents an in-memory file to commit.
817 """memfilectx represents an in-memory file to commit.
807
818
808 See memctx for more details.
819 See memctx for more details.
809 """
820 """
810 def __init__(self, path, data, islink, isexec, copied):
821 def __init__(self, path, data, islink, isexec, copied):
811 """
822 """
812 path is the normalized file path relative to repository root.
823 path is the normalized file path relative to repository root.
813 data is the file content as a string.
824 data is the file content as a string.
814 islink is True if the file is a symbolic link.
825 islink is True if the file is a symbolic link.
815 isexec is True if the file is executable.
826 isexec is True if the file is executable.
816 copied is the source file path if current file was copied in the
827 copied is the source file path if current file was copied in the
817 revision being committed, or None."""
828 revision being committed, or None."""
818 self._path = path
829 self._path = path
819 self._data = data
830 self._data = data
820 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
831 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
821 self._copied = None
832 self._copied = None
822 if copied:
833 if copied:
823 self._copied = (copied, nullid)
834 self._copied = (copied, nullid)
824
835
825 def __nonzero__(self): return True
836 def __nonzero__(self): return True
826 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
837 def __str__(self): return "%s@%s" % (self.path(), self._changectx)
827 def path(self): return self._path
838 def path(self): return self._path
828 def data(self): return self._data
839 def data(self): return self._data
829 def flags(self): return self._flags
840 def flags(self): return self._flags
830 def isexec(self): return 'x' in self._flags
841 def isexec(self): return 'x' in self._flags
831 def islink(self): return 'l' in self._flags
842 def islink(self): return 'l' in self._flags
832 def renamed(self): return self._copied
843 def renamed(self): return self._copied
@@ -1,245 +1,251
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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import util
9 import util
10 import heapq
10 import heapq
11
11
12 def _nonoverlap(d1, d2, d3):
12 def _nonoverlap(d1, d2, d3):
13 "Return list of elements in d1 not in d2 or d3"
13 "Return list of elements in d1 not in d2 or d3"
14 return sorted([d for d in d1 if d not in d3 and d not in d2])
14 return sorted([d for d in d1 if d not in d3 and d not in d2])
15
15
16 def _dirname(f):
16 def _dirname(f):
17 s = f.rfind("/")
17 s = f.rfind("/")
18 if s == -1:
18 if s == -1:
19 return ""
19 return ""
20 return f[:s]
20 return f[:s]
21
21
22 def _dirs(files):
22 def _dirs(files):
23 d = set()
23 d = set()
24 for f in files:
24 for f in files:
25 f = _dirname(f)
25 f = _dirname(f)
26 while f not in d:
26 while f not in d:
27 d.add(f)
27 d.add(f)
28 f = _dirname(f)
28 f = _dirname(f)
29 return d
29 return d
30
30
31 def _findoldnames(fctx, limit):
32 "find files that path was copied from, back to linkrev limit"
33 old = {}
34 seen = set()
35 orig = fctx.path()
36 visit = [(fctx, 0)]
37 while visit:
38 fc, depth = visit.pop()
39 s = str(fc)
40 if s in seen:
41 continue
42 seen.add(s)
43 if fc.path() != orig and fc.path() not in old:
44 old[fc.path()] = (depth, fc.path()) # remember depth
45 if fc.rev() is not None and fc.rev() < limit:
46 continue
47 visit += [(p, depth - 1) for p in fc.parents()]
48
49 # return old names sorted by depth
50 return [o[1] for o in sorted(old.values())]
51
52 def _findlimit(repo, a, b):
31 def _findlimit(repo, a, b):
53 """Find the earliest revision that's an ancestor of a or b but not both,
32 """Find the earliest revision that's an ancestor of a or b but not both,
54 None if no such revision exists.
33 None if no such revision exists.
55 """
34 """
56 # basic idea:
35 # basic idea:
57 # - mark a and b with different sides
36 # - mark a and b with different sides
58 # - if a parent's children are all on the same side, the parent is
37 # - if a parent's children are all on the same side, the parent is
59 # on that side, otherwise it is on no side
38 # on that side, otherwise it is on no side
60 # - walk the graph in topological order with the help of a heap;
39 # - walk the graph in topological order with the help of a heap;
61 # - add unseen parents to side map
40 # - add unseen parents to side map
62 # - clear side of any parent that has children on different sides
41 # - clear side of any parent that has children on different sides
63 # - track number of interesting revs that might still be on a side
42 # - track number of interesting revs that might still be on a side
64 # - track the lowest interesting rev seen
43 # - track the lowest interesting rev seen
65 # - quit when interesting revs is zero
44 # - quit when interesting revs is zero
66
45
67 cl = repo.changelog
46 cl = repo.changelog
68 working = len(cl) # pseudo rev for the working directory
47 working = len(cl) # pseudo rev for the working directory
69 if a is None:
48 if a is None:
70 a = working
49 a = working
71 if b is None:
50 if b is None:
72 b = working
51 b = working
73
52
74 side = {a: -1, b: 1}
53 side = {a: -1, b: 1}
75 visit = [-a, -b]
54 visit = [-a, -b]
76 heapq.heapify(visit)
55 heapq.heapify(visit)
77 interesting = len(visit)
56 interesting = len(visit)
78 hascommonancestor = False
57 hascommonancestor = False
79 limit = working
58 limit = working
80
59
81 while interesting:
60 while interesting:
82 r = -heapq.heappop(visit)
61 r = -heapq.heappop(visit)
83 if r == working:
62 if r == working:
84 parents = [cl.rev(p) for p in repo.dirstate.parents()]
63 parents = [cl.rev(p) for p in repo.dirstate.parents()]
85 else:
64 else:
86 parents = cl.parentrevs(r)
65 parents = cl.parentrevs(r)
87 for p in parents:
66 for p in parents:
88 if p < 0:
67 if p < 0:
89 continue
68 continue
90 if p not in side:
69 if p not in side:
91 # first time we see p; add it to visit
70 # first time we see p; add it to visit
92 side[p] = side[r]
71 side[p] = side[r]
93 if side[p]:
72 if side[p]:
94 interesting += 1
73 interesting += 1
95 heapq.heappush(visit, -p)
74 heapq.heappush(visit, -p)
96 elif side[p] and side[p] != side[r]:
75 elif side[p] and side[p] != side[r]:
97 # p was interesting but now we know better
76 # p was interesting but now we know better
98 side[p] = 0
77 side[p] = 0
99 interesting -= 1
78 interesting -= 1
100 hascommonancestor = True
79 hascommonancestor = True
101 if side[r]:
80 if side[r]:
102 limit = r # lowest rev visited
81 limit = r # lowest rev visited
103 interesting -= 1
82 interesting -= 1
104
83
105 if not hascommonancestor:
84 if not hascommonancestor:
106 return None
85 return None
107 return limit
86 return limit
108
87
109 def copies(repo, c1, c2, ca, checkdirs=False):
88 def copies(repo, c1, c2, ca, checkdirs=False):
110 """
89 """
111 Find moves and copies between context c1 and c2
90 Find moves and copies between context c1 and c2
112 """
91 """
113 # avoid silly behavior for update from empty dir
92 # avoid silly behavior for update from empty dir
114 if not c1 or not c2 or c1 == c2:
93 if not c1 or not c2 or c1 == c2:
115 return {}, {}
94 return {}, {}
116
95
117 # avoid silly behavior for parent -> working dir
96 # avoid silly behavior for parent -> working dir
118 if c2.node() is None and c1.node() == repo.dirstate.parents()[0]:
97 if c2.node() is None and c1.node() == repo.dirstate.parents()[0]:
119 return repo.dirstate.copies(), {}
98 return repo.dirstate.copies(), {}
120
99
121 limit = _findlimit(repo, c1.rev(), c2.rev())
100 limit = _findlimit(repo, c1.rev(), c2.rev())
122 if limit is None:
101 if limit is None:
123 # no common ancestor, no copies
102 # no common ancestor, no copies
124 return {}, {}
103 return {}, {}
125 m1 = c1.manifest()
104 m1 = c1.manifest()
126 m2 = c2.manifest()
105 m2 = c2.manifest()
127 ma = ca.manifest()
106 ma = ca.manifest()
128
107
129 def makectx(f, n):
108 def makectx(f, n):
130 if len(n) != 20: # in a working context?
109 if len(n) != 20: # in a working context?
131 if c1.rev() is None:
110 if c1.rev() is None:
132 return c1.filectx(f)
111 return c1.filectx(f)
133 return c2.filectx(f)
112 return c2.filectx(f)
134 return repo.filectx(f, fileid=n)
113 return repo.filectx(f, fileid=n)
135
114
136 ctx = util.lrucachefunc(makectx)
115 ctx = util.lrucachefunc(makectx)
137 copy = {}
116 copy = {}
138 fullcopy = {}
117 fullcopy = {}
139 diverge = {}
118 diverge = {}
140
119
120 def related(f1, f2, limit):
121 g1, g2 = f1.ancestors(), f2.ancestors()
122 try:
123 while 1:
124 f1r, f2r = f1.rev(), f2.rev()
125 if f1r > f2r:
126 f1 = g1.next()
127 elif f2r > f1r:
128 f2 = g2.next()
129 elif f1 == f2:
130 return f1 # a match
131 elif f1r == f2r or f1r < limit or f2r < limit:
132 return False # copy no longer relevant
133 except StopIteration:
134 return False
135
141 def checkcopies(f, m1, m2):
136 def checkcopies(f, m1, m2):
142 '''check possible copies of f from m1 to m2'''
137 '''check possible copies of f from m1 to m2'''
143 c1 = ctx(f, m1[f])
138 of = None
144 for of in _findoldnames(c1, limit):
139 seen = set([f])
140 for oc in ctx(f, m1[f]).ancestors():
141 ocr = oc.rev()
142 of = oc.path()
143 if of in seen:
144 # check limit late - grab last rename before
145 if ocr < limit:
146 break
147 continue
148 seen.add(of)
149
145 fullcopy[f] = of # remember for dir rename detection
150 fullcopy[f] = of # remember for dir rename detection
146 if of in m2: # original file not in other manifest?
151 if of not in m2:
147 # if the original file is unchanged on the other branch,
152 continue # no match, keep looking
148 # no merge needed
153 if m2[of] == ma.get(of):
149 if m2[of] != ma.get(of):
154 break # no merge needed, quit early
150 c2 = ctx(of, m2[of])
155 c2 = ctx(of, m2[of])
151 ca = c1.ancestor(c2)
156 cr = related(oc, c2, ca.rev())
152 # related and named changed on only one side?
157 if of == f or of == c2.path(): # non-divergent
153 if ca and (ca.path() == f or ca.path() == c2.path()):
158 copy[f] = of
154 if c1 != ca or c2 != ca: # merge needed?
159 of = None
155 copy[f] = of
160 break
156 elif of in ma:
161
157 diverge.setdefault(of, []).append(f)
162 if of in ma:
163 diverge.setdefault(of, []).append(f)
158
164
159 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
165 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
160
166
161 u1 = _nonoverlap(m1, m2, ma)
167 u1 = _nonoverlap(m1, m2, ma)
162 u2 = _nonoverlap(m2, m1, ma)
168 u2 = _nonoverlap(m2, m1, ma)
163
169
164 if u1:
170 if u1:
165 repo.ui.debug(" unmatched files in local:\n %s\n"
171 repo.ui.debug(" unmatched files in local:\n %s\n"
166 % "\n ".join(u1))
172 % "\n ".join(u1))
167 if u2:
173 if u2:
168 repo.ui.debug(" unmatched files in other:\n %s\n"
174 repo.ui.debug(" unmatched files in other:\n %s\n"
169 % "\n ".join(u2))
175 % "\n ".join(u2))
170
176
171 for f in u1:
177 for f in u1:
172 checkcopies(f, m1, m2)
178 checkcopies(f, m1, m2)
173 for f in u2:
179 for f in u2:
174 checkcopies(f, m2, m1)
180 checkcopies(f, m2, m1)
175
181
176 diverge2 = set()
182 diverge2 = set()
177 for of, fl in diverge.items():
183 for of, fl in diverge.items():
178 if len(fl) == 1:
184 if len(fl) == 1:
179 del diverge[of] # not actually divergent
185 del diverge[of] # not actually divergent
180 else:
186 else:
181 diverge2.update(fl) # reverse map for below
187 diverge2.update(fl) # reverse map for below
182
188
183 if fullcopy:
189 if fullcopy:
184 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
190 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
185 for f in fullcopy:
191 for f in fullcopy:
186 note = ""
192 note = ""
187 if f in copy: note += "*"
193 if f in copy: note += "*"
188 if f in diverge2: note += "!"
194 if f in diverge2: note += "!"
189 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
195 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
190 del diverge2
196 del diverge2
191
197
192 if not fullcopy or not checkdirs:
198 if not fullcopy or not checkdirs:
193 return copy, diverge
199 return copy, diverge
194
200
195 repo.ui.debug(" checking for directory renames\n")
201 repo.ui.debug(" checking for directory renames\n")
196
202
197 # generate a directory move map
203 # generate a directory move map
198 d1, d2 = _dirs(m1), _dirs(m2)
204 d1, d2 = _dirs(m1), _dirs(m2)
199 invalid = set()
205 invalid = set()
200 dirmove = {}
206 dirmove = {}
201
207
202 # examine each file copy for a potential directory move, which is
208 # examine each file copy for a potential directory move, which is
203 # when all the files in a directory are moved to a new directory
209 # when all the files in a directory are moved to a new directory
204 for dst, src in fullcopy.iteritems():
210 for dst, src in fullcopy.iteritems():
205 dsrc, ddst = _dirname(src), _dirname(dst)
211 dsrc, ddst = _dirname(src), _dirname(dst)
206 if dsrc in invalid:
212 if dsrc in invalid:
207 # already seen to be uninteresting
213 # already seen to be uninteresting
208 continue
214 continue
209 elif dsrc in d1 and ddst in d1:
215 elif dsrc in d1 and ddst in d1:
210 # directory wasn't entirely moved locally
216 # directory wasn't entirely moved locally
211 invalid.add(dsrc)
217 invalid.add(dsrc)
212 elif dsrc in d2 and ddst in d2:
218 elif dsrc in d2 and ddst in d2:
213 # directory wasn't entirely moved remotely
219 # directory wasn't entirely moved remotely
214 invalid.add(dsrc)
220 invalid.add(dsrc)
215 elif dsrc in dirmove and dirmove[dsrc] != ddst:
221 elif dsrc in dirmove and dirmove[dsrc] != ddst:
216 # files from the same directory moved to two different places
222 # files from the same directory moved to two different places
217 invalid.add(dsrc)
223 invalid.add(dsrc)
218 else:
224 else:
219 # looks good so far
225 # looks good so far
220 dirmove[dsrc + "/"] = ddst + "/"
226 dirmove[dsrc + "/"] = ddst + "/"
221
227
222 for i in invalid:
228 for i in invalid:
223 if i in dirmove:
229 if i in dirmove:
224 del dirmove[i]
230 del dirmove[i]
225 del d1, d2, invalid
231 del d1, d2, invalid
226
232
227 if not dirmove:
233 if not dirmove:
228 return copy, diverge
234 return copy, diverge
229
235
230 for d in dirmove:
236 for d in dirmove:
231 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
237 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
232
238
233 # check unaccounted nonoverlapping files against directory moves
239 # check unaccounted nonoverlapping files against directory moves
234 for f in u1 + u2:
240 for f in u1 + u2:
235 if f not in fullcopy:
241 if f not in fullcopy:
236 for d in dirmove:
242 for d in dirmove:
237 if f.startswith(d):
243 if f.startswith(d):
238 # new file added in a directory that was moved, move it
244 # new file added in a directory that was moved, move it
239 df = dirmove[d] + f[len(d):]
245 df = dirmove[d] + f[len(d):]
240 if df not in copy:
246 if df not in copy:
241 copy[f] = df
247 copy[f] = df
242 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
248 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
243 break
249 break
244
250
245 return copy, diverge
251 return copy, diverge
General Comments 0
You need to be logged in to leave comments. Login now