##// END OF EJS Templates
sidedatacopies: read rename information from sidedata...
marmoute -
r43416:0171483b default
parent child Browse files
Show More
@@ -1,754 +1,766 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 bin,
13 13 hex,
14 14 nullid,
15 15 )
16 16 from .thirdparty import attr
17 17
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 revlog,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 dateutil,
27 27 stringutil,
28 28 )
29 29
30 30 from .revlogutils import sidedata as sidedatamod
31 31
32 32 _defaultextra = {b'branch': b'default'}
33 33
34 34
35 35 def _string_escape(text):
36 36 """
37 37 >>> from .pycompat import bytechr as chr
38 38 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
39 39 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
40 40 >>> s
41 41 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
42 42 >>> res = _string_escape(s)
43 43 >>> s == _string_unescape(res)
44 44 True
45 45 """
46 46 # subset of the string_escape codec
47 47 text = (
48 48 text.replace(b'\\', b'\\\\')
49 49 .replace(b'\n', b'\\n')
50 50 .replace(b'\r', b'\\r')
51 51 )
52 52 return text.replace(b'\0', b'\\0')
53 53
54 54
55 55 def _string_unescape(text):
56 56 if b'\\0' in text:
57 57 # fix up \0 without getting into trouble with \\0
58 58 text = text.replace(b'\\\\', b'\\\\\n')
59 59 text = text.replace(b'\\0', b'\0')
60 60 text = text.replace(b'\n', b'')
61 61 return stringutil.unescapestr(text)
62 62
63 63
64 64 def decodeextra(text):
65 65 """
66 66 >>> from .pycompat import bytechr as chr
67 67 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
68 68 ... ).items())
69 69 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
70 70 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
71 71 ... b'baz': chr(92) + chr(0) + b'2'})
72 72 ... ).items())
73 73 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
74 74 """
75 75 extra = _defaultextra.copy()
76 76 for l in text.split(b'\0'):
77 77 if l:
78 78 k, v = _string_unescape(l).split(b':', 1)
79 79 extra[k] = v
80 80 return extra
81 81
82 82
83 83 def encodeextra(d):
84 84 # keys must be sorted to produce a deterministic changelog entry
85 85 items = [
86 86 _string_escape(b'%s:%s' % (k, pycompat.bytestr(d[k])))
87 87 for k in sorted(d)
88 88 ]
89 89 return b"\0".join(items)
90 90
91 91
92 92 def encodecopies(files, copies):
93 93 items = []
94 94 for i, dst in enumerate(files):
95 95 if dst in copies:
96 96 items.append(b'%d\0%s' % (i, copies[dst]))
97 97 if len(items) != len(copies):
98 98 raise error.ProgrammingError(
99 99 b'some copy targets missing from file list'
100 100 )
101 101 return b"\n".join(items)
102 102
103 103
104 104 def decodecopies(files, data):
105 105 try:
106 106 copies = {}
107 107 if not data:
108 108 return copies
109 109 for l in data.split(b'\n'):
110 110 strindex, src = l.split(b'\0')
111 111 i = int(strindex)
112 112 dst = files[i]
113 113 copies[dst] = src
114 114 return copies
115 115 except (ValueError, IndexError):
116 116 # Perhaps someone had chosen the same key name (e.g. "p1copies") and
117 117 # used different syntax for the value.
118 118 return None
119 119
120 120
121 121 def encodefileindices(files, subset):
122 122 subset = set(subset)
123 123 indices = []
124 124 for i, f in enumerate(files):
125 125 if f in subset:
126 126 indices.append(b'%d' % i)
127 127 return b'\n'.join(indices)
128 128
129 129
130 130 def decodefileindices(files, data):
131 131 try:
132 132 subset = []
133 133 if not data:
134 134 return subset
135 135 for strindex in data.split(b'\n'):
136 136 i = int(strindex)
137 137 if i < 0 or i >= len(files):
138 138 return None
139 139 subset.append(files[i])
140 140 return subset
141 141 except (ValueError, IndexError):
142 142 # Perhaps someone had chosen the same key name (e.g. "added") and
143 143 # used different syntax for the value.
144 144 return None
145 145
146 146
147 147 def stripdesc(desc):
148 148 """strip trailing whitespace and leading and trailing empty lines"""
149 149 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
150 150
151 151
152 152 class appender(object):
153 153 '''the changelog index must be updated last on disk, so we use this class
154 154 to delay writes to it'''
155 155
156 156 def __init__(self, vfs, name, mode, buf):
157 157 self.data = buf
158 158 fp = vfs(name, mode)
159 159 self.fp = fp
160 160 self.offset = fp.tell()
161 161 self.size = vfs.fstat(fp).st_size
162 162 self._end = self.size
163 163
164 164 def end(self):
165 165 return self._end
166 166
167 167 def tell(self):
168 168 return self.offset
169 169
170 170 def flush(self):
171 171 pass
172 172
173 173 @property
174 174 def closed(self):
175 175 return self.fp.closed
176 176
177 177 def close(self):
178 178 self.fp.close()
179 179
180 180 def seek(self, offset, whence=0):
181 181 '''virtual file offset spans real file and data'''
182 182 if whence == 0:
183 183 self.offset = offset
184 184 elif whence == 1:
185 185 self.offset += offset
186 186 elif whence == 2:
187 187 self.offset = self.end() + offset
188 188 if self.offset < self.size:
189 189 self.fp.seek(self.offset)
190 190
191 191 def read(self, count=-1):
192 192 '''only trick here is reads that span real file and data'''
193 193 ret = b""
194 194 if self.offset < self.size:
195 195 s = self.fp.read(count)
196 196 ret = s
197 197 self.offset += len(s)
198 198 if count > 0:
199 199 count -= len(s)
200 200 if count != 0:
201 201 doff = self.offset - self.size
202 202 self.data.insert(0, b"".join(self.data))
203 203 del self.data[1:]
204 204 s = self.data[0][doff : doff + count]
205 205 self.offset += len(s)
206 206 ret += s
207 207 return ret
208 208
209 209 def write(self, s):
210 210 self.data.append(bytes(s))
211 211 self.offset += len(s)
212 212 self._end += len(s)
213 213
214 214 def __enter__(self):
215 215 self.fp.__enter__()
216 216 return self
217 217
218 218 def __exit__(self, *args):
219 219 return self.fp.__exit__(*args)
220 220
221 221
222 222 def _divertopener(opener, target):
223 223 """build an opener that writes in 'target.a' instead of 'target'"""
224 224
225 225 def _divert(name, mode=b'r', checkambig=False):
226 226 if name != target:
227 227 return opener(name, mode)
228 228 return opener(name + b".a", mode)
229 229
230 230 return _divert
231 231
232 232
233 233 def _delayopener(opener, target, buf):
234 234 """build an opener that stores chunks in 'buf' instead of 'target'"""
235 235
236 236 def _delay(name, mode=b'r', checkambig=False):
237 237 if name != target:
238 238 return opener(name, mode)
239 239 return appender(opener, name, mode, buf)
240 240
241 241 return _delay
242 242
243 243
244 244 @attr.s
245 245 class _changelogrevision(object):
246 246 # Extensions might modify _defaultextra, so let the constructor below pass
247 247 # it in
248 248 extra = attr.ib()
249 249 manifest = attr.ib(default=nullid)
250 250 user = attr.ib(default=b'')
251 251 date = attr.ib(default=(0, 0))
252 252 files = attr.ib(default=attr.Factory(list))
253 253 filesadded = attr.ib(default=None)
254 254 filesremoved = attr.ib(default=None)
255 255 p1copies = attr.ib(default=None)
256 256 p2copies = attr.ib(default=None)
257 257 description = attr.ib(default=b'')
258 258
259 259
260 260 class changelogrevision(object):
261 261 """Holds results of a parsed changelog revision.
262 262
263 263 Changelog revisions consist of multiple pieces of data, including
264 264 the manifest node, user, and date. This object exposes a view into
265 265 the parsed object.
266 266 """
267 267
268 268 __slots__ = (
269 269 r'_offsets',
270 270 r'_text',
271 271 r'_sidedata',
272 272 )
273 273
274 274 def __new__(cls, text, sidedata):
275 275 if not text:
276 276 return _changelogrevision(extra=_defaultextra)
277 277
278 278 self = super(changelogrevision, cls).__new__(cls)
279 279 # We could return here and implement the following as an __init__.
280 280 # But doing it here is equivalent and saves an extra function call.
281 281
282 282 # format used:
283 283 # nodeid\n : manifest node in ascii
284 284 # user\n : user, no \n or \r allowed
285 285 # time tz extra\n : date (time is int or float, timezone is int)
286 286 # : extra is metadata, encoded and separated by '\0'
287 287 # : older versions ignore it
288 288 # files\n\n : files modified by the cset, no \n or \r allowed
289 289 # (.*) : comment (free text, ideally utf-8)
290 290 #
291 291 # changelog v0 doesn't use extra
292 292
293 293 nl1 = text.index(b'\n')
294 294 nl2 = text.index(b'\n', nl1 + 1)
295 295 nl3 = text.index(b'\n', nl2 + 1)
296 296
297 297 # The list of files may be empty. Which means nl3 is the first of the
298 298 # double newline that precedes the description.
299 299 if text[nl3 + 1 : nl3 + 2] == b'\n':
300 300 doublenl = nl3
301 301 else:
302 302 doublenl = text.index(b'\n\n', nl3 + 1)
303 303
304 304 self._offsets = (nl1, nl2, nl3, doublenl)
305 305 self._text = text
306 306 self._sidedata = sidedata
307 307
308 308 return self
309 309
310 310 @property
311 311 def manifest(self):
312 312 return bin(self._text[0 : self._offsets[0]])
313 313
314 314 @property
315 315 def user(self):
316 316 off = self._offsets
317 317 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
318 318
319 319 @property
320 320 def _rawdate(self):
321 321 off = self._offsets
322 322 dateextra = self._text[off[1] + 1 : off[2]]
323 323 return dateextra.split(b' ', 2)[0:2]
324 324
325 325 @property
326 326 def _rawextra(self):
327 327 off = self._offsets
328 328 dateextra = self._text[off[1] + 1 : off[2]]
329 329 fields = dateextra.split(b' ', 2)
330 330 if len(fields) != 3:
331 331 return None
332 332
333 333 return fields[2]
334 334
335 335 @property
336 336 def date(self):
337 337 raw = self._rawdate
338 338 time = float(raw[0])
339 339 # Various tools did silly things with the timezone.
340 340 try:
341 341 timezone = int(raw[1])
342 342 except ValueError:
343 343 timezone = 0
344 344
345 345 return time, timezone
346 346
347 347 @property
348 348 def extra(self):
349 349 raw = self._rawextra
350 350 if raw is None:
351 351 return _defaultextra
352 352
353 353 return decodeextra(raw)
354 354
355 355 @property
356 356 def files(self):
357 357 off = self._offsets
358 358 if off[2] == off[3]:
359 359 return []
360 360
361 361 return self._text[off[2] + 1 : off[3]].split(b'\n')
362 362
363 363 @property
364 364 def filesadded(self):
365 if sidedatamod.SD_FILESADDED in self._sidedata:
366 rawindices = self._sidedata.get(sidedatamod.SD_FILESADDED)
367 else:
365 368 rawindices = self.extra.get(b'filesadded')
366 369 if rawindices is None:
367 370 return None
368 371 return decodefileindices(self.files, rawindices)
369 372
370 373 @property
371 374 def filesremoved(self):
375 if sidedatamod.SD_FILESREMOVED in self._sidedata:
376 rawindices = self._sidedata.get(sidedatamod.SD_FILESREMOVED)
377 else:
372 378 rawindices = self.extra.get(b'filesremoved')
373 379 if rawindices is None:
374 380 return None
375 381 return decodefileindices(self.files, rawindices)
376 382
377 383 @property
378 384 def p1copies(self):
385 if sidedatamod.SD_P1COPIES in self._sidedata:
386 rawcopies = self._sidedata.get(sidedatamod.SD_P1COPIES)
387 else:
379 388 rawcopies = self.extra.get(b'p1copies')
380 389 if rawcopies is None:
381 390 return None
382 391 return decodecopies(self.files, rawcopies)
383 392
384 393 @property
385 394 def p2copies(self):
395 if sidedatamod.SD_P2COPIES in self._sidedata:
396 rawcopies = self._sidedata.get(sidedatamod.SD_P2COPIES)
397 else:
386 398 rawcopies = self.extra.get(b'p2copies')
387 399 if rawcopies is None:
388 400 return None
389 401 return decodecopies(self.files, rawcopies)
390 402
391 403 @property
392 404 def description(self):
393 405 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
394 406
395 407
396 408 class changelog(revlog.revlog):
397 409 def __init__(self, opener, trypending=False):
398 410 """Load a changelog revlog using an opener.
399 411
400 412 If ``trypending`` is true, we attempt to load the index from a
401 413 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
402 414 The ``00changelog.i.a`` file contains index (and possibly inline
403 415 revision) data for a transaction that hasn't been finalized yet.
404 416 It exists in a separate file to facilitate readers (such as
405 417 hooks processes) accessing data before a transaction is finalized.
406 418 """
407 419 if trypending and opener.exists(b'00changelog.i.a'):
408 420 indexfile = b'00changelog.i.a'
409 421 else:
410 422 indexfile = b'00changelog.i'
411 423
412 424 datafile = b'00changelog.d'
413 425 revlog.revlog.__init__(
414 426 self,
415 427 opener,
416 428 indexfile,
417 429 datafile=datafile,
418 430 checkambig=True,
419 431 mmaplargeindex=True,
420 432 )
421 433
422 434 if self._initempty and (self.version & 0xFFFF == revlog.REVLOGV1):
423 435 # changelogs don't benefit from generaldelta.
424 436
425 437 self.version &= ~revlog.FLAG_GENERALDELTA
426 438 self._generaldelta = False
427 439
428 440 # Delta chains for changelogs tend to be very small because entries
429 441 # tend to be small and don't delta well with each. So disable delta
430 442 # chains.
431 443 self._storedeltachains = False
432 444
433 445 self._realopener = opener
434 446 self._delayed = False
435 447 self._delaybuf = None
436 448 self._divert = False
437 449 self.filteredrevs = frozenset()
438 450 self._copiesstorage = opener.options.get(b'copies-storage')
439 451
440 452 def tiprev(self):
441 453 for i in pycompat.xrange(len(self) - 1, -2, -1):
442 454 if i not in self.filteredrevs:
443 455 return i
444 456
445 457 def tip(self):
446 458 """filtered version of revlog.tip"""
447 459 return self.node(self.tiprev())
448 460
449 461 def __contains__(self, rev):
450 462 """filtered version of revlog.__contains__"""
451 463 return 0 <= rev < len(self) and rev not in self.filteredrevs
452 464
453 465 def __iter__(self):
454 466 """filtered version of revlog.__iter__"""
455 467 if len(self.filteredrevs) == 0:
456 468 return revlog.revlog.__iter__(self)
457 469
458 470 def filterediter():
459 471 for i in pycompat.xrange(len(self)):
460 472 if i not in self.filteredrevs:
461 473 yield i
462 474
463 475 return filterediter()
464 476
465 477 def revs(self, start=0, stop=None):
466 478 """filtered version of revlog.revs"""
467 479 for i in super(changelog, self).revs(start, stop):
468 480 if i not in self.filteredrevs:
469 481 yield i
470 482
471 483 def _checknofilteredinrevs(self, revs):
472 484 """raise the appropriate error if 'revs' contains a filtered revision
473 485
474 486 This returns a version of 'revs' to be used thereafter by the caller.
475 487 In particular, if revs is an iterator, it is converted into a set.
476 488 """
477 489 safehasattr = util.safehasattr
478 490 if safehasattr(revs, '__next__'):
479 491 # Note that inspect.isgenerator() is not true for iterators,
480 492 revs = set(revs)
481 493
482 494 filteredrevs = self.filteredrevs
483 495 if safehasattr(revs, 'first'): # smartset
484 496 offenders = revs & filteredrevs
485 497 else:
486 498 offenders = filteredrevs.intersection(revs)
487 499
488 500 for rev in offenders:
489 501 raise error.FilteredIndexError(rev)
490 502 return revs
491 503
492 504 def headrevs(self, revs=None):
493 505 if revs is None and self.filteredrevs:
494 506 try:
495 507 return self.index.headrevsfiltered(self.filteredrevs)
496 508 # AttributeError covers non-c-extension environments and
497 509 # old c extensions without filter handling.
498 510 except AttributeError:
499 511 return self._headrevs()
500 512
501 513 if self.filteredrevs:
502 514 revs = self._checknofilteredinrevs(revs)
503 515 return super(changelog, self).headrevs(revs)
504 516
505 517 def strip(self, *args, **kwargs):
506 518 # XXX make something better than assert
507 519 # We can't expect proper strip behavior if we are filtered.
508 520 assert not self.filteredrevs
509 521 super(changelog, self).strip(*args, **kwargs)
510 522
511 523 def rev(self, node):
512 524 """filtered version of revlog.rev"""
513 525 r = super(changelog, self).rev(node)
514 526 if r in self.filteredrevs:
515 527 raise error.FilteredLookupError(
516 528 hex(node), self.indexfile, _(b'filtered node')
517 529 )
518 530 return r
519 531
520 532 def node(self, rev):
521 533 """filtered version of revlog.node"""
522 534 if rev in self.filteredrevs:
523 535 raise error.FilteredIndexError(rev)
524 536 return super(changelog, self).node(rev)
525 537
526 538 def linkrev(self, rev):
527 539 """filtered version of revlog.linkrev"""
528 540 if rev in self.filteredrevs:
529 541 raise error.FilteredIndexError(rev)
530 542 return super(changelog, self).linkrev(rev)
531 543
532 544 def parentrevs(self, rev):
533 545 """filtered version of revlog.parentrevs"""
534 546 if rev in self.filteredrevs:
535 547 raise error.FilteredIndexError(rev)
536 548 return super(changelog, self).parentrevs(rev)
537 549
538 550 def flags(self, rev):
539 551 """filtered version of revlog.flags"""
540 552 if rev in self.filteredrevs:
541 553 raise error.FilteredIndexError(rev)
542 554 return super(changelog, self).flags(rev)
543 555
544 556 def delayupdate(self, tr):
545 557 b"delay visibility of index updates to other readers"
546 558
547 559 if not self._delayed:
548 560 if len(self) == 0:
549 561 self._divert = True
550 562 if self._realopener.exists(self.indexfile + b'.a'):
551 563 self._realopener.unlink(self.indexfile + b'.a')
552 564 self.opener = _divertopener(self._realopener, self.indexfile)
553 565 else:
554 566 self._delaybuf = []
555 567 self.opener = _delayopener(
556 568 self._realopener, self.indexfile, self._delaybuf
557 569 )
558 570 self._delayed = True
559 571 tr.addpending(b'cl-%i' % id(self), self._writepending)
560 572 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
561 573
562 574 def _finalize(self, tr):
563 575 b"finalize index updates"
564 576 self._delayed = False
565 577 self.opener = self._realopener
566 578 # move redirected index data back into place
567 579 if self._divert:
568 580 assert not self._delaybuf
569 581 tmpname = self.indexfile + b".a"
570 582 nfile = self.opener.open(tmpname)
571 583 nfile.close()
572 584 self.opener.rename(tmpname, self.indexfile, checkambig=True)
573 585 elif self._delaybuf:
574 586 fp = self.opener(self.indexfile, b'a', checkambig=True)
575 587 fp.write(b"".join(self._delaybuf))
576 588 fp.close()
577 589 self._delaybuf = None
578 590 self._divert = False
579 591 # split when we're done
580 592 self._enforceinlinesize(tr)
581 593
582 594 def _writepending(self, tr):
583 595 b"create a file containing the unfinalized state for pretxnchangegroup"
584 596 if self._delaybuf:
585 597 # make a temporary copy of the index
586 598 fp1 = self._realopener(self.indexfile)
587 599 pendingfilename = self.indexfile + b".a"
588 600 # register as a temp file to ensure cleanup on failure
589 601 tr.registertmp(pendingfilename)
590 602 # write existing data
591 603 fp2 = self._realopener(pendingfilename, b"w")
592 604 fp2.write(fp1.read())
593 605 # add pending data
594 606 fp2.write(b"".join(self._delaybuf))
595 607 fp2.close()
596 608 # switch modes so finalize can simply rename
597 609 self._delaybuf = None
598 610 self._divert = True
599 611 self.opener = _divertopener(self._realopener, self.indexfile)
600 612
601 613 if self._divert:
602 614 return True
603 615
604 616 return False
605 617
606 618 def _enforceinlinesize(self, tr, fp=None):
607 619 if not self._delayed:
608 620 revlog.revlog._enforceinlinesize(self, tr, fp)
609 621
610 622 def read(self, node):
611 623 """Obtain data from a parsed changelog revision.
612 624
613 625 Returns a 6-tuple of:
614 626
615 627 - manifest node in binary
616 628 - author/user as a localstr
617 629 - date as a 2-tuple of (time, timezone)
618 630 - list of files
619 631 - commit message as a localstr
620 632 - dict of extra metadata
621 633
622 634 Unless you need to access all fields, consider calling
623 635 ``changelogrevision`` instead, as it is faster for partial object
624 636 access.
625 637 """
626 638 c = changelogrevision(*self._revisiondata(node))
627 639 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
628 640
629 641 def changelogrevision(self, nodeorrev):
630 642 """Obtain a ``changelogrevision`` for a node or revision."""
631 643 text, sidedata = self._revisiondata(nodeorrev)
632 644 return changelogrevision(text, sidedata)
633 645
634 646 def readfiles(self, node):
635 647 """
636 648 short version of read that only returns the files modified by the cset
637 649 """
638 650 text = self.revision(node)
639 651 if not text:
640 652 return []
641 653 last = text.index(b"\n\n")
642 654 l = text[:last].split(b'\n')
643 655 return l[3:]
644 656
645 657 def add(
646 658 self,
647 659 manifest,
648 660 files,
649 661 desc,
650 662 transaction,
651 663 p1,
652 664 p2,
653 665 user,
654 666 date=None,
655 667 extra=None,
656 668 p1copies=None,
657 669 p2copies=None,
658 670 filesadded=None,
659 671 filesremoved=None,
660 672 ):
661 673 # Convert to UTF-8 encoded bytestrings as the very first
662 674 # thing: calling any method on a localstr object will turn it
663 675 # into a str object and the cached UTF-8 string is thus lost.
664 676 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
665 677
666 678 user = user.strip()
667 679 # An empty username or a username with a "\n" will make the
668 680 # revision text contain two "\n\n" sequences -> corrupt
669 681 # repository since read cannot unpack the revision.
670 682 if not user:
671 683 raise error.StorageError(_(b"empty username"))
672 684 if b"\n" in user:
673 685 raise error.StorageError(
674 686 _(b"username %r contains a newline") % pycompat.bytestr(user)
675 687 )
676 688
677 689 desc = stripdesc(desc)
678 690
679 691 if date:
680 692 parseddate = b"%d %d" % dateutil.parsedate(date)
681 693 else:
682 694 parseddate = b"%d %d" % dateutil.makedate()
683 695 if extra:
684 696 branch = extra.get(b"branch")
685 697 if branch in (b"default", b""):
686 698 del extra[b"branch"]
687 699 elif branch in (b".", b"null", b"tip"):
688 700 raise error.StorageError(
689 701 _(b'the name \'%s\' is reserved') % branch
690 702 )
691 703 sortedfiles = sorted(files)
692 704 sidedata = None
693 705 if extra is not None:
694 706 for name in (
695 707 b'p1copies',
696 708 b'p2copies',
697 709 b'filesadded',
698 710 b'filesremoved',
699 711 ):
700 712 extra.pop(name, None)
701 713 if p1copies is not None:
702 714 p1copies = encodecopies(sortedfiles, p1copies)
703 715 if p2copies is not None:
704 716 p2copies = encodecopies(sortedfiles, p2copies)
705 717 if filesadded is not None:
706 718 filesadded = encodefileindices(sortedfiles, filesadded)
707 719 if filesremoved is not None:
708 720 filesremoved = encodefileindices(sortedfiles, filesremoved)
709 721 if self._copiesstorage == b'extra':
710 722 extrasentries = p1copies, p2copies, filesadded, filesremoved
711 723 if extra is None and any(x is not None for x in extrasentries):
712 724 extra = {}
713 725 if p1copies is not None:
714 726 extra[b'p1copies'] = p1copies
715 727 if p2copies is not None:
716 728 extra[b'p2copies'] = p2copies
717 729 if filesadded is not None:
718 730 extra[b'filesadded'] = filesadded
719 731 if filesremoved is not None:
720 732 extra[b'filesremoved'] = filesremoved
721 733 elif self._copiesstorage == b'changeset-sidedata':
722 734 sidedata = {}
723 735 if p1copies is not None:
724 736 sidedata[sidedatamod.SD_P1COPIES] = p1copies
725 737 if p2copies is not None:
726 738 sidedata[sidedatamod.SD_P2COPIES] = p2copies
727 739 if filesadded is not None:
728 740 sidedata[sidedatamod.SD_FILESADDED] = filesadded
729 741 if filesremoved is not None:
730 742 sidedata[sidedatamod.SD_FILESREMOVED] = filesremoved
731 743
732 744 if extra:
733 745 extra = encodeextra(extra)
734 746 parseddate = b"%s %s" % (parseddate, extra)
735 747 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
736 748 text = b"\n".join(l)
737 749 return self.addrevision(
738 750 text, transaction, len(self), p1, p2, sidedata=sidedata
739 751 )
740 752
741 753 def branchinfo(self, rev):
742 754 """return the branch name and open/close state of a revision
743 755
744 756 This function exists because creating a changectx object
745 757 just to access this is costly."""
746 758 extra = self.read(rev)[5]
747 759 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
748 760
749 761 def _nodeduplicatecallback(self, transaction, node):
750 762 # keep track of revisions that got "re-added", eg: unbunde of know rev.
751 763 #
752 764 # We track them in a list to preserve their order from the source bundle
753 765 duplicates = transaction.changes.setdefault(b'revduplicates', [])
754 766 duplicates.append(self.rev(node))
@@ -1,2966 +1,2987 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import filecmp
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 addednodeid,
18 18 hex,
19 19 modifiednodeid,
20 20 nullid,
21 21 nullrev,
22 22 short,
23 23 wdirfilenodeids,
24 24 wdirhex,
25 25 )
26 26 from .pycompat import (
27 27 getattr,
28 28 open,
29 29 )
30 30 from . import (
31 31 copies,
32 32 dagop,
33 33 encoding,
34 34 error,
35 35 fileset,
36 36 match as matchmod,
37 37 obsolete as obsmod,
38 38 patch,
39 39 pathutil,
40 40 phases,
41 41 pycompat,
42 42 repoview,
43 43 scmutil,
44 44 sparse,
45 45 subrepo,
46 46 subrepoutil,
47 47 util,
48 48 )
49 49 from .utils import (
50 50 dateutil,
51 51 stringutil,
52 52 )
53 53
54 54 propertycache = util.propertycache
55 55
56 56
57 57 class basectx(object):
58 58 """A basectx object represents the common logic for its children:
59 59 changectx: read-only context that is already present in the repo,
60 60 workingctx: a context that represents the working directory and can
61 61 be committed,
62 62 memctx: a context that represents changes in-memory and can also
63 63 be committed."""
64 64
65 65 def __init__(self, repo):
66 66 self._repo = repo
67 67
68 68 def __bytes__(self):
69 69 return short(self.node())
70 70
71 71 __str__ = encoding.strmethod(__bytes__)
72 72
73 73 def __repr__(self):
74 74 return r"<%s %s>" % (type(self).__name__, str(self))
75 75
76 76 def __eq__(self, other):
77 77 try:
78 78 return type(self) == type(other) and self._rev == other._rev
79 79 except AttributeError:
80 80 return False
81 81
82 82 def __ne__(self, other):
83 83 return not (self == other)
84 84
85 85 def __contains__(self, key):
86 86 return key in self._manifest
87 87
88 88 def __getitem__(self, key):
89 89 return self.filectx(key)
90 90
91 91 def __iter__(self):
92 92 return iter(self._manifest)
93 93
94 94 def _buildstatusmanifest(self, status):
95 95 """Builds a manifest that includes the given status results, if this is
96 96 a working copy context. For non-working copy contexts, it just returns
97 97 the normal manifest."""
98 98 return self.manifest()
99 99
100 100 def _matchstatus(self, other, match):
101 101 """This internal method provides a way for child objects to override the
102 102 match operator.
103 103 """
104 104 return match
105 105
106 106 def _buildstatus(
107 107 self, other, s, match, listignored, listclean, listunknown
108 108 ):
109 109 """build a status with respect to another context"""
110 110 # Load earliest manifest first for caching reasons. More specifically,
111 111 # if you have revisions 1000 and 1001, 1001 is probably stored as a
112 112 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
113 113 # 1000 and cache it so that when you read 1001, we just need to apply a
114 114 # delta to what's in the cache. So that's one full reconstruction + one
115 115 # delta application.
116 116 mf2 = None
117 117 if self.rev() is not None and self.rev() < other.rev():
118 118 mf2 = self._buildstatusmanifest(s)
119 119 mf1 = other._buildstatusmanifest(s)
120 120 if mf2 is None:
121 121 mf2 = self._buildstatusmanifest(s)
122 122
123 123 modified, added = [], []
124 124 removed = []
125 125 clean = []
126 126 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
127 127 deletedset = set(deleted)
128 128 d = mf1.diff(mf2, match=match, clean=listclean)
129 129 for fn, value in pycompat.iteritems(d):
130 130 if fn in deletedset:
131 131 continue
132 132 if value is None:
133 133 clean.append(fn)
134 134 continue
135 135 (node1, flag1), (node2, flag2) = value
136 136 if node1 is None:
137 137 added.append(fn)
138 138 elif node2 is None:
139 139 removed.append(fn)
140 140 elif flag1 != flag2:
141 141 modified.append(fn)
142 142 elif node2 not in wdirfilenodeids:
143 143 # When comparing files between two commits, we save time by
144 144 # not comparing the file contents when the nodeids differ.
145 145 # Note that this means we incorrectly report a reverted change
146 146 # to a file as a modification.
147 147 modified.append(fn)
148 148 elif self[fn].cmp(other[fn]):
149 149 modified.append(fn)
150 150 else:
151 151 clean.append(fn)
152 152
153 153 if removed:
154 154 # need to filter files if they are already reported as removed
155 155 unknown = [
156 156 fn
157 157 for fn in unknown
158 158 if fn not in mf1 and (not match or match(fn))
159 159 ]
160 160 ignored = [
161 161 fn
162 162 for fn in ignored
163 163 if fn not in mf1 and (not match or match(fn))
164 164 ]
165 165 # if they're deleted, don't report them as removed
166 166 removed = [fn for fn in removed if fn not in deletedset]
167 167
168 168 return scmutil.status(
169 169 modified, added, removed, deleted, unknown, ignored, clean
170 170 )
171 171
172 172 @propertycache
173 173 def substate(self):
174 174 return subrepoutil.state(self, self._repo.ui)
175 175
176 176 def subrev(self, subpath):
177 177 return self.substate[subpath][1]
178 178
179 179 def rev(self):
180 180 return self._rev
181 181
182 182 def node(self):
183 183 return self._node
184 184
185 185 def hex(self):
186 186 return hex(self.node())
187 187
188 188 def manifest(self):
189 189 return self._manifest
190 190
191 191 def manifestctx(self):
192 192 return self._manifestctx
193 193
194 194 def repo(self):
195 195 return self._repo
196 196
197 197 def phasestr(self):
198 198 return phases.phasenames[self.phase()]
199 199
200 200 def mutable(self):
201 201 return self.phase() > phases.public
202 202
203 203 def matchfileset(self, expr, badfn=None):
204 204 return fileset.match(self, expr, badfn=badfn)
205 205
206 206 def obsolete(self):
207 207 """True if the changeset is obsolete"""
208 208 return self.rev() in obsmod.getrevs(self._repo, b'obsolete')
209 209
210 210 def extinct(self):
211 211 """True if the changeset is extinct"""
212 212 return self.rev() in obsmod.getrevs(self._repo, b'extinct')
213 213
214 214 def orphan(self):
215 215 """True if the changeset is not obsolete, but its ancestor is"""
216 216 return self.rev() in obsmod.getrevs(self._repo, b'orphan')
217 217
218 218 def phasedivergent(self):
219 219 """True if the changeset tries to be a successor of a public changeset
220 220
221 221 Only non-public and non-obsolete changesets may be phase-divergent.
222 222 """
223 223 return self.rev() in obsmod.getrevs(self._repo, b'phasedivergent')
224 224
225 225 def contentdivergent(self):
226 226 """Is a successor of a changeset with multiple possible successor sets
227 227
228 228 Only non-public and non-obsolete changesets may be content-divergent.
229 229 """
230 230 return self.rev() in obsmod.getrevs(self._repo, b'contentdivergent')
231 231
232 232 def isunstable(self):
233 233 """True if the changeset is either orphan, phase-divergent or
234 234 content-divergent"""
235 235 return self.orphan() or self.phasedivergent() or self.contentdivergent()
236 236
237 237 def instabilities(self):
238 238 """return the list of instabilities affecting this changeset.
239 239
240 240 Instabilities are returned as strings. possible values are:
241 241 - orphan,
242 242 - phase-divergent,
243 243 - content-divergent.
244 244 """
245 245 instabilities = []
246 246 if self.orphan():
247 247 instabilities.append(b'orphan')
248 248 if self.phasedivergent():
249 249 instabilities.append(b'phase-divergent')
250 250 if self.contentdivergent():
251 251 instabilities.append(b'content-divergent')
252 252 return instabilities
253 253
254 254 def parents(self):
255 255 """return contexts for each parent changeset"""
256 256 return self._parents
257 257
258 258 def p1(self):
259 259 return self._parents[0]
260 260
261 261 def p2(self):
262 262 parents = self._parents
263 263 if len(parents) == 2:
264 264 return parents[1]
265 265 return self._repo[nullrev]
266 266
267 267 def _fileinfo(self, path):
268 268 if r'_manifest' in self.__dict__:
269 269 try:
270 270 return self._manifest[path], self._manifest.flags(path)
271 271 except KeyError:
272 272 raise error.ManifestLookupError(
273 273 self._node, path, _(b'not found in manifest')
274 274 )
275 275 if r'_manifestdelta' in self.__dict__ or path in self.files():
276 276 if path in self._manifestdelta:
277 277 return (
278 278 self._manifestdelta[path],
279 279 self._manifestdelta.flags(path),
280 280 )
281 281 mfl = self._repo.manifestlog
282 282 try:
283 283 node, flag = mfl[self._changeset.manifest].find(path)
284 284 except KeyError:
285 285 raise error.ManifestLookupError(
286 286 self._node, path, _(b'not found in manifest')
287 287 )
288 288
289 289 return node, flag
290 290
291 291 def filenode(self, path):
292 292 return self._fileinfo(path)[0]
293 293
294 294 def flags(self, path):
295 295 try:
296 296 return self._fileinfo(path)[1]
297 297 except error.LookupError:
298 298 return b''
299 299
300 300 @propertycache
301 301 def _copies(self):
302 302 return copies.computechangesetcopies(self)
303 303
304 304 def p1copies(self):
305 305 return self._copies[0]
306 306
307 307 def p2copies(self):
308 308 return self._copies[1]
309 309
310 310 def sub(self, path, allowcreate=True):
311 311 '''return a subrepo for the stored revision of path, never wdir()'''
312 312 return subrepo.subrepo(self, path, allowcreate=allowcreate)
313 313
314 314 def nullsub(self, path, pctx):
315 315 return subrepo.nullsubrepo(self, path, pctx)
316 316
317 317 def workingsub(self, path):
318 318 '''return a subrepo for the stored revision, or wdir if this is a wdir
319 319 context.
320 320 '''
321 321 return subrepo.subrepo(self, path, allowwdir=True)
322 322
323 323 def match(
324 324 self,
325 325 pats=None,
326 326 include=None,
327 327 exclude=None,
328 328 default=b'glob',
329 329 listsubrepos=False,
330 330 badfn=None,
331 331 ):
332 332 r = self._repo
333 333 return matchmod.match(
334 334 r.root,
335 335 r.getcwd(),
336 336 pats,
337 337 include,
338 338 exclude,
339 339 default,
340 340 auditor=r.nofsauditor,
341 341 ctx=self,
342 342 listsubrepos=listsubrepos,
343 343 badfn=badfn,
344 344 )
345 345
346 346 def diff(
347 347 self,
348 348 ctx2=None,
349 349 match=None,
350 350 changes=None,
351 351 opts=None,
352 352 losedatafn=None,
353 353 pathfn=None,
354 354 copy=None,
355 355 copysourcematch=None,
356 356 hunksfilterfn=None,
357 357 ):
358 358 """Returns a diff generator for the given contexts and matcher"""
359 359 if ctx2 is None:
360 360 ctx2 = self.p1()
361 361 if ctx2 is not None:
362 362 ctx2 = self._repo[ctx2]
363 363 return patch.diff(
364 364 self._repo,
365 365 ctx2,
366 366 self,
367 367 match=match,
368 368 changes=changes,
369 369 opts=opts,
370 370 losedatafn=losedatafn,
371 371 pathfn=pathfn,
372 372 copy=copy,
373 373 copysourcematch=copysourcematch,
374 374 hunksfilterfn=hunksfilterfn,
375 375 )
376 376
377 377 def dirs(self):
378 378 return self._manifest.dirs()
379 379
380 380 def hasdir(self, dir):
381 381 return self._manifest.hasdir(dir)
382 382
383 383 def status(
384 384 self,
385 385 other=None,
386 386 match=None,
387 387 listignored=False,
388 388 listclean=False,
389 389 listunknown=False,
390 390 listsubrepos=False,
391 391 ):
392 392 """return status of files between two nodes or node and working
393 393 directory.
394 394
395 395 If other is None, compare this node with working directory.
396 396
397 397 returns (modified, added, removed, deleted, unknown, ignored, clean)
398 398 """
399 399
400 400 ctx1 = self
401 401 ctx2 = self._repo[other]
402 402
403 403 # This next code block is, admittedly, fragile logic that tests for
404 404 # reversing the contexts and wouldn't need to exist if it weren't for
405 405 # the fast (and common) code path of comparing the working directory
406 406 # with its first parent.
407 407 #
408 408 # What we're aiming for here is the ability to call:
409 409 #
410 410 # workingctx.status(parentctx)
411 411 #
412 412 # If we always built the manifest for each context and compared those,
413 413 # then we'd be done. But the special case of the above call means we
414 414 # just copy the manifest of the parent.
415 415 reversed = False
416 416 if not isinstance(ctx1, changectx) and isinstance(ctx2, changectx):
417 417 reversed = True
418 418 ctx1, ctx2 = ctx2, ctx1
419 419
420 420 match = self._repo.narrowmatch(match)
421 421 match = ctx2._matchstatus(ctx1, match)
422 422 r = scmutil.status([], [], [], [], [], [], [])
423 423 r = ctx2._buildstatus(
424 424 ctx1, r, match, listignored, listclean, listunknown
425 425 )
426 426
427 427 if reversed:
428 428 # Reverse added and removed. Clear deleted, unknown and ignored as
429 429 # these make no sense to reverse.
430 430 r = scmutil.status(
431 431 r.modified, r.removed, r.added, [], [], [], r.clean
432 432 )
433 433
434 434 if listsubrepos:
435 435 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
436 436 try:
437 437 rev2 = ctx2.subrev(subpath)
438 438 except KeyError:
439 439 # A subrepo that existed in node1 was deleted between
440 440 # node1 and node2 (inclusive). Thus, ctx2's substate
441 441 # won't contain that subpath. The best we can do ignore it.
442 442 rev2 = None
443 443 submatch = matchmod.subdirmatcher(subpath, match)
444 444 s = sub.status(
445 445 rev2,
446 446 match=submatch,
447 447 ignored=listignored,
448 448 clean=listclean,
449 449 unknown=listunknown,
450 450 listsubrepos=True,
451 451 )
452 452 for rfiles, sfiles in zip(r, s):
453 453 rfiles.extend(b"%s/%s" % (subpath, f) for f in sfiles)
454 454
455 455 for l in r:
456 456 l.sort()
457 457
458 458 return r
459 459
460 460
461 461 class changectx(basectx):
462 462 """A changecontext object makes access to data related to a particular
463 463 changeset convenient. It represents a read-only context already present in
464 464 the repo."""
465 465
466 466 def __init__(self, repo, rev, node):
467 467 super(changectx, self).__init__(repo)
468 468 self._rev = rev
469 469 self._node = node
470 470
471 471 def __hash__(self):
472 472 try:
473 473 return hash(self._rev)
474 474 except AttributeError:
475 475 return id(self)
476 476
477 477 def __nonzero__(self):
478 478 return self._rev != nullrev
479 479
480 480 __bool__ = __nonzero__
481 481
482 482 @propertycache
483 483 def _changeset(self):
484 484 return self._repo.changelog.changelogrevision(self.rev())
485 485
486 486 @propertycache
487 487 def _manifest(self):
488 488 return self._manifestctx.read()
489 489
490 490 @property
491 491 def _manifestctx(self):
492 492 return self._repo.manifestlog[self._changeset.manifest]
493 493
494 494 @propertycache
495 495 def _manifestdelta(self):
496 496 return self._manifestctx.readdelta()
497 497
498 498 @propertycache
499 499 def _parents(self):
500 500 repo = self._repo
501 501 p1, p2 = repo.changelog.parentrevs(self._rev)
502 502 if p2 == nullrev:
503 503 return [repo[p1]]
504 504 return [repo[p1], repo[p2]]
505 505
506 506 def changeset(self):
507 507 c = self._changeset
508 508 return (
509 509 c.manifest,
510 510 c.user,
511 511 c.date,
512 512 c.files,
513 513 c.description,
514 514 c.extra,
515 515 )
516 516
517 517 def manifestnode(self):
518 518 return self._changeset.manifest
519 519
520 520 def user(self):
521 521 return self._changeset.user
522 522
523 523 def date(self):
524 524 return self._changeset.date
525 525
526 526 def files(self):
527 527 return self._changeset.files
528 528
529 529 def filesmodified(self):
530 530 modified = set(self.files())
531 531 modified.difference_update(self.filesadded())
532 532 modified.difference_update(self.filesremoved())
533 533 return sorted(modified)
534 534
535 535 def filesadded(self):
536 filesadded = self._changeset.filesadded
537 compute_on_none = True
538 if self._repo.filecopiesmode == b'changeset-sidedata':
539 compute_on_none = False
540 else:
536 541 source = self._repo.ui.config(b'experimental', b'copies.read-from')
537 filesadded = self._changeset.filesadded
538 542 if source == b'changeset-only':
543 compute_on_none = False
544 elif source != b'compatibility':
545 # filelog mode, ignore any changelog content
546 filesadded = None
539 547 if filesadded is None:
540 filesadded = []
541 elif source == b'compatibility':
542 if filesadded is None:
548 if compute_on_none:
543 549 filesadded = scmutil.computechangesetfilesadded(self)
544 550 else:
545 filesadded = scmutil.computechangesetfilesadded(self)
551 filesadded = []
546 552 return filesadded
547 553
548 554 def filesremoved(self):
555 filesremoved = self._changeset.filesremoved
556 compute_on_none = True
557 if self._repo.filecopiesmode == b'changeset-sidedata':
558 compute_on_none = False
559 else:
549 560 source = self._repo.ui.config(b'experimental', b'copies.read-from')
550 filesremoved = self._changeset.filesremoved
551 561 if source == b'changeset-only':
562 compute_on_none = False
563 elif source != b'compatibility':
564 # filelog mode, ignore any changelog content
565 filesremoved = None
552 566 if filesremoved is None:
553 filesremoved = []
554 elif source == b'compatibility':
555 if filesremoved is None:
567 if compute_on_none:
556 568 filesremoved = scmutil.computechangesetfilesremoved(self)
557 569 else:
558 filesremoved = scmutil.computechangesetfilesremoved(self)
570 filesremoved = []
559 571 return filesremoved
560 572
561 573 @propertycache
562 574 def _copies(self):
563 source = self._repo.ui.config(b'experimental', b'copies.read-from')
564 575 p1copies = self._changeset.p1copies
565 576 p2copies = self._changeset.p2copies
566 # If config says to get copy metadata only from changeset, then return
567 # that, defaulting to {} if there was no copy metadata.
568 # In compatibility mode, we return copy data from the changeset if
569 # it was recorded there, and otherwise we fall back to getting it from
577 compute_on_none = True
578 if self._repo.filecopiesmode == b'changeset-sidedata':
579 compute_on_none = False
580 else:
581 source = self._repo.ui.config(b'experimental', b'copies.read-from')
582 # If config says to get copy metadata only from changeset, then
583 # return that, defaulting to {} if there was no copy metadata. In
584 # compatibility mode, we return copy data from the changeset if it
585 # was recorded there, and otherwise we fall back to getting it from
570 586 # the filelogs (below).
587 #
588 # If we are in compatiblity mode and there is not data in the
589 # changeset), we get the copy metadata from the filelogs.
590 #
591 # otherwise, when config said to read only from filelog, we get the
592 # copy metadata from the filelogs.
571 593 if source == b'changeset-only':
594 compute_on_none = False
595 elif source != b'compatibility':
596 # filelog mode, ignore any changelog content
597 p1copies = p2copies = None
598 if p1copies is None:
599 if compute_on_none:
600 p1copies, p2copies = super(changectx, self)._copies
601 else:
572 602 if p1copies is None:
573 603 p1copies = {}
574 604 if p2copies is None:
575 605 p2copies = {}
576 elif source == b'compatibility':
577 if p1copies is None:
578 # we are in compatiblity mode and there is not data in the
579 # changeset), we get the copy metadata from the filelogs.
580 p1copies, p2copies = super(changectx, self)._copies
581 else:
582 # config said to read only from filelog, we get the copy metadata
583 # from the filelogs.
584 p1copies, p2copies = super(changectx, self)._copies
585 606 return p1copies, p2copies
586 607
587 608 def description(self):
588 609 return self._changeset.description
589 610
590 611 def branch(self):
591 612 return encoding.tolocal(self._changeset.extra.get(b"branch"))
592 613
593 614 def closesbranch(self):
594 615 return b'close' in self._changeset.extra
595 616
596 617 def extra(self):
597 618 """Return a dict of extra information."""
598 619 return self._changeset.extra
599 620
600 621 def tags(self):
601 622 """Return a list of byte tag names"""
602 623 return self._repo.nodetags(self._node)
603 624
604 625 def bookmarks(self):
605 626 """Return a list of byte bookmark names."""
606 627 return self._repo.nodebookmarks(self._node)
607 628
608 629 def phase(self):
609 630 return self._repo._phasecache.phase(self._repo, self._rev)
610 631
611 632 def hidden(self):
612 633 return self._rev in repoview.filterrevs(self._repo, b'visible')
613 634
614 635 def isinmemory(self):
615 636 return False
616 637
617 638 def children(self):
618 639 """return list of changectx contexts for each child changeset.
619 640
620 641 This returns only the immediate child changesets. Use descendants() to
621 642 recursively walk children.
622 643 """
623 644 c = self._repo.changelog.children(self._node)
624 645 return [self._repo[x] for x in c]
625 646
626 647 def ancestors(self):
627 648 for a in self._repo.changelog.ancestors([self._rev]):
628 649 yield self._repo[a]
629 650
630 651 def descendants(self):
631 652 """Recursively yield all children of the changeset.
632 653
633 654 For just the immediate children, use children()
634 655 """
635 656 for d in self._repo.changelog.descendants([self._rev]):
636 657 yield self._repo[d]
637 658
638 659 def filectx(self, path, fileid=None, filelog=None):
639 660 """get a file context from this changeset"""
640 661 if fileid is None:
641 662 fileid = self.filenode(path)
642 663 return filectx(
643 664 self._repo, path, fileid=fileid, changectx=self, filelog=filelog
644 665 )
645 666
646 667 def ancestor(self, c2, warn=False):
647 668 """return the "best" ancestor context of self and c2
648 669
649 670 If there are multiple candidates, it will show a message and check
650 671 merge.preferancestor configuration before falling back to the
651 672 revlog ancestor."""
652 673 # deal with workingctxs
653 674 n2 = c2._node
654 675 if n2 is None:
655 676 n2 = c2._parents[0]._node
656 677 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
657 678 if not cahs:
658 679 anc = nullid
659 680 elif len(cahs) == 1:
660 681 anc = cahs[0]
661 682 else:
662 683 # experimental config: merge.preferancestor
663 684 for r in self._repo.ui.configlist(b'merge', b'preferancestor'):
664 685 try:
665 686 ctx = scmutil.revsymbol(self._repo, r)
666 687 except error.RepoLookupError:
667 688 continue
668 689 anc = ctx.node()
669 690 if anc in cahs:
670 691 break
671 692 else:
672 693 anc = self._repo.changelog.ancestor(self._node, n2)
673 694 if warn:
674 695 self._repo.ui.status(
675 696 (
676 697 _(b"note: using %s as ancestor of %s and %s\n")
677 698 % (short(anc), short(self._node), short(n2))
678 699 )
679 700 + b''.join(
680 701 _(
681 702 b" alternatively, use --config "
682 703 b"merge.preferancestor=%s\n"
683 704 )
684 705 % short(n)
685 706 for n in sorted(cahs)
686 707 if n != anc
687 708 )
688 709 )
689 710 return self._repo[anc]
690 711
691 712 def isancestorof(self, other):
692 713 """True if this changeset is an ancestor of other"""
693 714 return self._repo.changelog.isancestorrev(self._rev, other._rev)
694 715
695 716 def walk(self, match):
696 717 '''Generates matching file names.'''
697 718
698 719 # Wrap match.bad method to have message with nodeid
699 720 def bad(fn, msg):
700 721 # The manifest doesn't know about subrepos, so don't complain about
701 722 # paths into valid subrepos.
702 723 if any(fn == s or fn.startswith(s + b'/') for s in self.substate):
703 724 return
704 725 match.bad(fn, _(b'no such file in rev %s') % self)
705 726
706 727 m = matchmod.badmatch(self._repo.narrowmatch(match), bad)
707 728 return self._manifest.walk(m)
708 729
709 730 def matches(self, match):
710 731 return self.walk(match)
711 732
712 733
713 734 class basefilectx(object):
714 735 """A filecontext object represents the common logic for its children:
715 736 filectx: read-only access to a filerevision that is already present
716 737 in the repo,
717 738 workingfilectx: a filecontext that represents files from the working
718 739 directory,
719 740 memfilectx: a filecontext that represents files in-memory,
720 741 """
721 742
722 743 @propertycache
723 744 def _filelog(self):
724 745 return self._repo.file(self._path)
725 746
726 747 @propertycache
727 748 def _changeid(self):
728 749 if r'_changectx' in self.__dict__:
729 750 return self._changectx.rev()
730 751 elif r'_descendantrev' in self.__dict__:
731 752 # this file context was created from a revision with a known
732 753 # descendant, we can (lazily) correct for linkrev aliases
733 754 return self._adjustlinkrev(self._descendantrev)
734 755 else:
735 756 return self._filelog.linkrev(self._filerev)
736 757
737 758 @propertycache
738 759 def _filenode(self):
739 760 if r'_fileid' in self.__dict__:
740 761 return self._filelog.lookup(self._fileid)
741 762 else:
742 763 return self._changectx.filenode(self._path)
743 764
744 765 @propertycache
745 766 def _filerev(self):
746 767 return self._filelog.rev(self._filenode)
747 768
748 769 @propertycache
749 770 def _repopath(self):
750 771 return self._path
751 772
752 773 def __nonzero__(self):
753 774 try:
754 775 self._filenode
755 776 return True
756 777 except error.LookupError:
757 778 # file is missing
758 779 return False
759 780
760 781 __bool__ = __nonzero__
761 782
762 783 def __bytes__(self):
763 784 try:
764 785 return b"%s@%s" % (self.path(), self._changectx)
765 786 except error.LookupError:
766 787 return b"%s@???" % self.path()
767 788
768 789 __str__ = encoding.strmethod(__bytes__)
769 790
770 791 def __repr__(self):
771 792 return r"<%s %s>" % (type(self).__name__, str(self))
772 793
773 794 def __hash__(self):
774 795 try:
775 796 return hash((self._path, self._filenode))
776 797 except AttributeError:
777 798 return id(self)
778 799
779 800 def __eq__(self, other):
780 801 try:
781 802 return (
782 803 type(self) == type(other)
783 804 and self._path == other._path
784 805 and self._filenode == other._filenode
785 806 )
786 807 except AttributeError:
787 808 return False
788 809
789 810 def __ne__(self, other):
790 811 return not (self == other)
791 812
792 813 def filerev(self):
793 814 return self._filerev
794 815
795 816 def filenode(self):
796 817 return self._filenode
797 818
798 819 @propertycache
799 820 def _flags(self):
800 821 return self._changectx.flags(self._path)
801 822
802 823 def flags(self):
803 824 return self._flags
804 825
805 826 def filelog(self):
806 827 return self._filelog
807 828
808 829 def rev(self):
809 830 return self._changeid
810 831
811 832 def linkrev(self):
812 833 return self._filelog.linkrev(self._filerev)
813 834
814 835 def node(self):
815 836 return self._changectx.node()
816 837
817 838 def hex(self):
818 839 return self._changectx.hex()
819 840
820 841 def user(self):
821 842 return self._changectx.user()
822 843
823 844 def date(self):
824 845 return self._changectx.date()
825 846
826 847 def files(self):
827 848 return self._changectx.files()
828 849
829 850 def description(self):
830 851 return self._changectx.description()
831 852
832 853 def branch(self):
833 854 return self._changectx.branch()
834 855
835 856 def extra(self):
836 857 return self._changectx.extra()
837 858
838 859 def phase(self):
839 860 return self._changectx.phase()
840 861
841 862 def phasestr(self):
842 863 return self._changectx.phasestr()
843 864
844 865 def obsolete(self):
845 866 return self._changectx.obsolete()
846 867
847 868 def instabilities(self):
848 869 return self._changectx.instabilities()
849 870
850 871 def manifest(self):
851 872 return self._changectx.manifest()
852 873
853 874 def changectx(self):
854 875 return self._changectx
855 876
856 877 def renamed(self):
857 878 return self._copied
858 879
859 880 def copysource(self):
860 881 return self._copied and self._copied[0]
861 882
862 883 def repo(self):
863 884 return self._repo
864 885
865 886 def size(self):
866 887 return len(self.data())
867 888
868 889 def path(self):
869 890 return self._path
870 891
871 892 def isbinary(self):
872 893 try:
873 894 return stringutil.binary(self.data())
874 895 except IOError:
875 896 return False
876 897
877 898 def isexec(self):
878 899 return b'x' in self.flags()
879 900
880 901 def islink(self):
881 902 return b'l' in self.flags()
882 903
883 904 def isabsent(self):
884 905 """whether this filectx represents a file not in self._changectx
885 906
886 907 This is mainly for merge code to detect change/delete conflicts. This is
887 908 expected to be True for all subclasses of basectx."""
888 909 return False
889 910
890 911 _customcmp = False
891 912
892 913 def cmp(self, fctx):
893 914 """compare with other file context
894 915
895 916 returns True if different than fctx.
896 917 """
897 918 if fctx._customcmp:
898 919 return fctx.cmp(self)
899 920
900 921 if self._filenode is None:
901 922 raise error.ProgrammingError(
902 923 b'filectx.cmp() must be reimplemented if not backed by revlog'
903 924 )
904 925
905 926 if fctx._filenode is None:
906 927 if self._repo._encodefilterpats:
907 928 # can't rely on size() because wdir content may be decoded
908 929 return self._filelog.cmp(self._filenode, fctx.data())
909 930 if self.size() - 4 == fctx.size():
910 931 # size() can match:
911 932 # if file data starts with '\1\n', empty metadata block is
912 933 # prepended, which adds 4 bytes to filelog.size().
913 934 return self._filelog.cmp(self._filenode, fctx.data())
914 935 if self.size() == fctx.size():
915 936 # size() matches: need to compare content
916 937 return self._filelog.cmp(self._filenode, fctx.data())
917 938
918 939 # size() differs
919 940 return True
920 941
921 942 def _adjustlinkrev(self, srcrev, inclusive=False, stoprev=None):
922 943 """return the first ancestor of <srcrev> introducing <fnode>
923 944
924 945 If the linkrev of the file revision does not point to an ancestor of
925 946 srcrev, we'll walk down the ancestors until we find one introducing
926 947 this file revision.
927 948
928 949 :srcrev: the changeset revision we search ancestors from
929 950 :inclusive: if true, the src revision will also be checked
930 951 :stoprev: an optional revision to stop the walk at. If no introduction
931 952 of this file content could be found before this floor
932 953 revision, the function will returns "None" and stops its
933 954 iteration.
934 955 """
935 956 repo = self._repo
936 957 cl = repo.unfiltered().changelog
937 958 mfl = repo.manifestlog
938 959 # fetch the linkrev
939 960 lkr = self.linkrev()
940 961 if srcrev == lkr:
941 962 return lkr
942 963 # hack to reuse ancestor computation when searching for renames
943 964 memberanc = getattr(self, '_ancestrycontext', None)
944 965 iteranc = None
945 966 if srcrev is None:
946 967 # wctx case, used by workingfilectx during mergecopy
947 968 revs = [p.rev() for p in self._repo[None].parents()]
948 969 inclusive = True # we skipped the real (revless) source
949 970 else:
950 971 revs = [srcrev]
951 972 if memberanc is None:
952 973 memberanc = iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
953 974 # check if this linkrev is an ancestor of srcrev
954 975 if lkr not in memberanc:
955 976 if iteranc is None:
956 977 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
957 978 fnode = self._filenode
958 979 path = self._path
959 980 for a in iteranc:
960 981 if stoprev is not None and a < stoprev:
961 982 return None
962 983 ac = cl.read(a) # get changeset data (we avoid object creation)
963 984 if path in ac[3]: # checking the 'files' field.
964 985 # The file has been touched, check if the content is
965 986 # similar to the one we search for.
966 987 if fnode == mfl[ac[0]].readfast().get(path):
967 988 return a
968 989 # In theory, we should never get out of that loop without a result.
969 990 # But if manifest uses a buggy file revision (not children of the
970 991 # one it replaces) we could. Such a buggy situation will likely
971 992 # result is crash somewhere else at to some point.
972 993 return lkr
973 994
974 995 def isintroducedafter(self, changelogrev):
975 996 """True if a filectx has been introduced after a given floor revision
976 997 """
977 998 if self.linkrev() >= changelogrev:
978 999 return True
979 1000 introrev = self._introrev(stoprev=changelogrev)
980 1001 if introrev is None:
981 1002 return False
982 1003 return introrev >= changelogrev
983 1004
984 1005 def introrev(self):
985 1006 """return the rev of the changeset which introduced this file revision
986 1007
987 1008 This method is different from linkrev because it take into account the
988 1009 changeset the filectx was created from. It ensures the returned
989 1010 revision is one of its ancestors. This prevents bugs from
990 1011 'linkrev-shadowing' when a file revision is used by multiple
991 1012 changesets.
992 1013 """
993 1014 return self._introrev()
994 1015
995 1016 def _introrev(self, stoprev=None):
996 1017 """
997 1018 Same as `introrev` but, with an extra argument to limit changelog
998 1019 iteration range in some internal usecase.
999 1020
1000 1021 If `stoprev` is set, the `introrev` will not be searched past that
1001 1022 `stoprev` revision and "None" might be returned. This is useful to
1002 1023 limit the iteration range.
1003 1024 """
1004 1025 toprev = None
1005 1026 attrs = vars(self)
1006 1027 if r'_changeid' in attrs:
1007 1028 # We have a cached value already
1008 1029 toprev = self._changeid
1009 1030 elif r'_changectx' in attrs:
1010 1031 # We know which changelog entry we are coming from
1011 1032 toprev = self._changectx.rev()
1012 1033
1013 1034 if toprev is not None:
1014 1035 return self._adjustlinkrev(toprev, inclusive=True, stoprev=stoprev)
1015 1036 elif r'_descendantrev' in attrs:
1016 1037 introrev = self._adjustlinkrev(self._descendantrev, stoprev=stoprev)
1017 1038 # be nice and cache the result of the computation
1018 1039 if introrev is not None:
1019 1040 self._changeid = introrev
1020 1041 return introrev
1021 1042 else:
1022 1043 return self.linkrev()
1023 1044
1024 1045 def introfilectx(self):
1025 1046 """Return filectx having identical contents, but pointing to the
1026 1047 changeset revision where this filectx was introduced"""
1027 1048 introrev = self.introrev()
1028 1049 if self.rev() == introrev:
1029 1050 return self
1030 1051 return self.filectx(self.filenode(), changeid=introrev)
1031 1052
1032 1053 def _parentfilectx(self, path, fileid, filelog):
1033 1054 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
1034 1055 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
1035 1056 if r'_changeid' in vars(self) or r'_changectx' in vars(self):
1036 1057 # If self is associated with a changeset (probably explicitly
1037 1058 # fed), ensure the created filectx is associated with a
1038 1059 # changeset that is an ancestor of self.changectx.
1039 1060 # This lets us later use _adjustlinkrev to get a correct link.
1040 1061 fctx._descendantrev = self.rev()
1041 1062 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1042 1063 elif r'_descendantrev' in vars(self):
1043 1064 # Otherwise propagate _descendantrev if we have one associated.
1044 1065 fctx._descendantrev = self._descendantrev
1045 1066 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1046 1067 return fctx
1047 1068
1048 1069 def parents(self):
1049 1070 _path = self._path
1050 1071 fl = self._filelog
1051 1072 parents = self._filelog.parents(self._filenode)
1052 1073 pl = [(_path, node, fl) for node in parents if node != nullid]
1053 1074
1054 1075 r = fl.renamed(self._filenode)
1055 1076 if r:
1056 1077 # - In the simple rename case, both parent are nullid, pl is empty.
1057 1078 # - In case of merge, only one of the parent is null id and should
1058 1079 # be replaced with the rename information. This parent is -always-
1059 1080 # the first one.
1060 1081 #
1061 1082 # As null id have always been filtered out in the previous list
1062 1083 # comprehension, inserting to 0 will always result in "replacing
1063 1084 # first nullid parent with rename information.
1064 1085 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
1065 1086
1066 1087 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
1067 1088
1068 1089 def p1(self):
1069 1090 return self.parents()[0]
1070 1091
1071 1092 def p2(self):
1072 1093 p = self.parents()
1073 1094 if len(p) == 2:
1074 1095 return p[1]
1075 1096 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
1076 1097
1077 1098 def annotate(self, follow=False, skiprevs=None, diffopts=None):
1078 1099 """Returns a list of annotateline objects for each line in the file
1079 1100
1080 1101 - line.fctx is the filectx of the node where that line was last changed
1081 1102 - line.lineno is the line number at the first appearance in the managed
1082 1103 file
1083 1104 - line.text is the data on that line (including newline character)
1084 1105 """
1085 1106 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
1086 1107
1087 1108 def parents(f):
1088 1109 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
1089 1110 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
1090 1111 # from the topmost introrev (= srcrev) down to p.linkrev() if it
1091 1112 # isn't an ancestor of the srcrev.
1092 1113 f._changeid
1093 1114 pl = f.parents()
1094 1115
1095 1116 # Don't return renamed parents if we aren't following.
1096 1117 if not follow:
1097 1118 pl = [p for p in pl if p.path() == f.path()]
1098 1119
1099 1120 # renamed filectx won't have a filelog yet, so set it
1100 1121 # from the cache to save time
1101 1122 for p in pl:
1102 1123 if not r'_filelog' in p.__dict__:
1103 1124 p._filelog = getlog(p.path())
1104 1125
1105 1126 return pl
1106 1127
1107 1128 # use linkrev to find the first changeset where self appeared
1108 1129 base = self.introfilectx()
1109 1130 if getattr(base, '_ancestrycontext', None) is None:
1110 1131 cl = self._repo.changelog
1111 1132 if base.rev() is None:
1112 1133 # wctx is not inclusive, but works because _ancestrycontext
1113 1134 # is used to test filelog revisions
1114 1135 ac = cl.ancestors(
1115 1136 [p.rev() for p in base.parents()], inclusive=True
1116 1137 )
1117 1138 else:
1118 1139 ac = cl.ancestors([base.rev()], inclusive=True)
1119 1140 base._ancestrycontext = ac
1120 1141
1121 1142 return dagop.annotate(
1122 1143 base, parents, skiprevs=skiprevs, diffopts=diffopts
1123 1144 )
1124 1145
1125 1146 def ancestors(self, followfirst=False):
1126 1147 visit = {}
1127 1148 c = self
1128 1149 if followfirst:
1129 1150 cut = 1
1130 1151 else:
1131 1152 cut = None
1132 1153
1133 1154 while True:
1134 1155 for parent in c.parents()[:cut]:
1135 1156 visit[(parent.linkrev(), parent.filenode())] = parent
1136 1157 if not visit:
1137 1158 break
1138 1159 c = visit.pop(max(visit))
1139 1160 yield c
1140 1161
1141 1162 def decodeddata(self):
1142 1163 """Returns `data()` after running repository decoding filters.
1143 1164
1144 1165 This is often equivalent to how the data would be expressed on disk.
1145 1166 """
1146 1167 return self._repo.wwritedata(self.path(), self.data())
1147 1168
1148 1169
1149 1170 class filectx(basefilectx):
1150 1171 """A filecontext object makes access to data related to a particular
1151 1172 filerevision convenient."""
1152 1173
1153 1174 def __init__(
1154 1175 self,
1155 1176 repo,
1156 1177 path,
1157 1178 changeid=None,
1158 1179 fileid=None,
1159 1180 filelog=None,
1160 1181 changectx=None,
1161 1182 ):
1162 1183 """changeid must be a revision number, if specified.
1163 1184 fileid can be a file revision or node."""
1164 1185 self._repo = repo
1165 1186 self._path = path
1166 1187
1167 1188 assert (
1168 1189 changeid is not None or fileid is not None or changectx is not None
1169 1190 ), b"bad args: changeid=%r, fileid=%r, changectx=%r" % (
1170 1191 changeid,
1171 1192 fileid,
1172 1193 changectx,
1173 1194 )
1174 1195
1175 1196 if filelog is not None:
1176 1197 self._filelog = filelog
1177 1198
1178 1199 if changeid is not None:
1179 1200 self._changeid = changeid
1180 1201 if changectx is not None:
1181 1202 self._changectx = changectx
1182 1203 if fileid is not None:
1183 1204 self._fileid = fileid
1184 1205
1185 1206 @propertycache
1186 1207 def _changectx(self):
1187 1208 try:
1188 1209 return self._repo[self._changeid]
1189 1210 except error.FilteredRepoLookupError:
1190 1211 # Linkrev may point to any revision in the repository. When the
1191 1212 # repository is filtered this may lead to `filectx` trying to build
1192 1213 # `changectx` for filtered revision. In such case we fallback to
1193 1214 # creating `changectx` on the unfiltered version of the reposition.
1194 1215 # This fallback should not be an issue because `changectx` from
1195 1216 # `filectx` are not used in complex operations that care about
1196 1217 # filtering.
1197 1218 #
1198 1219 # This fallback is a cheap and dirty fix that prevent several
1199 1220 # crashes. It does not ensure the behavior is correct. However the
1200 1221 # behavior was not correct before filtering either and "incorrect
1201 1222 # behavior" is seen as better as "crash"
1202 1223 #
1203 1224 # Linkrevs have several serious troubles with filtering that are
1204 1225 # complicated to solve. Proper handling of the issue here should be
1205 1226 # considered when solving linkrev issue are on the table.
1206 1227 return self._repo.unfiltered()[self._changeid]
1207 1228
1208 1229 def filectx(self, fileid, changeid=None):
1209 1230 '''opens an arbitrary revision of the file without
1210 1231 opening a new filelog'''
1211 1232 return filectx(
1212 1233 self._repo,
1213 1234 self._path,
1214 1235 fileid=fileid,
1215 1236 filelog=self._filelog,
1216 1237 changeid=changeid,
1217 1238 )
1218 1239
1219 1240 def rawdata(self):
1220 1241 return self._filelog.rawdata(self._filenode)
1221 1242
1222 1243 def rawflags(self):
1223 1244 """low-level revlog flags"""
1224 1245 return self._filelog.flags(self._filerev)
1225 1246
1226 1247 def data(self):
1227 1248 try:
1228 1249 return self._filelog.read(self._filenode)
1229 1250 except error.CensoredNodeError:
1230 1251 if self._repo.ui.config(b"censor", b"policy") == b"ignore":
1231 1252 return b""
1232 1253 raise error.Abort(
1233 1254 _(b"censored node: %s") % short(self._filenode),
1234 1255 hint=_(b"set censor.policy to ignore errors"),
1235 1256 )
1236 1257
1237 1258 def size(self):
1238 1259 return self._filelog.size(self._filerev)
1239 1260
1240 1261 @propertycache
1241 1262 def _copied(self):
1242 1263 """check if file was actually renamed in this changeset revision
1243 1264
1244 1265 If rename logged in file revision, we report copy for changeset only
1245 1266 if file revisions linkrev points back to the changeset in question
1246 1267 or both changeset parents contain different file revisions.
1247 1268 """
1248 1269
1249 1270 renamed = self._filelog.renamed(self._filenode)
1250 1271 if not renamed:
1251 1272 return None
1252 1273
1253 1274 if self.rev() == self.linkrev():
1254 1275 return renamed
1255 1276
1256 1277 name = self.path()
1257 1278 fnode = self._filenode
1258 1279 for p in self._changectx.parents():
1259 1280 try:
1260 1281 if fnode == p.filenode(name):
1261 1282 return None
1262 1283 except error.LookupError:
1263 1284 pass
1264 1285 return renamed
1265 1286
1266 1287 def children(self):
1267 1288 # hard for renames
1268 1289 c = self._filelog.children(self._filenode)
1269 1290 return [
1270 1291 filectx(self._repo, self._path, fileid=x, filelog=self._filelog)
1271 1292 for x in c
1272 1293 ]
1273 1294
1274 1295
1275 1296 class committablectx(basectx):
1276 1297 """A committablectx object provides common functionality for a context that
1277 1298 wants the ability to commit, e.g. workingctx or memctx."""
1278 1299
1279 1300 def __init__(
1280 1301 self,
1281 1302 repo,
1282 1303 text=b"",
1283 1304 user=None,
1284 1305 date=None,
1285 1306 extra=None,
1286 1307 changes=None,
1287 1308 branch=None,
1288 1309 ):
1289 1310 super(committablectx, self).__init__(repo)
1290 1311 self._rev = None
1291 1312 self._node = None
1292 1313 self._text = text
1293 1314 if date:
1294 1315 self._date = dateutil.parsedate(date)
1295 1316 if user:
1296 1317 self._user = user
1297 1318 if changes:
1298 1319 self._status = changes
1299 1320
1300 1321 self._extra = {}
1301 1322 if extra:
1302 1323 self._extra = extra.copy()
1303 1324 if branch is not None:
1304 1325 self._extra[b'branch'] = encoding.fromlocal(branch)
1305 1326 if not self._extra.get(b'branch'):
1306 1327 self._extra[b'branch'] = b'default'
1307 1328
1308 1329 def __bytes__(self):
1309 1330 return bytes(self._parents[0]) + b"+"
1310 1331
1311 1332 __str__ = encoding.strmethod(__bytes__)
1312 1333
1313 1334 def __nonzero__(self):
1314 1335 return True
1315 1336
1316 1337 __bool__ = __nonzero__
1317 1338
1318 1339 @propertycache
1319 1340 def _status(self):
1320 1341 return self._repo.status()
1321 1342
1322 1343 @propertycache
1323 1344 def _user(self):
1324 1345 return self._repo.ui.username()
1325 1346
1326 1347 @propertycache
1327 1348 def _date(self):
1328 1349 ui = self._repo.ui
1329 1350 date = ui.configdate(b'devel', b'default-date')
1330 1351 if date is None:
1331 1352 date = dateutil.makedate()
1332 1353 return date
1333 1354
1334 1355 def subrev(self, subpath):
1335 1356 return None
1336 1357
1337 1358 def manifestnode(self):
1338 1359 return None
1339 1360
1340 1361 def user(self):
1341 1362 return self._user or self._repo.ui.username()
1342 1363
1343 1364 def date(self):
1344 1365 return self._date
1345 1366
1346 1367 def description(self):
1347 1368 return self._text
1348 1369
1349 1370 def files(self):
1350 1371 return sorted(
1351 1372 self._status.modified + self._status.added + self._status.removed
1352 1373 )
1353 1374
1354 1375 def modified(self):
1355 1376 return self._status.modified
1356 1377
1357 1378 def added(self):
1358 1379 return self._status.added
1359 1380
1360 1381 def removed(self):
1361 1382 return self._status.removed
1362 1383
1363 1384 def deleted(self):
1364 1385 return self._status.deleted
1365 1386
1366 1387 filesmodified = modified
1367 1388 filesadded = added
1368 1389 filesremoved = removed
1369 1390
1370 1391 def branch(self):
1371 1392 return encoding.tolocal(self._extra[b'branch'])
1372 1393
1373 1394 def closesbranch(self):
1374 1395 return b'close' in self._extra
1375 1396
1376 1397 def extra(self):
1377 1398 return self._extra
1378 1399
1379 1400 def isinmemory(self):
1380 1401 return False
1381 1402
1382 1403 def tags(self):
1383 1404 return []
1384 1405
1385 1406 def bookmarks(self):
1386 1407 b = []
1387 1408 for p in self.parents():
1388 1409 b.extend(p.bookmarks())
1389 1410 return b
1390 1411
1391 1412 def phase(self):
1392 1413 phase = phases.draft # default phase to draft
1393 1414 for p in self.parents():
1394 1415 phase = max(phase, p.phase())
1395 1416 return phase
1396 1417
1397 1418 def hidden(self):
1398 1419 return False
1399 1420
1400 1421 def children(self):
1401 1422 return []
1402 1423
1403 1424 def ancestor(self, c2):
1404 1425 """return the "best" ancestor context of self and c2"""
1405 1426 return self._parents[0].ancestor(c2) # punt on two parents for now
1406 1427
1407 1428 def ancestors(self):
1408 1429 for p in self._parents:
1409 1430 yield p
1410 1431 for a in self._repo.changelog.ancestors(
1411 1432 [p.rev() for p in self._parents]
1412 1433 ):
1413 1434 yield self._repo[a]
1414 1435
1415 1436 def markcommitted(self, node):
1416 1437 """Perform post-commit cleanup necessary after committing this ctx
1417 1438
1418 1439 Specifically, this updates backing stores this working context
1419 1440 wraps to reflect the fact that the changes reflected by this
1420 1441 workingctx have been committed. For example, it marks
1421 1442 modified and added files as normal in the dirstate.
1422 1443
1423 1444 """
1424 1445
1425 1446 def dirty(self, missing=False, merge=True, branch=True):
1426 1447 return False
1427 1448
1428 1449
1429 1450 class workingctx(committablectx):
1430 1451 """A workingctx object makes access to data related to
1431 1452 the current working directory convenient.
1432 1453 date - any valid date string or (unixtime, offset), or None.
1433 1454 user - username string, or None.
1434 1455 extra - a dictionary of extra values, or None.
1435 1456 changes - a list of file lists as returned by localrepo.status()
1436 1457 or None to use the repository status.
1437 1458 """
1438 1459
1439 1460 def __init__(
1440 1461 self, repo, text=b"", user=None, date=None, extra=None, changes=None
1441 1462 ):
1442 1463 branch = None
1443 1464 if not extra or b'branch' not in extra:
1444 1465 try:
1445 1466 branch = repo.dirstate.branch()
1446 1467 except UnicodeDecodeError:
1447 1468 raise error.Abort(_(b'branch name not in UTF-8!'))
1448 1469 super(workingctx, self).__init__(
1449 1470 repo, text, user, date, extra, changes, branch=branch
1450 1471 )
1451 1472
1452 1473 def __iter__(self):
1453 1474 d = self._repo.dirstate
1454 1475 for f in d:
1455 1476 if d[f] != b'r':
1456 1477 yield f
1457 1478
1458 1479 def __contains__(self, key):
1459 1480 return self._repo.dirstate[key] not in b"?r"
1460 1481
1461 1482 def hex(self):
1462 1483 return wdirhex
1463 1484
1464 1485 @propertycache
1465 1486 def _parents(self):
1466 1487 p = self._repo.dirstate.parents()
1467 1488 if p[1] == nullid:
1468 1489 p = p[:-1]
1469 1490 # use unfiltered repo to delay/avoid loading obsmarkers
1470 1491 unfi = self._repo.unfiltered()
1471 1492 return [changectx(self._repo, unfi.changelog.rev(n), n) for n in p]
1472 1493
1473 1494 def _fileinfo(self, path):
1474 1495 # populate __dict__['_manifest'] as workingctx has no _manifestdelta
1475 1496 self._manifest
1476 1497 return super(workingctx, self)._fileinfo(path)
1477 1498
1478 1499 def _buildflagfunc(self):
1479 1500 # Create a fallback function for getting file flags when the
1480 1501 # filesystem doesn't support them
1481 1502
1482 1503 copiesget = self._repo.dirstate.copies().get
1483 1504 parents = self.parents()
1484 1505 if len(parents) < 2:
1485 1506 # when we have one parent, it's easy: copy from parent
1486 1507 man = parents[0].manifest()
1487 1508
1488 1509 def func(f):
1489 1510 f = copiesget(f, f)
1490 1511 return man.flags(f)
1491 1512
1492 1513 else:
1493 1514 # merges are tricky: we try to reconstruct the unstored
1494 1515 # result from the merge (issue1802)
1495 1516 p1, p2 = parents
1496 1517 pa = p1.ancestor(p2)
1497 1518 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1498 1519
1499 1520 def func(f):
1500 1521 f = copiesget(f, f) # may be wrong for merges with copies
1501 1522 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1502 1523 if fl1 == fl2:
1503 1524 return fl1
1504 1525 if fl1 == fla:
1505 1526 return fl2
1506 1527 if fl2 == fla:
1507 1528 return fl1
1508 1529 return b'' # punt for conflicts
1509 1530
1510 1531 return func
1511 1532
1512 1533 @propertycache
1513 1534 def _flagfunc(self):
1514 1535 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1515 1536
1516 1537 def flags(self, path):
1517 1538 if r'_manifest' in self.__dict__:
1518 1539 try:
1519 1540 return self._manifest.flags(path)
1520 1541 except KeyError:
1521 1542 return b''
1522 1543
1523 1544 try:
1524 1545 return self._flagfunc(path)
1525 1546 except OSError:
1526 1547 return b''
1527 1548
1528 1549 def filectx(self, path, filelog=None):
1529 1550 """get a file context from the working directory"""
1530 1551 return workingfilectx(
1531 1552 self._repo, path, workingctx=self, filelog=filelog
1532 1553 )
1533 1554
1534 1555 def dirty(self, missing=False, merge=True, branch=True):
1535 1556 b"check whether a working directory is modified"
1536 1557 # check subrepos first
1537 1558 for s in sorted(self.substate):
1538 1559 if self.sub(s).dirty(missing=missing):
1539 1560 return True
1540 1561 # check current working dir
1541 1562 return (
1542 1563 (merge and self.p2())
1543 1564 or (branch and self.branch() != self.p1().branch())
1544 1565 or self.modified()
1545 1566 or self.added()
1546 1567 or self.removed()
1547 1568 or (missing and self.deleted())
1548 1569 )
1549 1570
1550 1571 def add(self, list, prefix=b""):
1551 1572 with self._repo.wlock():
1552 1573 ui, ds = self._repo.ui, self._repo.dirstate
1553 1574 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1554 1575 rejected = []
1555 1576 lstat = self._repo.wvfs.lstat
1556 1577 for f in list:
1557 1578 # ds.pathto() returns an absolute file when this is invoked from
1558 1579 # the keyword extension. That gets flagged as non-portable on
1559 1580 # Windows, since it contains the drive letter and colon.
1560 1581 scmutil.checkportable(ui, os.path.join(prefix, f))
1561 1582 try:
1562 1583 st = lstat(f)
1563 1584 except OSError:
1564 1585 ui.warn(_(b"%s does not exist!\n") % uipath(f))
1565 1586 rejected.append(f)
1566 1587 continue
1567 1588 limit = ui.configbytes(b'ui', b'large-file-limit')
1568 1589 if limit != 0 and st.st_size > limit:
1569 1590 ui.warn(
1570 1591 _(
1571 1592 b"%s: up to %d MB of RAM may be required "
1572 1593 b"to manage this file\n"
1573 1594 b"(use 'hg revert %s' to cancel the "
1574 1595 b"pending addition)\n"
1575 1596 )
1576 1597 % (f, 3 * st.st_size // 1000000, uipath(f))
1577 1598 )
1578 1599 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1579 1600 ui.warn(
1580 1601 _(
1581 1602 b"%s not added: only files and symlinks "
1582 1603 b"supported currently\n"
1583 1604 )
1584 1605 % uipath(f)
1585 1606 )
1586 1607 rejected.append(f)
1587 1608 elif ds[f] in b'amn':
1588 1609 ui.warn(_(b"%s already tracked!\n") % uipath(f))
1589 1610 elif ds[f] == b'r':
1590 1611 ds.normallookup(f)
1591 1612 else:
1592 1613 ds.add(f)
1593 1614 return rejected
1594 1615
1595 1616 def forget(self, files, prefix=b""):
1596 1617 with self._repo.wlock():
1597 1618 ds = self._repo.dirstate
1598 1619 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1599 1620 rejected = []
1600 1621 for f in files:
1601 1622 if f not in ds:
1602 1623 self._repo.ui.warn(_(b"%s not tracked!\n") % uipath(f))
1603 1624 rejected.append(f)
1604 1625 elif ds[f] != b'a':
1605 1626 ds.remove(f)
1606 1627 else:
1607 1628 ds.drop(f)
1608 1629 return rejected
1609 1630
1610 1631 def copy(self, source, dest):
1611 1632 try:
1612 1633 st = self._repo.wvfs.lstat(dest)
1613 1634 except OSError as err:
1614 1635 if err.errno != errno.ENOENT:
1615 1636 raise
1616 1637 self._repo.ui.warn(
1617 1638 _(b"%s does not exist!\n") % self._repo.dirstate.pathto(dest)
1618 1639 )
1619 1640 return
1620 1641 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1621 1642 self._repo.ui.warn(
1622 1643 _(b"copy failed: %s is not a file or a symbolic link\n")
1623 1644 % self._repo.dirstate.pathto(dest)
1624 1645 )
1625 1646 else:
1626 1647 with self._repo.wlock():
1627 1648 ds = self._repo.dirstate
1628 1649 if ds[dest] in b'?':
1629 1650 ds.add(dest)
1630 1651 elif ds[dest] in b'r':
1631 1652 ds.normallookup(dest)
1632 1653 ds.copy(source, dest)
1633 1654
1634 1655 def match(
1635 1656 self,
1636 1657 pats=None,
1637 1658 include=None,
1638 1659 exclude=None,
1639 1660 default=b'glob',
1640 1661 listsubrepos=False,
1641 1662 badfn=None,
1642 1663 ):
1643 1664 r = self._repo
1644 1665
1645 1666 # Only a case insensitive filesystem needs magic to translate user input
1646 1667 # to actual case in the filesystem.
1647 1668 icasefs = not util.fscasesensitive(r.root)
1648 1669 return matchmod.match(
1649 1670 r.root,
1650 1671 r.getcwd(),
1651 1672 pats,
1652 1673 include,
1653 1674 exclude,
1654 1675 default,
1655 1676 auditor=r.auditor,
1656 1677 ctx=self,
1657 1678 listsubrepos=listsubrepos,
1658 1679 badfn=badfn,
1659 1680 icasefs=icasefs,
1660 1681 )
1661 1682
1662 1683 def _filtersuspectsymlink(self, files):
1663 1684 if not files or self._repo.dirstate._checklink:
1664 1685 return files
1665 1686
1666 1687 # Symlink placeholders may get non-symlink-like contents
1667 1688 # via user error or dereferencing by NFS or Samba servers,
1668 1689 # so we filter out any placeholders that don't look like a
1669 1690 # symlink
1670 1691 sane = []
1671 1692 for f in files:
1672 1693 if self.flags(f) == b'l':
1673 1694 d = self[f].data()
1674 1695 if (
1675 1696 d == b''
1676 1697 or len(d) >= 1024
1677 1698 or b'\n' in d
1678 1699 or stringutil.binary(d)
1679 1700 ):
1680 1701 self._repo.ui.debug(
1681 1702 b'ignoring suspect symlink placeholder "%s"\n' % f
1682 1703 )
1683 1704 continue
1684 1705 sane.append(f)
1685 1706 return sane
1686 1707
1687 1708 def _checklookup(self, files):
1688 1709 # check for any possibly clean files
1689 1710 if not files:
1690 1711 return [], [], []
1691 1712
1692 1713 modified = []
1693 1714 deleted = []
1694 1715 fixup = []
1695 1716 pctx = self._parents[0]
1696 1717 # do a full compare of any files that might have changed
1697 1718 for f in sorted(files):
1698 1719 try:
1699 1720 # This will return True for a file that got replaced by a
1700 1721 # directory in the interim, but fixing that is pretty hard.
1701 1722 if (
1702 1723 f not in pctx
1703 1724 or self.flags(f) != pctx.flags(f)
1704 1725 or pctx[f].cmp(self[f])
1705 1726 ):
1706 1727 modified.append(f)
1707 1728 else:
1708 1729 fixup.append(f)
1709 1730 except (IOError, OSError):
1710 1731 # A file become inaccessible in between? Mark it as deleted,
1711 1732 # matching dirstate behavior (issue5584).
1712 1733 # The dirstate has more complex behavior around whether a
1713 1734 # missing file matches a directory, etc, but we don't need to
1714 1735 # bother with that: if f has made it to this point, we're sure
1715 1736 # it's in the dirstate.
1716 1737 deleted.append(f)
1717 1738
1718 1739 return modified, deleted, fixup
1719 1740
1720 1741 def _poststatusfixup(self, status, fixup):
1721 1742 """update dirstate for files that are actually clean"""
1722 1743 poststatus = self._repo.postdsstatus()
1723 1744 if fixup or poststatus:
1724 1745 try:
1725 1746 oldid = self._repo.dirstate.identity()
1726 1747
1727 1748 # updating the dirstate is optional
1728 1749 # so we don't wait on the lock
1729 1750 # wlock can invalidate the dirstate, so cache normal _after_
1730 1751 # taking the lock
1731 1752 with self._repo.wlock(False):
1732 1753 if self._repo.dirstate.identity() == oldid:
1733 1754 if fixup:
1734 1755 normal = self._repo.dirstate.normal
1735 1756 for f in fixup:
1736 1757 normal(f)
1737 1758 # write changes out explicitly, because nesting
1738 1759 # wlock at runtime may prevent 'wlock.release()'
1739 1760 # after this block from doing so for subsequent
1740 1761 # changing files
1741 1762 tr = self._repo.currenttransaction()
1742 1763 self._repo.dirstate.write(tr)
1743 1764
1744 1765 if poststatus:
1745 1766 for ps in poststatus:
1746 1767 ps(self, status)
1747 1768 else:
1748 1769 # in this case, writing changes out breaks
1749 1770 # consistency, because .hg/dirstate was
1750 1771 # already changed simultaneously after last
1751 1772 # caching (see also issue5584 for detail)
1752 1773 self._repo.ui.debug(
1753 1774 b'skip updating dirstate: identity mismatch\n'
1754 1775 )
1755 1776 except error.LockError:
1756 1777 pass
1757 1778 finally:
1758 1779 # Even if the wlock couldn't be grabbed, clear out the list.
1759 1780 self._repo.clearpostdsstatus()
1760 1781
1761 1782 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
1762 1783 '''Gets the status from the dirstate -- internal use only.'''
1763 1784 subrepos = []
1764 1785 if b'.hgsub' in self:
1765 1786 subrepos = sorted(self.substate)
1766 1787 cmp, s = self._repo.dirstate.status(
1767 1788 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1768 1789 )
1769 1790
1770 1791 # check for any possibly clean files
1771 1792 fixup = []
1772 1793 if cmp:
1773 1794 modified2, deleted2, fixup = self._checklookup(cmp)
1774 1795 s.modified.extend(modified2)
1775 1796 s.deleted.extend(deleted2)
1776 1797
1777 1798 if fixup and clean:
1778 1799 s.clean.extend(fixup)
1779 1800
1780 1801 self._poststatusfixup(s, fixup)
1781 1802
1782 1803 if match.always():
1783 1804 # cache for performance
1784 1805 if s.unknown or s.ignored or s.clean:
1785 1806 # "_status" is cached with list*=False in the normal route
1786 1807 self._status = scmutil.status(
1787 1808 s.modified, s.added, s.removed, s.deleted, [], [], []
1788 1809 )
1789 1810 else:
1790 1811 self._status = s
1791 1812
1792 1813 return s
1793 1814
1794 1815 @propertycache
1795 1816 def _copies(self):
1796 1817 p1copies = {}
1797 1818 p2copies = {}
1798 1819 parents = self._repo.dirstate.parents()
1799 1820 p1manifest = self._repo[parents[0]].manifest()
1800 1821 p2manifest = self._repo[parents[1]].manifest()
1801 1822 changedset = set(self.added()) | set(self.modified())
1802 1823 narrowmatch = self._repo.narrowmatch()
1803 1824 for dst, src in self._repo.dirstate.copies().items():
1804 1825 if dst not in changedset or not narrowmatch(dst):
1805 1826 continue
1806 1827 if src in p1manifest:
1807 1828 p1copies[dst] = src
1808 1829 elif src in p2manifest:
1809 1830 p2copies[dst] = src
1810 1831 return p1copies, p2copies
1811 1832
1812 1833 @propertycache
1813 1834 def _manifest(self):
1814 1835 """generate a manifest corresponding to the values in self._status
1815 1836
1816 1837 This reuse the file nodeid from parent, but we use special node
1817 1838 identifiers for added and modified files. This is used by manifests
1818 1839 merge to see that files are different and by update logic to avoid
1819 1840 deleting newly added files.
1820 1841 """
1821 1842 return self._buildstatusmanifest(self._status)
1822 1843
1823 1844 def _buildstatusmanifest(self, status):
1824 1845 """Builds a manifest that includes the given status results."""
1825 1846 parents = self.parents()
1826 1847
1827 1848 man = parents[0].manifest().copy()
1828 1849
1829 1850 ff = self._flagfunc
1830 1851 for i, l in (
1831 1852 (addednodeid, status.added),
1832 1853 (modifiednodeid, status.modified),
1833 1854 ):
1834 1855 for f in l:
1835 1856 man[f] = i
1836 1857 try:
1837 1858 man.setflag(f, ff(f))
1838 1859 except OSError:
1839 1860 pass
1840 1861
1841 1862 for f in status.deleted + status.removed:
1842 1863 if f in man:
1843 1864 del man[f]
1844 1865
1845 1866 return man
1846 1867
1847 1868 def _buildstatus(
1848 1869 self, other, s, match, listignored, listclean, listunknown
1849 1870 ):
1850 1871 """build a status with respect to another context
1851 1872
1852 1873 This includes logic for maintaining the fast path of status when
1853 1874 comparing the working directory against its parent, which is to skip
1854 1875 building a new manifest if self (working directory) is not comparing
1855 1876 against its parent (repo['.']).
1856 1877 """
1857 1878 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1858 1879 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1859 1880 # might have accidentally ended up with the entire contents of the file
1860 1881 # they are supposed to be linking to.
1861 1882 s.modified[:] = self._filtersuspectsymlink(s.modified)
1862 1883 if other != self._repo[b'.']:
1863 1884 s = super(workingctx, self)._buildstatus(
1864 1885 other, s, match, listignored, listclean, listunknown
1865 1886 )
1866 1887 return s
1867 1888
1868 1889 def _matchstatus(self, other, match):
1869 1890 """override the match method with a filter for directory patterns
1870 1891
1871 1892 We use inheritance to customize the match.bad method only in cases of
1872 1893 workingctx since it belongs only to the working directory when
1873 1894 comparing against the parent changeset.
1874 1895
1875 1896 If we aren't comparing against the working directory's parent, then we
1876 1897 just use the default match object sent to us.
1877 1898 """
1878 1899 if other != self._repo[b'.']:
1879 1900
1880 1901 def bad(f, msg):
1881 1902 # 'f' may be a directory pattern from 'match.files()',
1882 1903 # so 'f not in ctx1' is not enough
1883 1904 if f not in other and not other.hasdir(f):
1884 1905 self._repo.ui.warn(
1885 1906 b'%s: %s\n' % (self._repo.dirstate.pathto(f), msg)
1886 1907 )
1887 1908
1888 1909 match.bad = bad
1889 1910 return match
1890 1911
1891 1912 def walk(self, match):
1892 1913 '''Generates matching file names.'''
1893 1914 return sorted(
1894 1915 self._repo.dirstate.walk(
1895 1916 self._repo.narrowmatch(match),
1896 1917 subrepos=sorted(self.substate),
1897 1918 unknown=True,
1898 1919 ignored=False,
1899 1920 )
1900 1921 )
1901 1922
1902 1923 def matches(self, match):
1903 1924 match = self._repo.narrowmatch(match)
1904 1925 ds = self._repo.dirstate
1905 1926 return sorted(f for f in ds.matches(match) if ds[f] != b'r')
1906 1927
1907 1928 def markcommitted(self, node):
1908 1929 with self._repo.dirstate.parentchange():
1909 1930 for f in self.modified() + self.added():
1910 1931 self._repo.dirstate.normal(f)
1911 1932 for f in self.removed():
1912 1933 self._repo.dirstate.drop(f)
1913 1934 self._repo.dirstate.setparents(node)
1914 1935
1915 1936 # write changes out explicitly, because nesting wlock at
1916 1937 # runtime may prevent 'wlock.release()' in 'repo.commit()'
1917 1938 # from immediately doing so for subsequent changing files
1918 1939 self._repo.dirstate.write(self._repo.currenttransaction())
1919 1940
1920 1941 sparse.aftercommit(self._repo, node)
1921 1942
1922 1943
1923 1944 class committablefilectx(basefilectx):
1924 1945 """A committablefilectx provides common functionality for a file context
1925 1946 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1926 1947
1927 1948 def __init__(self, repo, path, filelog=None, ctx=None):
1928 1949 self._repo = repo
1929 1950 self._path = path
1930 1951 self._changeid = None
1931 1952 self._filerev = self._filenode = None
1932 1953
1933 1954 if filelog is not None:
1934 1955 self._filelog = filelog
1935 1956 if ctx:
1936 1957 self._changectx = ctx
1937 1958
1938 1959 def __nonzero__(self):
1939 1960 return True
1940 1961
1941 1962 __bool__ = __nonzero__
1942 1963
1943 1964 def linkrev(self):
1944 1965 # linked to self._changectx no matter if file is modified or not
1945 1966 return self.rev()
1946 1967
1947 1968 def renamed(self):
1948 1969 path = self.copysource()
1949 1970 if not path:
1950 1971 return None
1951 1972 return path, self._changectx._parents[0]._manifest.get(path, nullid)
1952 1973
1953 1974 def parents(self):
1954 1975 '''return parent filectxs, following copies if necessary'''
1955 1976
1956 1977 def filenode(ctx, path):
1957 1978 return ctx._manifest.get(path, nullid)
1958 1979
1959 1980 path = self._path
1960 1981 fl = self._filelog
1961 1982 pcl = self._changectx._parents
1962 1983 renamed = self.renamed()
1963 1984
1964 1985 if renamed:
1965 1986 pl = [renamed + (None,)]
1966 1987 else:
1967 1988 pl = [(path, filenode(pcl[0], path), fl)]
1968 1989
1969 1990 for pc in pcl[1:]:
1970 1991 pl.append((path, filenode(pc, path), fl))
1971 1992
1972 1993 return [
1973 1994 self._parentfilectx(p, fileid=n, filelog=l)
1974 1995 for p, n, l in pl
1975 1996 if n != nullid
1976 1997 ]
1977 1998
1978 1999 def children(self):
1979 2000 return []
1980 2001
1981 2002
1982 2003 class workingfilectx(committablefilectx):
1983 2004 """A workingfilectx object makes access to data related to a particular
1984 2005 file in the working directory convenient."""
1985 2006
1986 2007 def __init__(self, repo, path, filelog=None, workingctx=None):
1987 2008 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
1988 2009
1989 2010 @propertycache
1990 2011 def _changectx(self):
1991 2012 return workingctx(self._repo)
1992 2013
1993 2014 def data(self):
1994 2015 return self._repo.wread(self._path)
1995 2016
1996 2017 def copysource(self):
1997 2018 return self._repo.dirstate.copied(self._path)
1998 2019
1999 2020 def size(self):
2000 2021 return self._repo.wvfs.lstat(self._path).st_size
2001 2022
2002 2023 def lstat(self):
2003 2024 return self._repo.wvfs.lstat(self._path)
2004 2025
2005 2026 def date(self):
2006 2027 t, tz = self._changectx.date()
2007 2028 try:
2008 2029 return (self._repo.wvfs.lstat(self._path)[stat.ST_MTIME], tz)
2009 2030 except OSError as err:
2010 2031 if err.errno != errno.ENOENT:
2011 2032 raise
2012 2033 return (t, tz)
2013 2034
2014 2035 def exists(self):
2015 2036 return self._repo.wvfs.exists(self._path)
2016 2037
2017 2038 def lexists(self):
2018 2039 return self._repo.wvfs.lexists(self._path)
2019 2040
2020 2041 def audit(self):
2021 2042 return self._repo.wvfs.audit(self._path)
2022 2043
2023 2044 def cmp(self, fctx):
2024 2045 """compare with other file context
2025 2046
2026 2047 returns True if different than fctx.
2027 2048 """
2028 2049 # fctx should be a filectx (not a workingfilectx)
2029 2050 # invert comparison to reuse the same code path
2030 2051 return fctx.cmp(self)
2031 2052
2032 2053 def remove(self, ignoremissing=False):
2033 2054 """wraps unlink for a repo's working directory"""
2034 2055 rmdir = self._repo.ui.configbool(b'experimental', b'removeemptydirs')
2035 2056 self._repo.wvfs.unlinkpath(
2036 2057 self._path, ignoremissing=ignoremissing, rmdir=rmdir
2037 2058 )
2038 2059
2039 2060 def write(self, data, flags, backgroundclose=False, **kwargs):
2040 2061 """wraps repo.wwrite"""
2041 2062 return self._repo.wwrite(
2042 2063 self._path, data, flags, backgroundclose=backgroundclose, **kwargs
2043 2064 )
2044 2065
2045 2066 def markcopied(self, src):
2046 2067 """marks this file a copy of `src`"""
2047 2068 self._repo.dirstate.copy(src, self._path)
2048 2069
2049 2070 def clearunknown(self):
2050 2071 """Removes conflicting items in the working directory so that
2051 2072 ``write()`` can be called successfully.
2052 2073 """
2053 2074 wvfs = self._repo.wvfs
2054 2075 f = self._path
2055 2076 wvfs.audit(f)
2056 2077 if self._repo.ui.configbool(
2057 2078 b'experimental', b'merge.checkpathconflicts'
2058 2079 ):
2059 2080 # remove files under the directory as they should already be
2060 2081 # warned and backed up
2061 2082 if wvfs.isdir(f) and not wvfs.islink(f):
2062 2083 wvfs.rmtree(f, forcibly=True)
2063 2084 for p in reversed(list(util.finddirs(f))):
2064 2085 if wvfs.isfileorlink(p):
2065 2086 wvfs.unlink(p)
2066 2087 break
2067 2088 else:
2068 2089 # don't remove files if path conflicts are not processed
2069 2090 if wvfs.isdir(f) and not wvfs.islink(f):
2070 2091 wvfs.removedirs(f)
2071 2092
2072 2093 def setflags(self, l, x):
2073 2094 self._repo.wvfs.setflags(self._path, l, x)
2074 2095
2075 2096
2076 2097 class overlayworkingctx(committablectx):
2077 2098 """Wraps another mutable context with a write-back cache that can be
2078 2099 converted into a commit context.
2079 2100
2080 2101 self._cache[path] maps to a dict with keys: {
2081 2102 'exists': bool?
2082 2103 'date': date?
2083 2104 'data': str?
2084 2105 'flags': str?
2085 2106 'copied': str? (path or None)
2086 2107 }
2087 2108 If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
2088 2109 is `False`, the file was deleted.
2089 2110 """
2090 2111
2091 2112 def __init__(self, repo):
2092 2113 super(overlayworkingctx, self).__init__(repo)
2093 2114 self.clean()
2094 2115
2095 2116 def setbase(self, wrappedctx):
2096 2117 self._wrappedctx = wrappedctx
2097 2118 self._parents = [wrappedctx]
2098 2119 # Drop old manifest cache as it is now out of date.
2099 2120 # This is necessary when, e.g., rebasing several nodes with one
2100 2121 # ``overlayworkingctx`` (e.g. with --collapse).
2101 2122 util.clearcachedproperty(self, b'_manifest')
2102 2123
2103 2124 def data(self, path):
2104 2125 if self.isdirty(path):
2105 2126 if self._cache[path][b'exists']:
2106 2127 if self._cache[path][b'data'] is not None:
2107 2128 return self._cache[path][b'data']
2108 2129 else:
2109 2130 # Must fallback here, too, because we only set flags.
2110 2131 return self._wrappedctx[path].data()
2111 2132 else:
2112 2133 raise error.ProgrammingError(
2113 2134 b"No such file or directory: %s" % path
2114 2135 )
2115 2136 else:
2116 2137 return self._wrappedctx[path].data()
2117 2138
2118 2139 @propertycache
2119 2140 def _manifest(self):
2120 2141 parents = self.parents()
2121 2142 man = parents[0].manifest().copy()
2122 2143
2123 2144 flag = self._flagfunc
2124 2145 for path in self.added():
2125 2146 man[path] = addednodeid
2126 2147 man.setflag(path, flag(path))
2127 2148 for path in self.modified():
2128 2149 man[path] = modifiednodeid
2129 2150 man.setflag(path, flag(path))
2130 2151 for path in self.removed():
2131 2152 del man[path]
2132 2153 return man
2133 2154
2134 2155 @propertycache
2135 2156 def _flagfunc(self):
2136 2157 def f(path):
2137 2158 return self._cache[path][b'flags']
2138 2159
2139 2160 return f
2140 2161
2141 2162 def files(self):
2142 2163 return sorted(self.added() + self.modified() + self.removed())
2143 2164
2144 2165 def modified(self):
2145 2166 return [
2146 2167 f
2147 2168 for f in self._cache.keys()
2148 2169 if self._cache[f][b'exists'] and self._existsinparent(f)
2149 2170 ]
2150 2171
2151 2172 def added(self):
2152 2173 return [
2153 2174 f
2154 2175 for f in self._cache.keys()
2155 2176 if self._cache[f][b'exists'] and not self._existsinparent(f)
2156 2177 ]
2157 2178
2158 2179 def removed(self):
2159 2180 return [
2160 2181 f
2161 2182 for f in self._cache.keys()
2162 2183 if not self._cache[f][b'exists'] and self._existsinparent(f)
2163 2184 ]
2164 2185
2165 2186 def p1copies(self):
2166 2187 copies = self._repo._wrappedctx.p1copies().copy()
2167 2188 narrowmatch = self._repo.narrowmatch()
2168 2189 for f in self._cache.keys():
2169 2190 if not narrowmatch(f):
2170 2191 continue
2171 2192 copies.pop(f, None) # delete if it exists
2172 2193 source = self._cache[f][b'copied']
2173 2194 if source:
2174 2195 copies[f] = source
2175 2196 return copies
2176 2197
2177 2198 def p2copies(self):
2178 2199 copies = self._repo._wrappedctx.p2copies().copy()
2179 2200 narrowmatch = self._repo.narrowmatch()
2180 2201 for f in self._cache.keys():
2181 2202 if not narrowmatch(f):
2182 2203 continue
2183 2204 copies.pop(f, None) # delete if it exists
2184 2205 source = self._cache[f][b'copied']
2185 2206 if source:
2186 2207 copies[f] = source
2187 2208 return copies
2188 2209
2189 2210 def isinmemory(self):
2190 2211 return True
2191 2212
2192 2213 def filedate(self, path):
2193 2214 if self.isdirty(path):
2194 2215 return self._cache[path][b'date']
2195 2216 else:
2196 2217 return self._wrappedctx[path].date()
2197 2218
2198 2219 def markcopied(self, path, origin):
2199 2220 self._markdirty(
2200 2221 path,
2201 2222 exists=True,
2202 2223 date=self.filedate(path),
2203 2224 flags=self.flags(path),
2204 2225 copied=origin,
2205 2226 )
2206 2227
2207 2228 def copydata(self, path):
2208 2229 if self.isdirty(path):
2209 2230 return self._cache[path][b'copied']
2210 2231 else:
2211 2232 return None
2212 2233
2213 2234 def flags(self, path):
2214 2235 if self.isdirty(path):
2215 2236 if self._cache[path][b'exists']:
2216 2237 return self._cache[path][b'flags']
2217 2238 else:
2218 2239 raise error.ProgrammingError(
2219 2240 b"No such file or directory: %s" % self._path
2220 2241 )
2221 2242 else:
2222 2243 return self._wrappedctx[path].flags()
2223 2244
2224 2245 def __contains__(self, key):
2225 2246 if key in self._cache:
2226 2247 return self._cache[key][b'exists']
2227 2248 return key in self.p1()
2228 2249
2229 2250 def _existsinparent(self, path):
2230 2251 try:
2231 2252 # ``commitctx` raises a ``ManifestLookupError`` if a path does not
2232 2253 # exist, unlike ``workingctx``, which returns a ``workingfilectx``
2233 2254 # with an ``exists()`` function.
2234 2255 self._wrappedctx[path]
2235 2256 return True
2236 2257 except error.ManifestLookupError:
2237 2258 return False
2238 2259
2239 2260 def _auditconflicts(self, path):
2240 2261 """Replicates conflict checks done by wvfs.write().
2241 2262
2242 2263 Since we never write to the filesystem and never call `applyupdates` in
2243 2264 IMM, we'll never check that a path is actually writable -- e.g., because
2244 2265 it adds `a/foo`, but `a` is actually a file in the other commit.
2245 2266 """
2246 2267
2247 2268 def fail(path, component):
2248 2269 # p1() is the base and we're receiving "writes" for p2()'s
2249 2270 # files.
2250 2271 if b'l' in self.p1()[component].flags():
2251 2272 raise error.Abort(
2252 2273 b"error: %s conflicts with symlink %s "
2253 2274 b"in %d." % (path, component, self.p1().rev())
2254 2275 )
2255 2276 else:
2256 2277 raise error.Abort(
2257 2278 b"error: '%s' conflicts with file '%s' in "
2258 2279 b"%d." % (path, component, self.p1().rev())
2259 2280 )
2260 2281
2261 2282 # Test that each new directory to be created to write this path from p2
2262 2283 # is not a file in p1.
2263 2284 components = path.split(b'/')
2264 2285 for i in pycompat.xrange(len(components)):
2265 2286 component = b"/".join(components[0:i])
2266 2287 if component in self:
2267 2288 fail(path, component)
2268 2289
2269 2290 # Test the other direction -- that this path from p2 isn't a directory
2270 2291 # in p1 (test that p1 doesn't have any paths matching `path/*`).
2271 2292 match = self.match([path], default=b'path')
2272 2293 matches = self.p1().manifest().matches(match)
2273 2294 mfiles = matches.keys()
2274 2295 if len(mfiles) > 0:
2275 2296 if len(mfiles) == 1 and mfiles[0] == path:
2276 2297 return
2277 2298 # omit the files which are deleted in current IMM wctx
2278 2299 mfiles = [m for m in mfiles if m in self]
2279 2300 if not mfiles:
2280 2301 return
2281 2302 raise error.Abort(
2282 2303 b"error: file '%s' cannot be written because "
2283 2304 b" '%s/' is a directory in %s (containing %d "
2284 2305 b"entries: %s)"
2285 2306 % (path, path, self.p1(), len(mfiles), b', '.join(mfiles))
2286 2307 )
2287 2308
2288 2309 def write(self, path, data, flags=b'', **kwargs):
2289 2310 if data is None:
2290 2311 raise error.ProgrammingError(b"data must be non-None")
2291 2312 self._auditconflicts(path)
2292 2313 self._markdirty(
2293 2314 path, exists=True, data=data, date=dateutil.makedate(), flags=flags
2294 2315 )
2295 2316
2296 2317 def setflags(self, path, l, x):
2297 2318 flag = b''
2298 2319 if l:
2299 2320 flag = b'l'
2300 2321 elif x:
2301 2322 flag = b'x'
2302 2323 self._markdirty(path, exists=True, date=dateutil.makedate(), flags=flag)
2303 2324
2304 2325 def remove(self, path):
2305 2326 self._markdirty(path, exists=False)
2306 2327
2307 2328 def exists(self, path):
2308 2329 """exists behaves like `lexists`, but needs to follow symlinks and
2309 2330 return False if they are broken.
2310 2331 """
2311 2332 if self.isdirty(path):
2312 2333 # If this path exists and is a symlink, "follow" it by calling
2313 2334 # exists on the destination path.
2314 2335 if (
2315 2336 self._cache[path][b'exists']
2316 2337 and b'l' in self._cache[path][b'flags']
2317 2338 ):
2318 2339 return self.exists(self._cache[path][b'data'].strip())
2319 2340 else:
2320 2341 return self._cache[path][b'exists']
2321 2342
2322 2343 return self._existsinparent(path)
2323 2344
2324 2345 def lexists(self, path):
2325 2346 """lexists returns True if the path exists"""
2326 2347 if self.isdirty(path):
2327 2348 return self._cache[path][b'exists']
2328 2349
2329 2350 return self._existsinparent(path)
2330 2351
2331 2352 def size(self, path):
2332 2353 if self.isdirty(path):
2333 2354 if self._cache[path][b'exists']:
2334 2355 return len(self._cache[path][b'data'])
2335 2356 else:
2336 2357 raise error.ProgrammingError(
2337 2358 b"No such file or directory: %s" % self._path
2338 2359 )
2339 2360 return self._wrappedctx[path].size()
2340 2361
2341 2362 def tomemctx(
2342 2363 self,
2343 2364 text,
2344 2365 branch=None,
2345 2366 extra=None,
2346 2367 date=None,
2347 2368 parents=None,
2348 2369 user=None,
2349 2370 editor=None,
2350 2371 ):
2351 2372 """Converts this ``overlayworkingctx`` into a ``memctx`` ready to be
2352 2373 committed.
2353 2374
2354 2375 ``text`` is the commit message.
2355 2376 ``parents`` (optional) are rev numbers.
2356 2377 """
2357 2378 # Default parents to the wrapped contexts' if not passed.
2358 2379 if parents is None:
2359 2380 parents = self._wrappedctx.parents()
2360 2381 if len(parents) == 1:
2361 2382 parents = (parents[0], None)
2362 2383
2363 2384 # ``parents`` is passed as rev numbers; convert to ``commitctxs``.
2364 2385 if parents[1] is None:
2365 2386 parents = (self._repo[parents[0]], None)
2366 2387 else:
2367 2388 parents = (self._repo[parents[0]], self._repo[parents[1]])
2368 2389
2369 2390 files = self.files()
2370 2391
2371 2392 def getfile(repo, memctx, path):
2372 2393 if self._cache[path][b'exists']:
2373 2394 return memfilectx(
2374 2395 repo,
2375 2396 memctx,
2376 2397 path,
2377 2398 self._cache[path][b'data'],
2378 2399 b'l' in self._cache[path][b'flags'],
2379 2400 b'x' in self._cache[path][b'flags'],
2380 2401 self._cache[path][b'copied'],
2381 2402 )
2382 2403 else:
2383 2404 # Returning None, but including the path in `files`, is
2384 2405 # necessary for memctx to register a deletion.
2385 2406 return None
2386 2407
2387 2408 return memctx(
2388 2409 self._repo,
2389 2410 parents,
2390 2411 text,
2391 2412 files,
2392 2413 getfile,
2393 2414 date=date,
2394 2415 extra=extra,
2395 2416 user=user,
2396 2417 branch=branch,
2397 2418 editor=editor,
2398 2419 )
2399 2420
2400 2421 def isdirty(self, path):
2401 2422 return path in self._cache
2402 2423
2403 2424 def isempty(self):
2404 2425 # We need to discard any keys that are actually clean before the empty
2405 2426 # commit check.
2406 2427 self._compact()
2407 2428 return len(self._cache) == 0
2408 2429
2409 2430 def clean(self):
2410 2431 self._cache = {}
2411 2432
2412 2433 def _compact(self):
2413 2434 """Removes keys from the cache that are actually clean, by comparing
2414 2435 them with the underlying context.
2415 2436
2416 2437 This can occur during the merge process, e.g. by passing --tool :local
2417 2438 to resolve a conflict.
2418 2439 """
2419 2440 keys = []
2420 2441 # This won't be perfect, but can help performance significantly when
2421 2442 # using things like remotefilelog.
2422 2443 scmutil.prefetchfiles(
2423 2444 self.repo(),
2424 2445 [self.p1().rev()],
2425 2446 scmutil.matchfiles(self.repo(), self._cache.keys()),
2426 2447 )
2427 2448
2428 2449 for path in self._cache.keys():
2429 2450 cache = self._cache[path]
2430 2451 try:
2431 2452 underlying = self._wrappedctx[path]
2432 2453 if (
2433 2454 underlying.data() == cache[b'data']
2434 2455 and underlying.flags() == cache[b'flags']
2435 2456 ):
2436 2457 keys.append(path)
2437 2458 except error.ManifestLookupError:
2438 2459 # Path not in the underlying manifest (created).
2439 2460 continue
2440 2461
2441 2462 for path in keys:
2442 2463 del self._cache[path]
2443 2464 return keys
2444 2465
2445 2466 def _markdirty(
2446 2467 self, path, exists, data=None, date=None, flags=b'', copied=None
2447 2468 ):
2448 2469 # data not provided, let's see if we already have some; if not, let's
2449 2470 # grab it from our underlying context, so that we always have data if
2450 2471 # the file is marked as existing.
2451 2472 if exists and data is None:
2452 2473 oldentry = self._cache.get(path) or {}
2453 2474 data = oldentry.get(b'data')
2454 2475 if data is None:
2455 2476 data = self._wrappedctx[path].data()
2456 2477
2457 2478 self._cache[path] = {
2458 2479 b'exists': exists,
2459 2480 b'data': data,
2460 2481 b'date': date,
2461 2482 b'flags': flags,
2462 2483 b'copied': copied,
2463 2484 }
2464 2485
2465 2486 def filectx(self, path, filelog=None):
2466 2487 return overlayworkingfilectx(
2467 2488 self._repo, path, parent=self, filelog=filelog
2468 2489 )
2469 2490
2470 2491
2471 2492 class overlayworkingfilectx(committablefilectx):
2472 2493 """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
2473 2494 cache, which can be flushed through later by calling ``flush()``."""
2474 2495
2475 2496 def __init__(self, repo, path, filelog=None, parent=None):
2476 2497 super(overlayworkingfilectx, self).__init__(repo, path, filelog, parent)
2477 2498 self._repo = repo
2478 2499 self._parent = parent
2479 2500 self._path = path
2480 2501
2481 2502 def cmp(self, fctx):
2482 2503 return self.data() != fctx.data()
2483 2504
2484 2505 def changectx(self):
2485 2506 return self._parent
2486 2507
2487 2508 def data(self):
2488 2509 return self._parent.data(self._path)
2489 2510
2490 2511 def date(self):
2491 2512 return self._parent.filedate(self._path)
2492 2513
2493 2514 def exists(self):
2494 2515 return self.lexists()
2495 2516
2496 2517 def lexists(self):
2497 2518 return self._parent.exists(self._path)
2498 2519
2499 2520 def copysource(self):
2500 2521 return self._parent.copydata(self._path)
2501 2522
2502 2523 def size(self):
2503 2524 return self._parent.size(self._path)
2504 2525
2505 2526 def markcopied(self, origin):
2506 2527 self._parent.markcopied(self._path, origin)
2507 2528
2508 2529 def audit(self):
2509 2530 pass
2510 2531
2511 2532 def flags(self):
2512 2533 return self._parent.flags(self._path)
2513 2534
2514 2535 def setflags(self, islink, isexec):
2515 2536 return self._parent.setflags(self._path, islink, isexec)
2516 2537
2517 2538 def write(self, data, flags, backgroundclose=False, **kwargs):
2518 2539 return self._parent.write(self._path, data, flags, **kwargs)
2519 2540
2520 2541 def remove(self, ignoremissing=False):
2521 2542 return self._parent.remove(self._path)
2522 2543
2523 2544 def clearunknown(self):
2524 2545 pass
2525 2546
2526 2547
2527 2548 class workingcommitctx(workingctx):
2528 2549 """A workingcommitctx object makes access to data related to
2529 2550 the revision being committed convenient.
2530 2551
2531 2552 This hides changes in the working directory, if they aren't
2532 2553 committed in this context.
2533 2554 """
2534 2555
2535 2556 def __init__(
2536 2557 self, repo, changes, text=b"", user=None, date=None, extra=None
2537 2558 ):
2538 2559 super(workingcommitctx, self).__init__(
2539 2560 repo, text, user, date, extra, changes
2540 2561 )
2541 2562
2542 2563 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
2543 2564 """Return matched files only in ``self._status``
2544 2565
2545 2566 Uncommitted files appear "clean" via this context, even if
2546 2567 they aren't actually so in the working directory.
2547 2568 """
2548 2569 if clean:
2549 2570 clean = [f for f in self._manifest if f not in self._changedset]
2550 2571 else:
2551 2572 clean = []
2552 2573 return scmutil.status(
2553 2574 [f for f in self._status.modified if match(f)],
2554 2575 [f for f in self._status.added if match(f)],
2555 2576 [f for f in self._status.removed if match(f)],
2556 2577 [],
2557 2578 [],
2558 2579 [],
2559 2580 clean,
2560 2581 )
2561 2582
2562 2583 @propertycache
2563 2584 def _changedset(self):
2564 2585 """Return the set of files changed in this context
2565 2586 """
2566 2587 changed = set(self._status.modified)
2567 2588 changed.update(self._status.added)
2568 2589 changed.update(self._status.removed)
2569 2590 return changed
2570 2591
2571 2592
2572 2593 def makecachingfilectxfn(func):
2573 2594 """Create a filectxfn that caches based on the path.
2574 2595
2575 2596 We can't use util.cachefunc because it uses all arguments as the cache
2576 2597 key and this creates a cycle since the arguments include the repo and
2577 2598 memctx.
2578 2599 """
2579 2600 cache = {}
2580 2601
2581 2602 def getfilectx(repo, memctx, path):
2582 2603 if path not in cache:
2583 2604 cache[path] = func(repo, memctx, path)
2584 2605 return cache[path]
2585 2606
2586 2607 return getfilectx
2587 2608
2588 2609
2589 2610 def memfilefromctx(ctx):
2590 2611 """Given a context return a memfilectx for ctx[path]
2591 2612
2592 2613 This is a convenience method for building a memctx based on another
2593 2614 context.
2594 2615 """
2595 2616
2596 2617 def getfilectx(repo, memctx, path):
2597 2618 fctx = ctx[path]
2598 2619 copysource = fctx.copysource()
2599 2620 return memfilectx(
2600 2621 repo,
2601 2622 memctx,
2602 2623 path,
2603 2624 fctx.data(),
2604 2625 islink=fctx.islink(),
2605 2626 isexec=fctx.isexec(),
2606 2627 copysource=copysource,
2607 2628 )
2608 2629
2609 2630 return getfilectx
2610 2631
2611 2632
2612 2633 def memfilefrompatch(patchstore):
2613 2634 """Given a patch (e.g. patchstore object) return a memfilectx
2614 2635
2615 2636 This is a convenience method for building a memctx based on a patchstore.
2616 2637 """
2617 2638
2618 2639 def getfilectx(repo, memctx, path):
2619 2640 data, mode, copysource = patchstore.getfile(path)
2620 2641 if data is None:
2621 2642 return None
2622 2643 islink, isexec = mode
2623 2644 return memfilectx(
2624 2645 repo,
2625 2646 memctx,
2626 2647 path,
2627 2648 data,
2628 2649 islink=islink,
2629 2650 isexec=isexec,
2630 2651 copysource=copysource,
2631 2652 )
2632 2653
2633 2654 return getfilectx
2634 2655
2635 2656
2636 2657 class memctx(committablectx):
2637 2658 """Use memctx to perform in-memory commits via localrepo.commitctx().
2638 2659
2639 2660 Revision information is supplied at initialization time while
2640 2661 related files data and is made available through a callback
2641 2662 mechanism. 'repo' is the current localrepo, 'parents' is a
2642 2663 sequence of two parent revisions identifiers (pass None for every
2643 2664 missing parent), 'text' is the commit message and 'files' lists
2644 2665 names of files touched by the revision (normalized and relative to
2645 2666 repository root).
2646 2667
2647 2668 filectxfn(repo, memctx, path) is a callable receiving the
2648 2669 repository, the current memctx object and the normalized path of
2649 2670 requested file, relative to repository root. It is fired by the
2650 2671 commit function for every file in 'files', but calls order is
2651 2672 undefined. If the file is available in the revision being
2652 2673 committed (updated or added), filectxfn returns a memfilectx
2653 2674 object. If the file was removed, filectxfn return None for recent
2654 2675 Mercurial. Moved files are represented by marking the source file
2655 2676 removed and the new file added with copy information (see
2656 2677 memfilectx).
2657 2678
2658 2679 user receives the committer name and defaults to current
2659 2680 repository username, date is the commit date in any format
2660 2681 supported by dateutil.parsedate() and defaults to current date, extra
2661 2682 is a dictionary of metadata or is left empty.
2662 2683 """
2663 2684
2664 2685 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2665 2686 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2666 2687 # this field to determine what to do in filectxfn.
2667 2688 _returnnoneformissingfiles = True
2668 2689
2669 2690 def __init__(
2670 2691 self,
2671 2692 repo,
2672 2693 parents,
2673 2694 text,
2674 2695 files,
2675 2696 filectxfn,
2676 2697 user=None,
2677 2698 date=None,
2678 2699 extra=None,
2679 2700 branch=None,
2680 2701 editor=False,
2681 2702 ):
2682 2703 super(memctx, self).__init__(
2683 2704 repo, text, user, date, extra, branch=branch
2684 2705 )
2685 2706 self._rev = None
2686 2707 self._node = None
2687 2708 parents = [(p or nullid) for p in parents]
2688 2709 p1, p2 = parents
2689 2710 self._parents = [self._repo[p] for p in (p1, p2)]
2690 2711 files = sorted(set(files))
2691 2712 self._files = files
2692 2713 self.substate = {}
2693 2714
2694 2715 if isinstance(filectxfn, patch.filestore):
2695 2716 filectxfn = memfilefrompatch(filectxfn)
2696 2717 elif not callable(filectxfn):
2697 2718 # if store is not callable, wrap it in a function
2698 2719 filectxfn = memfilefromctx(filectxfn)
2699 2720
2700 2721 # memoizing increases performance for e.g. vcs convert scenarios.
2701 2722 self._filectxfn = makecachingfilectxfn(filectxfn)
2702 2723
2703 2724 if editor:
2704 2725 self._text = editor(self._repo, self, [])
2705 2726 self._repo.savecommitmessage(self._text)
2706 2727
2707 2728 def filectx(self, path, filelog=None):
2708 2729 """get a file context from the working directory
2709 2730
2710 2731 Returns None if file doesn't exist and should be removed."""
2711 2732 return self._filectxfn(self._repo, self, path)
2712 2733
2713 2734 def commit(self):
2714 2735 """commit context to the repo"""
2715 2736 return self._repo.commitctx(self)
2716 2737
2717 2738 @propertycache
2718 2739 def _manifest(self):
2719 2740 """generate a manifest based on the return values of filectxfn"""
2720 2741
2721 2742 # keep this simple for now; just worry about p1
2722 2743 pctx = self._parents[0]
2723 2744 man = pctx.manifest().copy()
2724 2745
2725 2746 for f in self._status.modified:
2726 2747 man[f] = modifiednodeid
2727 2748
2728 2749 for f in self._status.added:
2729 2750 man[f] = addednodeid
2730 2751
2731 2752 for f in self._status.removed:
2732 2753 if f in man:
2733 2754 del man[f]
2734 2755
2735 2756 return man
2736 2757
2737 2758 @propertycache
2738 2759 def _status(self):
2739 2760 """Calculate exact status from ``files`` specified at construction
2740 2761 """
2741 2762 man1 = self.p1().manifest()
2742 2763 p2 = self._parents[1]
2743 2764 # "1 < len(self._parents)" can't be used for checking
2744 2765 # existence of the 2nd parent, because "memctx._parents" is
2745 2766 # explicitly initialized by the list, of which length is 2.
2746 2767 if p2.node() != nullid:
2747 2768 man2 = p2.manifest()
2748 2769 managing = lambda f: f in man1 or f in man2
2749 2770 else:
2750 2771 managing = lambda f: f in man1
2751 2772
2752 2773 modified, added, removed = [], [], []
2753 2774 for f in self._files:
2754 2775 if not managing(f):
2755 2776 added.append(f)
2756 2777 elif self[f]:
2757 2778 modified.append(f)
2758 2779 else:
2759 2780 removed.append(f)
2760 2781
2761 2782 return scmutil.status(modified, added, removed, [], [], [], [])
2762 2783
2763 2784
2764 2785 class memfilectx(committablefilectx):
2765 2786 """memfilectx represents an in-memory file to commit.
2766 2787
2767 2788 See memctx and committablefilectx for more details.
2768 2789 """
2769 2790
2770 2791 def __init__(
2771 2792 self,
2772 2793 repo,
2773 2794 changectx,
2774 2795 path,
2775 2796 data,
2776 2797 islink=False,
2777 2798 isexec=False,
2778 2799 copysource=None,
2779 2800 ):
2780 2801 """
2781 2802 path is the normalized file path relative to repository root.
2782 2803 data is the file content as a string.
2783 2804 islink is True if the file is a symbolic link.
2784 2805 isexec is True if the file is executable.
2785 2806 copied is the source file path if current file was copied in the
2786 2807 revision being committed, or None."""
2787 2808 super(memfilectx, self).__init__(repo, path, None, changectx)
2788 2809 self._data = data
2789 2810 if islink:
2790 2811 self._flags = b'l'
2791 2812 elif isexec:
2792 2813 self._flags = b'x'
2793 2814 else:
2794 2815 self._flags = b''
2795 2816 self._copysource = copysource
2796 2817
2797 2818 def copysource(self):
2798 2819 return self._copysource
2799 2820
2800 2821 def cmp(self, fctx):
2801 2822 return self.data() != fctx.data()
2802 2823
2803 2824 def data(self):
2804 2825 return self._data
2805 2826
2806 2827 def remove(self, ignoremissing=False):
2807 2828 """wraps unlink for a repo's working directory"""
2808 2829 # need to figure out what to do here
2809 2830 del self._changectx[self._path]
2810 2831
2811 2832 def write(self, data, flags, **kwargs):
2812 2833 """wraps repo.wwrite"""
2813 2834 self._data = data
2814 2835
2815 2836
2816 2837 class metadataonlyctx(committablectx):
2817 2838 """Like memctx but it's reusing the manifest of different commit.
2818 2839 Intended to be used by lightweight operations that are creating
2819 2840 metadata-only changes.
2820 2841
2821 2842 Revision information is supplied at initialization time. 'repo' is the
2822 2843 current localrepo, 'ctx' is original revision which manifest we're reuisng
2823 2844 'parents' is a sequence of two parent revisions identifiers (pass None for
2824 2845 every missing parent), 'text' is the commit.
2825 2846
2826 2847 user receives the committer name and defaults to current repository
2827 2848 username, date is the commit date in any format supported by
2828 2849 dateutil.parsedate() and defaults to current date, extra is a dictionary of
2829 2850 metadata or is left empty.
2830 2851 """
2831 2852
2832 2853 def __init__(
2833 2854 self,
2834 2855 repo,
2835 2856 originalctx,
2836 2857 parents=None,
2837 2858 text=None,
2838 2859 user=None,
2839 2860 date=None,
2840 2861 extra=None,
2841 2862 editor=False,
2842 2863 ):
2843 2864 if text is None:
2844 2865 text = originalctx.description()
2845 2866 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
2846 2867 self._rev = None
2847 2868 self._node = None
2848 2869 self._originalctx = originalctx
2849 2870 self._manifestnode = originalctx.manifestnode()
2850 2871 if parents is None:
2851 2872 parents = originalctx.parents()
2852 2873 else:
2853 2874 parents = [repo[p] for p in parents if p is not None]
2854 2875 parents = parents[:]
2855 2876 while len(parents) < 2:
2856 2877 parents.append(repo[nullid])
2857 2878 p1, p2 = self._parents = parents
2858 2879
2859 2880 # sanity check to ensure that the reused manifest parents are
2860 2881 # manifests of our commit parents
2861 2882 mp1, mp2 = self.manifestctx().parents
2862 2883 if p1 != nullid and p1.manifestnode() != mp1:
2863 2884 raise RuntimeError(
2864 2885 r"can't reuse the manifest: its p1 "
2865 2886 r"doesn't match the new ctx p1"
2866 2887 )
2867 2888 if p2 != nullid and p2.manifestnode() != mp2:
2868 2889 raise RuntimeError(
2869 2890 r"can't reuse the manifest: "
2870 2891 r"its p2 doesn't match the new ctx p2"
2871 2892 )
2872 2893
2873 2894 self._files = originalctx.files()
2874 2895 self.substate = {}
2875 2896
2876 2897 if editor:
2877 2898 self._text = editor(self._repo, self, [])
2878 2899 self._repo.savecommitmessage(self._text)
2879 2900
2880 2901 def manifestnode(self):
2881 2902 return self._manifestnode
2882 2903
2883 2904 @property
2884 2905 def _manifestctx(self):
2885 2906 return self._repo.manifestlog[self._manifestnode]
2886 2907
2887 2908 def filectx(self, path, filelog=None):
2888 2909 return self._originalctx.filectx(path, filelog=filelog)
2889 2910
2890 2911 def commit(self):
2891 2912 """commit context to the repo"""
2892 2913 return self._repo.commitctx(self)
2893 2914
2894 2915 @property
2895 2916 def _manifest(self):
2896 2917 return self._originalctx.manifest()
2897 2918
2898 2919 @propertycache
2899 2920 def _status(self):
2900 2921 """Calculate exact status from ``files`` specified in the ``origctx``
2901 2922 and parents manifests.
2902 2923 """
2903 2924 man1 = self.p1().manifest()
2904 2925 p2 = self._parents[1]
2905 2926 # "1 < len(self._parents)" can't be used for checking
2906 2927 # existence of the 2nd parent, because "metadataonlyctx._parents" is
2907 2928 # explicitly initialized by the list, of which length is 2.
2908 2929 if p2.node() != nullid:
2909 2930 man2 = p2.manifest()
2910 2931 managing = lambda f: f in man1 or f in man2
2911 2932 else:
2912 2933 managing = lambda f: f in man1
2913 2934
2914 2935 modified, added, removed = [], [], []
2915 2936 for f in self._files:
2916 2937 if not managing(f):
2917 2938 added.append(f)
2918 2939 elif f in self:
2919 2940 modified.append(f)
2920 2941 else:
2921 2942 removed.append(f)
2922 2943
2923 2944 return scmutil.status(modified, added, removed, [], [], [], [])
2924 2945
2925 2946
2926 2947 class arbitraryfilectx(object):
2927 2948 """Allows you to use filectx-like functions on a file in an arbitrary
2928 2949 location on disk, possibly not in the working directory.
2929 2950 """
2930 2951
2931 2952 def __init__(self, path, repo=None):
2932 2953 # Repo is optional because contrib/simplemerge uses this class.
2933 2954 self._repo = repo
2934 2955 self._path = path
2935 2956
2936 2957 def cmp(self, fctx):
2937 2958 # filecmp follows symlinks whereas `cmp` should not, so skip the fast
2938 2959 # path if either side is a symlink.
2939 2960 symlinks = b'l' in self.flags() or b'l' in fctx.flags()
2940 2961 if not symlinks and isinstance(fctx, workingfilectx) and self._repo:
2941 2962 # Add a fast-path for merge if both sides are disk-backed.
2942 2963 # Note that filecmp uses the opposite return values (True if same)
2943 2964 # from our cmp functions (True if different).
2944 2965 return not filecmp.cmp(self.path(), self._repo.wjoin(fctx.path()))
2945 2966 return self.data() != fctx.data()
2946 2967
2947 2968 def path(self):
2948 2969 return self._path
2949 2970
2950 2971 def flags(self):
2951 2972 return b''
2952 2973
2953 2974 def data(self):
2954 2975 return util.readfile(self._path)
2955 2976
2956 2977 def decodeddata(self):
2957 2978 with open(self._path, b"rb") as f:
2958 2979 return f.read()
2959 2980
2960 2981 def remove(self):
2961 2982 util.unlink(self._path)
2962 2983
2963 2984 def write(self, data, flags, **kwargs):
2964 2985 assert not flags
2965 2986 with open(self._path, b"wb") as f:
2966 2987 f.write(data)
@@ -1,879 +1,881 b''
1 1 # copies.py - copy detection for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import heapq
12 12 import os
13 13
14 14 from .i18n import _
15 15
16 16 from . import (
17 17 match as matchmod,
18 18 node,
19 19 pathutil,
20 20 pycompat,
21 21 util,
22 22 )
23 23 from .utils import stringutil
24 24
25 25
26 26 def _findlimit(repo, ctxa, ctxb):
27 27 """
28 28 Find the last revision that needs to be checked to ensure that a full
29 29 transitive closure for file copies can be properly calculated.
30 30 Generally, this means finding the earliest revision number that's an
31 31 ancestor of a or b but not both, except when a or b is a direct descendent
32 32 of the other, in which case we can return the minimum revnum of a and b.
33 33 """
34 34
35 35 # basic idea:
36 36 # - mark a and b with different sides
37 37 # - if a parent's children are all on the same side, the parent is
38 38 # on that side, otherwise it is on no side
39 39 # - walk the graph in topological order with the help of a heap;
40 40 # - add unseen parents to side map
41 41 # - clear side of any parent that has children on different sides
42 42 # - track number of interesting revs that might still be on a side
43 43 # - track the lowest interesting rev seen
44 44 # - quit when interesting revs is zero
45 45
46 46 cl = repo.changelog
47 47 wdirparents = None
48 48 a = ctxa.rev()
49 49 b = ctxb.rev()
50 50 if a is None:
51 51 wdirparents = (ctxa.p1(), ctxa.p2())
52 52 a = node.wdirrev
53 53 if b is None:
54 54 assert not wdirparents
55 55 wdirparents = (ctxb.p1(), ctxb.p2())
56 56 b = node.wdirrev
57 57
58 58 side = {a: -1, b: 1}
59 59 visit = [-a, -b]
60 60 heapq.heapify(visit)
61 61 interesting = len(visit)
62 62 limit = node.wdirrev
63 63
64 64 while interesting:
65 65 r = -(heapq.heappop(visit))
66 66 if r == node.wdirrev:
67 67 parents = [pctx.rev() for pctx in wdirparents]
68 68 else:
69 69 parents = cl.parentrevs(r)
70 70 if parents[1] == node.nullrev:
71 71 parents = parents[:1]
72 72 for p in parents:
73 73 if p not in side:
74 74 # first time we see p; add it to visit
75 75 side[p] = side[r]
76 76 if side[p]:
77 77 interesting += 1
78 78 heapq.heappush(visit, -p)
79 79 elif side[p] and side[p] != side[r]:
80 80 # p was interesting but now we know better
81 81 side[p] = 0
82 82 interesting -= 1
83 83 if side[r]:
84 84 limit = r # lowest rev visited
85 85 interesting -= 1
86 86
87 87 # Consider the following flow (see test-commit-amend.t under issue4405):
88 88 # 1/ File 'a0' committed
89 89 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
90 90 # 3/ Move back to first commit
91 91 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
92 92 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
93 93 #
94 94 # During the amend in step five, we will be in this state:
95 95 #
96 96 # @ 3 temporary amend commit for a1-amend
97 97 # |
98 98 # o 2 a1-amend
99 99 # |
100 100 # | o 1 a1
101 101 # |/
102 102 # o 0 a0
103 103 #
104 104 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
105 105 # yet the filelog has the copy information in rev 1 and we will not look
106 106 # back far enough unless we also look at the a and b as candidates.
107 107 # This only occurs when a is a descendent of b or visa-versa.
108 108 return min(limit, a, b)
109 109
110 110
111 111 def _filter(src, dst, t):
112 112 """filters out invalid copies after chaining"""
113 113
114 114 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
115 115 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
116 116 # in the following table (not including trivial cases). For example, case 2
117 117 # is where a file existed in 'src' and remained under that name in 'mid' and
118 118 # then was renamed between 'mid' and 'dst'.
119 119 #
120 120 # case src mid dst result
121 121 # 1 x y - -
122 122 # 2 x y y x->y
123 123 # 3 x y x -
124 124 # 4 x y z x->z
125 125 # 5 - x y -
126 126 # 6 x x y x->y
127 127 #
128 128 # _chain() takes care of chaining the copies in 'a' and 'b', but it
129 129 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
130 130 # between 5 and 6, so it includes all cases in its result.
131 131 # Cases 1, 3, and 5 are then removed by _filter().
132 132
133 133 for k, v in list(t.items()):
134 134 # remove copies from files that didn't exist
135 135 if v not in src:
136 136 del t[k]
137 137 # remove criss-crossed copies
138 138 elif k in src and v in dst:
139 139 del t[k]
140 140 # remove copies to files that were then removed
141 141 elif k not in dst:
142 142 del t[k]
143 143
144 144
145 145 def _chain(a, b):
146 146 """chain two sets of copies 'a' and 'b'"""
147 147 t = a.copy()
148 148 for k, v in pycompat.iteritems(b):
149 149 if v in t:
150 150 t[k] = t[v]
151 151 else:
152 152 t[k] = v
153 153 return t
154 154
155 155
156 156 def _tracefile(fctx, am, basemf, limit):
157 157 """return file context that is the ancestor of fctx present in ancestor
158 158 manifest am, stopping after the first ancestor lower than limit"""
159 159
160 160 for f in fctx.ancestors():
161 161 path = f.path()
162 162 if am.get(path, None) == f.filenode():
163 163 return path
164 164 if basemf and basemf.get(path, None) == f.filenode():
165 165 return path
166 166 if not f.isintroducedafter(limit):
167 167 return None
168 168
169 169
170 170 def _dirstatecopies(repo, match=None):
171 171 ds = repo.dirstate
172 172 c = ds.copies().copy()
173 173 for k in list(c):
174 174 if ds[k] not in b'anm' or (match and not match(k)):
175 175 del c[k]
176 176 return c
177 177
178 178
179 179 def _computeforwardmissing(a, b, match=None):
180 180 """Computes which files are in b but not a.
181 181 This is its own function so extensions can easily wrap this call to see what
182 182 files _forwardcopies is about to process.
183 183 """
184 184 ma = a.manifest()
185 185 mb = b.manifest()
186 186 return mb.filesnotin(ma, match=match)
187 187
188 188
189 189 def usechangesetcentricalgo(repo):
190 190 """Checks if we should use changeset-centric copy algorithms"""
191 if repo.filecopiesmode == b'changeset-sidedata':
192 return True
191 193 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
192 194 changesetsource = (b'changeset-only', b'compatibility')
193 195 return readfrom in changesetsource
194 196
195 197
196 198 def _committedforwardcopies(a, b, base, match):
197 199 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
198 200 # files might have to be traced back to the fctx parent of the last
199 201 # one-side-only changeset, but not further back than that
200 202 repo = a._repo
201 203
202 204 if usechangesetcentricalgo(repo):
203 205 return _changesetforwardcopies(a, b, match)
204 206
205 207 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
206 208 dbg = repo.ui.debug
207 209 if debug:
208 210 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
209 211 limit = _findlimit(repo, a, b)
210 212 if debug:
211 213 dbg(b'debug.copies: search limit: %d\n' % limit)
212 214 am = a.manifest()
213 215 basemf = None if base is None else base.manifest()
214 216
215 217 # find where new files came from
216 218 # we currently don't try to find where old files went, too expensive
217 219 # this means we can miss a case like 'hg rm b; hg cp a b'
218 220 cm = {}
219 221
220 222 # Computing the forward missing is quite expensive on large manifests, since
221 223 # it compares the entire manifests. We can optimize it in the common use
222 224 # case of computing what copies are in a commit versus its parent (like
223 225 # during a rebase or histedit). Note, we exclude merge commits from this
224 226 # optimization, since the ctx.files() for a merge commit is not correct for
225 227 # this comparison.
226 228 forwardmissingmatch = match
227 229 if b.p1() == a and b.p2().node() == node.nullid:
228 230 filesmatcher = matchmod.exact(b.files())
229 231 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
230 232 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
231 233
232 234 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
233 235
234 236 if debug:
235 237 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
236 238
237 239 for f in sorted(missing):
238 240 if debug:
239 241 dbg(b'debug.copies: tracing file: %s\n' % f)
240 242 fctx = b[f]
241 243 fctx._ancestrycontext = ancestrycontext
242 244
243 245 if debug:
244 246 start = util.timer()
245 247 opath = _tracefile(fctx, am, basemf, limit)
246 248 if opath:
247 249 if debug:
248 250 dbg(b'debug.copies: rename of: %s\n' % opath)
249 251 cm[f] = opath
250 252 if debug:
251 253 dbg(
252 254 b'debug.copies: time: %f seconds\n'
253 255 % (util.timer() - start)
254 256 )
255 257 return cm
256 258
257 259
258 260 def _changesetforwardcopies(a, b, match):
259 261 if a.rev() in (node.nullrev, b.rev()):
260 262 return {}
261 263
262 264 repo = a.repo()
263 265 children = {}
264 266 cl = repo.changelog
265 267 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
266 268 for r in missingrevs:
267 269 for p in cl.parentrevs(r):
268 270 if p == node.nullrev:
269 271 continue
270 272 if p not in children:
271 273 children[p] = [r]
272 274 else:
273 275 children[p].append(r)
274 276
275 277 roots = set(children) - set(missingrevs)
276 278 # 'work' contains 3-tuples of a (revision number, parent number, copies).
277 279 # The parent number is only used for knowing which parent the copies dict
278 280 # came from.
279 281 # NOTE: To reduce costly copying the 'copies' dicts, we reuse the same
280 282 # instance for *one* of the child nodes (the last one). Once an instance
281 283 # has been put on the queue, it is thus no longer safe to modify it.
282 284 # Conversely, it *is* safe to modify an instance popped off the queue.
283 285 work = [(r, 1, {}) for r in roots]
284 286 heapq.heapify(work)
285 287 alwaysmatch = match.always()
286 288 while work:
287 289 r, i1, copies = heapq.heappop(work)
288 290 if work and work[0][0] == r:
289 291 # We are tracing copies from both parents
290 292 r, i2, copies2 = heapq.heappop(work)
291 293 for dst, src in copies2.items():
292 294 # Unlike when copies are stored in the filelog, we consider
293 295 # it a copy even if the destination already existed on the
294 296 # other branch. It's simply too expensive to check if the
295 297 # file existed in the manifest.
296 298 if dst not in copies:
297 299 # If it was copied on the p1 side, leave it as copied from
298 300 # that side, even if it was also copied on the p2 side.
299 301 copies[dst] = copies2[dst]
300 302 if r == b.rev():
301 303 return copies
302 304 for i, c in enumerate(children[r]):
303 305 childctx = repo[c]
304 306 if r == childctx.p1().rev():
305 307 parent = 1
306 308 childcopies = childctx.p1copies()
307 309 else:
308 310 assert r == childctx.p2().rev()
309 311 parent = 2
310 312 childcopies = childctx.p2copies()
311 313 if not alwaysmatch:
312 314 childcopies = {
313 315 dst: src for dst, src in childcopies.items() if match(dst)
314 316 }
315 317 # Copy the dict only if later iterations will also need it
316 318 if i != len(children[r]) - 1:
317 319 newcopies = copies.copy()
318 320 else:
319 321 newcopies = copies
320 322 if childcopies:
321 323 newcopies = _chain(newcopies, childcopies)
322 324 for f in childctx.filesremoved():
323 325 if f in newcopies:
324 326 del newcopies[f]
325 327 heapq.heappush(work, (c, parent, newcopies))
326 328 assert False
327 329
328 330
329 331 def _forwardcopies(a, b, base=None, match=None):
330 332 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
331 333
332 334 if base is None:
333 335 base = a
334 336 match = a.repo().narrowmatch(match)
335 337 # check for working copy
336 338 if b.rev() is None:
337 339 cm = _committedforwardcopies(a, b.p1(), base, match)
338 340 # combine copies from dirstate if necessary
339 341 copies = _chain(cm, _dirstatecopies(b._repo, match))
340 342 else:
341 343 copies = _committedforwardcopies(a, b, base, match)
342 344 return copies
343 345
344 346
345 347 def _backwardrenames(a, b, match):
346 348 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
347 349 return {}
348 350
349 351 # Even though we're not taking copies into account, 1:n rename situations
350 352 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
351 353 # arbitrarily pick one of the renames.
352 354 # We don't want to pass in "match" here, since that would filter
353 355 # the destination by it. Since we're reversing the copies, we want
354 356 # to filter the source instead.
355 357 f = _forwardcopies(b, a)
356 358 r = {}
357 359 for k, v in sorted(pycompat.iteritems(f)):
358 360 if match and not match(v):
359 361 continue
360 362 # remove copies
361 363 if v in a:
362 364 continue
363 365 r[v] = k
364 366 return r
365 367
366 368
367 369 def pathcopies(x, y, match=None):
368 370 """find {dst@y: src@x} copy mapping for directed compare"""
369 371 repo = x._repo
370 372 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
371 373 if debug:
372 374 repo.ui.debug(
373 375 b'debug.copies: searching copies from %s to %s\n' % (x, y)
374 376 )
375 377 if x == y or not x or not y:
376 378 return {}
377 379 a = y.ancestor(x)
378 380 if a == x:
379 381 if debug:
380 382 repo.ui.debug(b'debug.copies: search mode: forward\n')
381 383 if y.rev() is None and x == y.p1():
382 384 # short-circuit to avoid issues with merge states
383 385 return _dirstatecopies(repo, match)
384 386 copies = _forwardcopies(x, y, match=match)
385 387 elif a == y:
386 388 if debug:
387 389 repo.ui.debug(b'debug.copies: search mode: backward\n')
388 390 copies = _backwardrenames(x, y, match=match)
389 391 else:
390 392 if debug:
391 393 repo.ui.debug(b'debug.copies: search mode: combined\n')
392 394 base = None
393 395 if a.rev() != node.nullrev:
394 396 base = x
395 397 copies = _chain(
396 398 _backwardrenames(x, a, match=match),
397 399 _forwardcopies(a, y, base, match=match),
398 400 )
399 401 _filter(x, y, copies)
400 402 return copies
401 403
402 404
403 405 def mergecopies(repo, c1, c2, base):
404 406 """
405 407 Finds moves and copies between context c1 and c2 that are relevant for
406 408 merging. 'base' will be used as the merge base.
407 409
408 410 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
409 411 files that were moved/ copied in one merge parent and modified in another.
410 412 For example:
411 413
412 414 o ---> 4 another commit
413 415 |
414 416 | o ---> 3 commit that modifies a.txt
415 417 | /
416 418 o / ---> 2 commit that moves a.txt to b.txt
417 419 |/
418 420 o ---> 1 merge base
419 421
420 422 If we try to rebase revision 3 on revision 4, since there is no a.txt in
421 423 revision 4, and if user have copytrace disabled, we prints the following
422 424 message:
423 425
424 426 ```other changed <file> which local deleted```
425 427
426 428 Returns five dicts: "copy", "movewithdir", "diverge", "renamedelete" and
427 429 "dirmove".
428 430
429 431 "copy" is a mapping from destination name -> source name,
430 432 where source is in c1 and destination is in c2 or vice-versa.
431 433
432 434 "movewithdir" is a mapping from source name -> destination name,
433 435 where the file at source present in one context but not the other
434 436 needs to be moved to destination by the merge process, because the
435 437 other context moved the directory it is in.
436 438
437 439 "diverge" is a mapping of source name -> list of destination names
438 440 for divergent renames.
439 441
440 442 "renamedelete" is a mapping of source name -> list of destination
441 443 names for files deleted in c1 that were renamed in c2 or vice-versa.
442 444
443 445 "dirmove" is a mapping of detected source dir -> destination dir renames.
444 446 This is needed for handling changes to new files previously grafted into
445 447 renamed directories.
446 448
447 449 This function calls different copytracing algorithms based on config.
448 450 """
449 451 # avoid silly behavior for update from empty dir
450 452 if not c1 or not c2 or c1 == c2:
451 453 return {}, {}, {}, {}, {}
452 454
453 455 narrowmatch = c1.repo().narrowmatch()
454 456
455 457 # avoid silly behavior for parent -> working dir
456 458 if c2.node() is None and c1.node() == repo.dirstate.p1():
457 459 return _dirstatecopies(repo, narrowmatch), {}, {}, {}, {}
458 460
459 461 copytracing = repo.ui.config(b'experimental', b'copytrace')
460 462 if stringutil.parsebool(copytracing) is False:
461 463 # stringutil.parsebool() returns None when it is unable to parse the
462 464 # value, so we should rely on making sure copytracing is on such cases
463 465 return {}, {}, {}, {}, {}
464 466
465 467 if usechangesetcentricalgo(repo):
466 468 # The heuristics don't make sense when we need changeset-centric algos
467 469 return _fullcopytracing(repo, c1, c2, base)
468 470
469 471 # Copy trace disabling is explicitly below the node == p1 logic above
470 472 # because the logic above is required for a simple copy to be kept across a
471 473 # rebase.
472 474 if copytracing == b'heuristics':
473 475 # Do full copytracing if only non-public revisions are involved as
474 476 # that will be fast enough and will also cover the copies which could
475 477 # be missed by heuristics
476 478 if _isfullcopytraceable(repo, c1, base):
477 479 return _fullcopytracing(repo, c1, c2, base)
478 480 return _heuristicscopytracing(repo, c1, c2, base)
479 481 else:
480 482 return _fullcopytracing(repo, c1, c2, base)
481 483
482 484
483 485 def _isfullcopytraceable(repo, c1, base):
484 486 """ Checks that if base, source and destination are all no-public branches,
485 487 if yes let's use the full copytrace algorithm for increased capabilities
486 488 since it will be fast enough.
487 489
488 490 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
489 491 number of changesets from c1 to base such that if number of changesets are
490 492 more than the limit, full copytracing algorithm won't be used.
491 493 """
492 494 if c1.rev() is None:
493 495 c1 = c1.p1()
494 496 if c1.mutable() and base.mutable():
495 497 sourcecommitlimit = repo.ui.configint(
496 498 b'experimental', b'copytrace.sourcecommitlimit'
497 499 )
498 500 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
499 501 return commits < sourcecommitlimit
500 502 return False
501 503
502 504
503 505 def _checksinglesidecopies(
504 506 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
505 507 ):
506 508 if src not in m2:
507 509 # deleted on side 2
508 510 if src not in m1:
509 511 # renamed on side 1, deleted on side 2
510 512 renamedelete[src] = dsts1
511 513 elif m2[src] != mb[src]:
512 514 if not _related(c2[src], base[src]):
513 515 return
514 516 # modified on side 2
515 517 for dst in dsts1:
516 518 if dst not in m2:
517 519 # dst not added on side 2 (handle as regular
518 520 # "both created" case in manifestmerge otherwise)
519 521 copy[dst] = src
520 522
521 523
522 524 def _fullcopytracing(repo, c1, c2, base):
523 525 """ The full copytracing algorithm which finds all the new files that were
524 526 added from merge base up to the top commit and for each file it checks if
525 527 this file was copied from another file.
526 528
527 529 This is pretty slow when a lot of changesets are involved but will track all
528 530 the copies.
529 531 """
530 532 m1 = c1.manifest()
531 533 m2 = c2.manifest()
532 534 mb = base.manifest()
533 535
534 536 copies1 = pathcopies(base, c1)
535 537 copies2 = pathcopies(base, c2)
536 538
537 539 inversecopies1 = {}
538 540 inversecopies2 = {}
539 541 for dst, src in copies1.items():
540 542 inversecopies1.setdefault(src, []).append(dst)
541 543 for dst, src in copies2.items():
542 544 inversecopies2.setdefault(src, []).append(dst)
543 545
544 546 copy = {}
545 547 diverge = {}
546 548 renamedelete = {}
547 549 allsources = set(inversecopies1) | set(inversecopies2)
548 550 for src in allsources:
549 551 dsts1 = inversecopies1.get(src)
550 552 dsts2 = inversecopies2.get(src)
551 553 if dsts1 and dsts2:
552 554 # copied/renamed on both sides
553 555 if src not in m1 and src not in m2:
554 556 # renamed on both sides
555 557 dsts1 = set(dsts1)
556 558 dsts2 = set(dsts2)
557 559 # If there's some overlap in the rename destinations, we
558 560 # consider it not divergent. For example, if side 1 copies 'a'
559 561 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
560 562 # and 'd' and deletes 'a'.
561 563 if dsts1 & dsts2:
562 564 for dst in dsts1 & dsts2:
563 565 copy[dst] = src
564 566 else:
565 567 diverge[src] = sorted(dsts1 | dsts2)
566 568 elif src in m1 and src in m2:
567 569 # copied on both sides
568 570 dsts1 = set(dsts1)
569 571 dsts2 = set(dsts2)
570 572 for dst in dsts1 & dsts2:
571 573 copy[dst] = src
572 574 # TODO: Handle cases where it was renamed on one side and copied
573 575 # on the other side
574 576 elif dsts1:
575 577 # copied/renamed only on side 1
576 578 _checksinglesidecopies(
577 579 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
578 580 )
579 581 elif dsts2:
580 582 # copied/renamed only on side 2
581 583 _checksinglesidecopies(
582 584 src, dsts2, m2, m1, mb, c1, base, copy, renamedelete
583 585 )
584 586
585 587 renamedeleteset = set()
586 588 divergeset = set()
587 589 for dsts in diverge.values():
588 590 divergeset.update(dsts)
589 591 for dsts in renamedelete.values():
590 592 renamedeleteset.update(dsts)
591 593
592 594 # find interesting file sets from manifests
593 595 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
594 596 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
595 597 u1 = sorted(addedinm1 - addedinm2)
596 598 u2 = sorted(addedinm2 - addedinm1)
597 599
598 600 header = b" unmatched files in %s"
599 601 if u1:
600 602 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1)))
601 603 if u2:
602 604 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2)))
603 605
604 606 fullcopy = copies1.copy()
605 607 fullcopy.update(copies2)
606 608 if not fullcopy:
607 609 return copy, {}, diverge, renamedelete, {}
608 610
609 611 if repo.ui.debugflag:
610 612 repo.ui.debug(
611 613 b" all copies found (* = to merge, ! = divergent, "
612 614 b"% = renamed and deleted):\n"
613 615 )
614 616 for f in sorted(fullcopy):
615 617 note = b""
616 618 if f in copy:
617 619 note += b"*"
618 620 if f in divergeset:
619 621 note += b"!"
620 622 if f in renamedeleteset:
621 623 note += b"%"
622 624 repo.ui.debug(
623 625 b" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f, note)
624 626 )
625 627 del divergeset
626 628
627 629 repo.ui.debug(b" checking for directory renames\n")
628 630
629 631 # generate a directory move map
630 632 d1, d2 = c1.dirs(), c2.dirs()
631 633 invalid = set()
632 634 dirmove = {}
633 635
634 636 # examine each file copy for a potential directory move, which is
635 637 # when all the files in a directory are moved to a new directory
636 638 for dst, src in pycompat.iteritems(fullcopy):
637 639 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
638 640 if dsrc in invalid:
639 641 # already seen to be uninteresting
640 642 continue
641 643 elif dsrc in d1 and ddst in d1:
642 644 # directory wasn't entirely moved locally
643 645 invalid.add(dsrc)
644 646 elif dsrc in d2 and ddst in d2:
645 647 # directory wasn't entirely moved remotely
646 648 invalid.add(dsrc)
647 649 elif dsrc in dirmove and dirmove[dsrc] != ddst:
648 650 # files from the same directory moved to two different places
649 651 invalid.add(dsrc)
650 652 else:
651 653 # looks good so far
652 654 dirmove[dsrc] = ddst
653 655
654 656 for i in invalid:
655 657 if i in dirmove:
656 658 del dirmove[i]
657 659 del d1, d2, invalid
658 660
659 661 if not dirmove:
660 662 return copy, {}, diverge, renamedelete, {}
661 663
662 664 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
663 665
664 666 for d in dirmove:
665 667 repo.ui.debug(
666 668 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
667 669 )
668 670
669 671 movewithdir = {}
670 672 # check unaccounted nonoverlapping files against directory moves
671 673 for f in u1 + u2:
672 674 if f not in fullcopy:
673 675 for d in dirmove:
674 676 if f.startswith(d):
675 677 # new file added in a directory that was moved, move it
676 678 df = dirmove[d] + f[len(d) :]
677 679 if df not in copy:
678 680 movewithdir[f] = df
679 681 repo.ui.debug(
680 682 b" pending file src: '%s' -> dst: '%s'\n"
681 683 % (f, df)
682 684 )
683 685 break
684 686
685 687 return copy, movewithdir, diverge, renamedelete, dirmove
686 688
687 689
688 690 def _heuristicscopytracing(repo, c1, c2, base):
689 691 """ Fast copytracing using filename heuristics
690 692
691 693 Assumes that moves or renames are of following two types:
692 694
693 695 1) Inside a directory only (same directory name but different filenames)
694 696 2) Move from one directory to another
695 697 (same filenames but different directory names)
696 698
697 699 Works only when there are no merge commits in the "source branch".
698 700 Source branch is commits from base up to c2 not including base.
699 701
700 702 If merge is involved it fallbacks to _fullcopytracing().
701 703
702 704 Can be used by setting the following config:
703 705
704 706 [experimental]
705 707 copytrace = heuristics
706 708
707 709 In some cases the copy/move candidates found by heuristics can be very large
708 710 in number and that will make the algorithm slow. The number of possible
709 711 candidates to check can be limited by using the config
710 712 `experimental.copytrace.movecandidateslimit` which defaults to 100.
711 713 """
712 714
713 715 if c1.rev() is None:
714 716 c1 = c1.p1()
715 717 if c2.rev() is None:
716 718 c2 = c2.p1()
717 719
718 720 copies = {}
719 721
720 722 changedfiles = set()
721 723 m1 = c1.manifest()
722 724 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
723 725 # If base is not in c2 branch, we switch to fullcopytracing
724 726 repo.ui.debug(
725 727 b"switching to full copytracing as base is not "
726 728 b"an ancestor of c2\n"
727 729 )
728 730 return _fullcopytracing(repo, c1, c2, base)
729 731
730 732 ctx = c2
731 733 while ctx != base:
732 734 if len(ctx.parents()) == 2:
733 735 # To keep things simple let's not handle merges
734 736 repo.ui.debug(b"switching to full copytracing because of merges\n")
735 737 return _fullcopytracing(repo, c1, c2, base)
736 738 changedfiles.update(ctx.files())
737 739 ctx = ctx.p1()
738 740
739 741 cp = _forwardcopies(base, c2)
740 742 for dst, src in pycompat.iteritems(cp):
741 743 if src in m1:
742 744 copies[dst] = src
743 745
744 746 # file is missing if it isn't present in the destination, but is present in
745 747 # the base and present in the source.
746 748 # Presence in the base is important to exclude added files, presence in the
747 749 # source is important to exclude removed files.
748 750 filt = lambda f: f not in m1 and f in base and f in c2
749 751 missingfiles = [f for f in changedfiles if filt(f)]
750 752
751 753 if missingfiles:
752 754 basenametofilename = collections.defaultdict(list)
753 755 dirnametofilename = collections.defaultdict(list)
754 756
755 757 for f in m1.filesnotin(base.manifest()):
756 758 basename = os.path.basename(f)
757 759 dirname = os.path.dirname(f)
758 760 basenametofilename[basename].append(f)
759 761 dirnametofilename[dirname].append(f)
760 762
761 763 for f in missingfiles:
762 764 basename = os.path.basename(f)
763 765 dirname = os.path.dirname(f)
764 766 samebasename = basenametofilename[basename]
765 767 samedirname = dirnametofilename[dirname]
766 768 movecandidates = samebasename + samedirname
767 769 # f is guaranteed to be present in c2, that's why
768 770 # c2.filectx(f) won't fail
769 771 f2 = c2.filectx(f)
770 772 # we can have a lot of candidates which can slow down the heuristics
771 773 # config value to limit the number of candidates moves to check
772 774 maxcandidates = repo.ui.configint(
773 775 b'experimental', b'copytrace.movecandidateslimit'
774 776 )
775 777
776 778 if len(movecandidates) > maxcandidates:
777 779 repo.ui.status(
778 780 _(
779 781 b"skipping copytracing for '%s', more "
780 782 b"candidates than the limit: %d\n"
781 783 )
782 784 % (f, len(movecandidates))
783 785 )
784 786 continue
785 787
786 788 for candidate in movecandidates:
787 789 f1 = c1.filectx(candidate)
788 790 if _related(f1, f2):
789 791 # if there are a few related copies then we'll merge
790 792 # changes into all of them. This matches the behaviour
791 793 # of upstream copytracing
792 794 copies[candidate] = f
793 795
794 796 return copies, {}, {}, {}, {}
795 797
796 798
797 799 def _related(f1, f2):
798 800 """return True if f1 and f2 filectx have a common ancestor
799 801
800 802 Walk back to common ancestor to see if the two files originate
801 803 from the same file. Since workingfilectx's rev() is None it messes
802 804 up the integer comparison logic, hence the pre-step check for
803 805 None (f1 and f2 can only be workingfilectx's initially).
804 806 """
805 807
806 808 if f1 == f2:
807 809 return True # a match
808 810
809 811 g1, g2 = f1.ancestors(), f2.ancestors()
810 812 try:
811 813 f1r, f2r = f1.linkrev(), f2.linkrev()
812 814
813 815 if f1r is None:
814 816 f1 = next(g1)
815 817 if f2r is None:
816 818 f2 = next(g2)
817 819
818 820 while True:
819 821 f1r, f2r = f1.linkrev(), f2.linkrev()
820 822 if f1r > f2r:
821 823 f1 = next(g1)
822 824 elif f2r > f1r:
823 825 f2 = next(g2)
824 826 else: # f1 and f2 point to files in the same linkrev
825 827 return f1 == f2 # true if they point to the same file
826 828 except StopIteration:
827 829 return False
828 830
829 831
830 832 def duplicatecopies(repo, wctx, rev, fromrev, skiprev=None):
831 833 """reproduce copies from fromrev to rev in the dirstate
832 834
833 835 If skiprev is specified, it's a revision that should be used to
834 836 filter copy records. Any copies that occur between fromrev and
835 837 skiprev will not be duplicated, even if they appear in the set of
836 838 copies between fromrev and rev.
837 839 """
838 840 exclude = {}
839 841 ctraceconfig = repo.ui.config(b'experimental', b'copytrace')
840 842 bctrace = stringutil.parsebool(ctraceconfig)
841 843 if skiprev is not None and (
842 844 ctraceconfig == b'heuristics' or bctrace or bctrace is None
843 845 ):
844 846 # copytrace='off' skips this line, but not the entire function because
845 847 # the line below is O(size of the repo) during a rebase, while the rest
846 848 # of the function is much faster (and is required for carrying copy
847 849 # metadata across the rebase anyway).
848 850 exclude = pathcopies(repo[fromrev], repo[skiprev])
849 851 for dst, src in pycompat.iteritems(pathcopies(repo[fromrev], repo[rev])):
850 852 if dst in exclude:
851 853 continue
852 854 if dst in wctx:
853 855 wctx[dst].markcopied(src)
854 856
855 857
856 858 def computechangesetcopies(ctx):
857 859 """return the copies data for a changeset
858 860
859 861 The copies data are returned as a pair of dictionnary (p1copies, p2copies).
860 862
861 863 Each dictionnary are in the form: `{newname: oldname}`
862 864 """
863 865 p1copies = {}
864 866 p2copies = {}
865 867 p1 = ctx.p1()
866 868 p2 = ctx.p2()
867 869 narrowmatch = ctx._repo.narrowmatch()
868 870 for dst in ctx.files():
869 871 if not narrowmatch(dst) or dst not in ctx:
870 872 continue
871 873 copied = ctx[dst].renamed()
872 874 if not copied:
873 875 continue
874 876 src, srcnode = copied
875 877 if src in p1 and p1[src].filenode() == srcnode:
876 878 p1copies[dst] = src
877 879 elif src in p2 and p2[src].filenode() == srcnode:
878 880 p2copies[dst] = src
879 881 return p1copies, p2copies
@@ -1,396 +1,396 b''
1 1 #testcases filelog compatibility changeset sidedata
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > rebase=
6 6 > [alias]
7 7 > l = log -G -T '{rev} {desc}\n{files}\n'
8 8 > EOF
9 9
10 10 #if compatibility
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [experimental]
13 13 > copies.read-from = compatibility
14 14 > EOF
15 15 #endif
16 16
17 17 #if changeset
18 18 $ cat >> $HGRCPATH << EOF
19 19 > [experimental]
20 20 > copies.read-from = changeset-only
21 21 > copies.write-to = changeset-only
22 22 > EOF
23 23 #endif
24 24
25 25 #if sidedata
26 26 $ cat >> $HGRCPATH << EOF
27 27 > [format]
28 28 > exp-use-copies-side-data-changeset = yes
29 29 > EOF
30 30 #endif
31 31
32 32 $ REPONUM=0
33 33 $ newrepo() {
34 34 > cd $TESTTMP
35 35 > REPONUM=`expr $REPONUM + 1`
36 36 > hg init repo-$REPONUM
37 37 > cd repo-$REPONUM
38 38 > }
39 39
40 40 Copy a file, then delete destination, then copy again. This does not create a new filelog entry.
41 41 $ newrepo
42 42 $ echo x > x
43 43 $ hg ci -Aqm 'add x'
44 44 $ echo x2 > x
45 45 $ hg ci -m 'modify x'
46 46 $ hg co -q 0
47 47 $ hg cp x y
48 48 $ hg ci -qm 'copy x to y'
49 49 $ hg rm y
50 50 $ hg ci -m 'remove y'
51 51 $ hg cp -f x y
52 52 $ hg ci -m 'copy x onto y (again)'
53 53 $ hg l
54 54 @ 4 copy x onto y (again)
55 55 | y
56 56 o 3 remove y
57 57 | y
58 58 o 2 copy x to y
59 59 | y
60 60 | o 1 modify x
61 61 |/ x
62 62 o 0 add x
63 63 x
64 64 $ hg debugp1copies -r 4
65 65 x -> y
66 66 $ hg debugpathcopies 0 4
67 67 x -> y
68 68 $ hg graft -r 1
69 69 grafting 1:* "modify x" (glob)
70 70 merging y and x to y
71 71 $ hg co -qC 1
72 72 $ hg graft -r 4
73 73 grafting 4:* "copy x onto y (again)" (glob)
74 74 merging x and y to y
75 75
76 76 Copy x to y, then remove y, then add back y. With copy metadata in the
77 77 changeset, this could easily end up reporting y as copied from x (if we don't
78 78 unmark it as a copy when it's removed). Despite x and y not being related, we
79 79 want grafts to propagate across the rename.
80 80 $ newrepo
81 81 $ echo x > x
82 82 $ hg ci -Aqm 'add x'
83 83 $ echo x2 > x
84 84 $ hg ci -m 'modify x'
85 85 $ hg co -q 0
86 86 $ hg mv x y
87 87 $ hg ci -qm 'rename x to y'
88 88 $ hg rm y
89 89 $ hg ci -qm 'remove y'
90 90 $ echo x > y
91 91 $ hg ci -Aqm 'add back y'
92 92 $ hg l
93 93 @ 4 add back y
94 94 | y
95 95 o 3 remove y
96 96 | y
97 97 o 2 rename x to y
98 98 | x y
99 99 | o 1 modify x
100 100 |/ x
101 101 o 0 add x
102 102 x
103 103 $ hg debugpathcopies 0 4
104 104 BROKEN: This should succeed and merge the changes from x into y
105 105 $ hg graft -r 1
106 106 grafting 1:* "modify x" (glob)
107 107 file 'x' was deleted in local [local] but was modified in other [graft].
108 108 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
109 109 What do you want to do? u
110 110 abort: unresolved conflicts, can't continue
111 111 (use 'hg resolve' and 'hg graft --continue')
112 112 [255]
113 113
114 114 Add x, remove it, then add it back, then rename x to y. Similar to the case
115 115 above, but here the break in history is before the rename.
116 116 $ newrepo
117 117 $ echo x > x
118 118 $ hg ci -Aqm 'add x'
119 119 $ echo x2 > x
120 120 $ hg ci -m 'modify x'
121 121 $ hg co -q 0
122 122 $ hg rm x
123 123 $ hg ci -qm 'remove x'
124 124 $ echo x > x
125 125 $ hg ci -Aqm 'add x again'
126 126 $ hg mv x y
127 127 $ hg ci -m 'rename x to y'
128 128 $ hg l
129 129 @ 4 rename x to y
130 130 | x y
131 131 o 3 add x again
132 132 | x
133 133 o 2 remove x
134 134 | x
135 135 | o 1 modify x
136 136 |/ x
137 137 o 0 add x
138 138 x
139 139 $ hg debugpathcopies 0 4
140 140 x -> y
141 141 $ hg graft -r 1
142 142 grafting 1:* "modify x" (glob)
143 143 merging y and x to y
144 144 $ hg co -qC 1
145 145 $ hg graft -r 4
146 146 grafting 4:* "rename x to y" (glob)
147 147 merging x and y to y
148 148
149 149 Add x, modify it, remove it, then add it back, then rename x to y. Similar to
150 150 the case above, but here the re-added file's nodeid is different from before
151 151 the break.
152 152
153 153 $ newrepo
154 154 $ echo x > x
155 155 $ hg ci -Aqm 'add x'
156 156 $ echo x2 > x
157 157 $ hg ci -m 'modify x'
158 158 $ echo x3 > x
159 159 $ hg ci -qm 'modify x again'
160 160 $ hg co -q 1
161 161 $ hg rm x
162 162 $ hg ci -qm 'remove x'
163 163 # Same content to avoid conflicts
164 164 $ hg revert -r 1 x
165 165 $ hg ci -Aqm 'add x again'
166 166 $ hg mv x y
167 167 $ hg ci -m 'rename x to y'
168 168 $ hg l
169 169 @ 5 rename x to y
170 170 | x y
171 171 o 4 add x again
172 172 | x
173 173 o 3 remove x
174 174 | x
175 175 | o 2 modify x again
176 176 |/ x
177 177 o 1 modify x
178 178 | x
179 179 o 0 add x
180 180 x
181 181 $ hg debugpathcopies 0 5
182 x -> y (no-filelog no-sidedata !)
183 #if no-filelog no-sidedata
182 x -> y (no-filelog !)
183 #if no-filelog
184 184 $ hg graft -r 2
185 185 grafting 2:* "modify x again" (glob)
186 186 merging y and x to y
187 187 #else
188 188 BROKEN: This should succeed and merge the changes from x into y
189 189 $ hg graft -r 2
190 190 grafting 2:* "modify x again" (glob)
191 191 file 'x' was deleted in local [local] but was modified in other [graft].
192 192 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
193 193 What do you want to do? u
194 194 abort: unresolved conflicts, can't continue
195 195 (use 'hg resolve' and 'hg graft --continue')
196 196 [255]
197 197 #endif
198 198 $ hg co -qC 2
199 199 BROKEN: This should succeed and merge the changes from x into y
200 200 $ hg graft -r 5
201 201 grafting 5:* "rename x to y"* (glob)
202 202 file 'x' was deleted in other [graft] but was modified in local [local].
203 203 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
204 204 What do you want to do? u
205 205 abort: unresolved conflicts, can't continue
206 206 (use 'hg resolve' and 'hg graft --continue')
207 207 [255]
208 208
209 209 Add x, remove it, then add it back, rename x to y from the first commit.
210 210 Similar to the case above, but here the break in history is parallel to the
211 211 rename.
212 212 $ newrepo
213 213 $ echo x > x
214 214 $ hg ci -Aqm 'add x'
215 215 $ hg rm x
216 216 $ hg ci -qm 'remove x'
217 217 $ echo x > x
218 218 $ hg ci -Aqm 'add x again'
219 219 $ echo x2 > x
220 220 $ hg ci -m 'modify x'
221 221 $ hg co -q 0
222 222 $ hg mv x y
223 223 $ hg ci -qm 'rename x to y'
224 224 $ hg l
225 225 @ 4 rename x to y
226 226 | x y
227 227 | o 3 modify x
228 228 | | x
229 229 | o 2 add x again
230 230 | | x
231 231 | o 1 remove x
232 232 |/ x
233 233 o 0 add x
234 234 x
235 235 $ hg debugpathcopies 2 4
236 236 x -> y
237 237 $ hg graft -r 3
238 238 grafting 3:* "modify x" (glob)
239 239 merging y and x to y
240 240 $ hg co -qC 3
241 241 $ hg graft -r 4
242 242 grafting 4:* "rename x to y" (glob)
243 243 merging x and y to y
244 244
245 245 Add x, remove it, then add it back, rename x to y from the first commit.
246 246 Similar to the case above, but here the re-added file's nodeid is different
247 247 from the base.
248 248 $ newrepo
249 249 $ echo x > x
250 250 $ hg ci -Aqm 'add x'
251 251 $ hg rm x
252 252 $ hg ci -qm 'remove x'
253 253 $ echo x2 > x
254 254 $ hg ci -Aqm 'add x again with different content'
255 255 $ hg co -q 0
256 256 $ hg mv x y
257 257 $ hg ci -qm 'rename x to y'
258 258 $ hg l
259 259 @ 3 rename x to y
260 260 | x y
261 261 | o 2 add x again with different content
262 262 | | x
263 263 | o 1 remove x
264 264 |/ x
265 265 o 0 add x
266 266 x
267 267 $ hg debugpathcopies 2 3
268 268 x -> y
269 269 BROKEN: This should merge the changes from x into y
270 270 $ hg graft -r 2
271 271 grafting 2:* "add x again with different content" (glob)
272 272 $ hg co -qC 2
273 273 BROKEN: This should succeed and merge the changes from x into y
274 274 $ hg graft -r 3
275 275 grafting 3:* "rename x to y" (glob)
276 276 file 'x' was deleted in other [graft] but was modified in local [local].
277 277 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
278 278 What do you want to do? u
279 279 abort: unresolved conflicts, can't continue
280 280 (use 'hg resolve' and 'hg graft --continue')
281 281 [255]
282 282
283 283 Add x on two branches, then rename x to y on one side. Similar to the case
284 284 above, but here the break in history is via the base commit.
285 285 $ newrepo
286 286 $ echo a > a
287 287 $ hg ci -Aqm 'base'
288 288 $ echo x > x
289 289 $ hg ci -Aqm 'add x'
290 290 $ echo x2 > x
291 291 $ hg ci -m 'modify x'
292 292 $ hg co -q 0
293 293 $ echo x > x
294 294 $ hg ci -Aqm 'add x again'
295 295 $ hg mv x y
296 296 $ hg ci -qm 'rename x to y'
297 297 $ hg l
298 298 @ 4 rename x to y
299 299 | x y
300 300 o 3 add x again
301 301 | x
302 302 | o 2 modify x
303 303 | | x
304 304 | o 1 add x
305 305 |/ x
306 306 o 0 base
307 307 a
308 308 $ hg debugpathcopies 1 4
309 309 x -> y
310 310 $ hg graft -r 2
311 311 grafting 2:* "modify x" (glob)
312 312 merging y and x to y
313 313 $ hg co -qC 2
314 314 $ hg graft -r 4
315 315 grafting 4:* "rename x to y"* (glob)
316 316 merging x and y to y
317 317
318 318 Add x on two branches, with same content but different history, then rename x
319 319 to y on one side. Similar to the case above, here the file's nodeid is
320 320 different between the branches.
321 321 $ newrepo
322 322 $ echo a > a
323 323 $ hg ci -Aqm 'base'
324 324 $ echo x > x
325 325 $ hg ci -Aqm 'add x'
326 326 $ echo x2 > x
327 327 $ hg ci -m 'modify x'
328 328 $ hg co -q 0
329 329 $ touch x
330 330 $ hg ci -Aqm 'add empty x'
331 331 # Same content to avoid conflicts
332 332 $ hg revert -r 1 x
333 333 $ hg ci -m 'modify x to match commit 1'
334 334 $ hg mv x y
335 335 $ hg ci -qm 'rename x to y'
336 336 $ hg l
337 337 @ 5 rename x to y
338 338 | x y
339 339 o 4 modify x to match commit 1
340 340 | x
341 341 o 3 add empty x
342 342 | x
343 343 | o 2 modify x
344 344 | | x
345 345 | o 1 add x
346 346 |/ x
347 347 o 0 base
348 348 a
349 349 $ hg debugpathcopies 1 5
350 x -> y (no-filelog no-sidedata !)
351 #if no-filelog no-sidedata
350 x -> y (no-filelog !)
351 #if no-filelog
352 352 $ hg graft -r 2
353 353 grafting 2:* "modify x" (glob)
354 354 merging y and x to y
355 355 #else
356 356 BROKEN: This should succeed and merge the changes from x into y
357 357 $ hg graft -r 2
358 358 grafting 2:* "modify x" (glob)
359 359 file 'x' was deleted in local [local] but was modified in other [graft].
360 360 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
361 361 What do you want to do? u
362 362 abort: unresolved conflicts, can't continue
363 363 (use 'hg resolve' and 'hg graft --continue')
364 364 [255]
365 365 #endif
366 366 $ hg co -qC 2
367 367 BROKEN: This should succeed and merge the changes from x into y
368 368 $ hg graft -r 5
369 369 grafting 5:* "rename x to y"* (glob)
370 370 file 'x' was deleted in other [graft] but was modified in local [local].
371 371 You can use (c)hanged version, (d)elete, or leave (u)nresolved.
372 372 What do you want to do? u
373 373 abort: unresolved conflicts, can't continue
374 374 (use 'hg resolve' and 'hg graft --continue')
375 375 [255]
376 376
377 377 Copies via null revision (there shouldn't be any)
378 378 $ newrepo
379 379 $ echo x > x
380 380 $ hg ci -Aqm 'add x'
381 381 $ hg cp x y
382 382 $ hg ci -m 'copy x to y'
383 383 $ hg co -q null
384 384 $ echo x > x
385 385 $ hg ci -Aqm 'add x (again)'
386 386 $ hg l
387 387 @ 2 add x (again)
388 388 x
389 389 o 1 copy x to y
390 390 | y
391 391 o 0 add x
392 392 x
393 393 $ hg debugpathcopies 1 2
394 394 $ hg debugpathcopies 2 1
395 395 $ hg graft -r 1
396 396 grafting 1:* "copy x to y" (glob)
@@ -1,607 +1,606 b''
1 1 #testcases filelog compatibility changeset sidedata
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > rebase=
6 6 > [alias]
7 7 > l = log -G -T '{rev} {desc}\n{files}\n'
8 8 > EOF
9 9
10 10 #if compatibility
11 11 $ cat >> $HGRCPATH << EOF
12 12 > [experimental]
13 13 > copies.read-from = compatibility
14 14 > EOF
15 15 #endif
16 16
17 17 #if changeset
18 18 $ cat >> $HGRCPATH << EOF
19 19 > [experimental]
20 20 > copies.read-from = changeset-only
21 21 > copies.write-to = changeset-only
22 22 > EOF
23 23 #endif
24 24
25 25 #if sidedata
26 26 $ cat >> $HGRCPATH << EOF
27 27 > [format]
28 28 > exp-use-copies-side-data-changeset = yes
29 29 > EOF
30 30 #endif
31 31
32 32 $ REPONUM=0
33 33 $ newrepo() {
34 34 > cd $TESTTMP
35 35 > REPONUM=`expr $REPONUM + 1`
36 36 > hg init repo-$REPONUM
37 37 > cd repo-$REPONUM
38 38 > }
39 39
40 40 Simple rename case
41 41 $ newrepo
42 42 $ echo x > x
43 43 $ hg ci -Aqm 'add x'
44 44 $ hg mv x y
45 45 $ hg debugp1copies
46 46 x -> y
47 47 $ hg debugp2copies
48 48 $ hg ci -m 'rename x to y'
49 49 $ hg l
50 50 @ 1 rename x to y
51 51 | x y
52 52 o 0 add x
53 53 x
54 54 $ hg debugp1copies -r 1
55 55 x -> y
56 56 $ hg debugpathcopies 0 1
57 57 x -> y
58 58 $ hg debugpathcopies 1 0
59 59 y -> x
60 60 Test filtering copies by path. We do filtering by destination.
61 61 $ hg debugpathcopies 0 1 x
62 62 $ hg debugpathcopies 1 0 x
63 63 y -> x
64 64 $ hg debugpathcopies 0 1 y
65 65 x -> y
66 66 $ hg debugpathcopies 1 0 y
67 67
68 68 Copies not including commit changes
69 69 $ newrepo
70 70 $ echo x > x
71 71 $ hg ci -Aqm 'add x'
72 72 $ hg mv x y
73 73 $ hg debugpathcopies . .
74 74 $ hg debugpathcopies . 'wdir()'
75 75 x -> y
76 76 $ hg debugpathcopies 'wdir()' .
77 77 y -> x
78 78
79 79 Copy a file onto another file
80 80 $ newrepo
81 81 $ echo x > x
82 82 $ echo y > y
83 83 $ hg ci -Aqm 'add x and y'
84 84 $ hg cp -f x y
85 85 $ hg debugp1copies
86 86 x -> y
87 87 $ hg debugp2copies
88 88 $ hg ci -m 'copy x onto y'
89 89 $ hg l
90 90 @ 1 copy x onto y
91 91 | y
92 92 o 0 add x and y
93 93 x y
94 94 $ hg debugp1copies -r 1
95 95 x -> y
96 96 Incorrectly doesn't show the rename
97 97 $ hg debugpathcopies 0 1
98 98
99 99 Copy a file onto another file with same content. If metadata is stored in changeset, this does not
100 100 produce a new filelog entry. The changeset's "files" entry should still list the file.
101 101 $ newrepo
102 102 $ echo x > x
103 103 $ echo x > x2
104 104 $ hg ci -Aqm 'add x and x2 with same content'
105 105 $ hg cp -f x x2
106 106 $ hg ci -m 'copy x onto x2'
107 107 $ hg l
108 108 @ 1 copy x onto x2
109 109 | x2
110 110 o 0 add x and x2 with same content
111 111 x x2
112 112 $ hg debugp1copies -r 1
113 113 x -> x2
114 114 Incorrectly doesn't show the rename
115 115 $ hg debugpathcopies 0 1
116 116
117 117 Rename file in a loop: x->y->z->x
118 118 $ newrepo
119 119 $ echo x > x
120 120 $ hg ci -Aqm 'add x'
121 121 $ hg mv x y
122 122 $ hg debugp1copies
123 123 x -> y
124 124 $ hg debugp2copies
125 125 $ hg ci -m 'rename x to y'
126 126 $ hg mv y z
127 127 $ hg ci -m 'rename y to z'
128 128 $ hg mv z x
129 129 $ hg ci -m 'rename z to x'
130 130 $ hg l
131 131 @ 3 rename z to x
132 132 | x z
133 133 o 2 rename y to z
134 134 | y z
135 135 o 1 rename x to y
136 136 | x y
137 137 o 0 add x
138 138 x
139 139 $ hg debugpathcopies 0 3
140 140
141 141 Copy x to z, then remove z, then copy x2 (same content as x) to z. With copy metadata in the
142 142 changeset, the two copies here will have the same filelog entry, so ctx['z'].introrev() might point
143 143 to the first commit that added the file. We should still report the copy as being from x2.
144 144 $ newrepo
145 145 $ echo x > x
146 146 $ echo x > x2
147 147 $ hg ci -Aqm 'add x and x2 with same content'
148 148 $ hg cp x z
149 149 $ hg ci -qm 'copy x to z'
150 150 $ hg rm z
151 151 $ hg ci -m 'remove z'
152 152 $ hg cp x2 z
153 153 $ hg ci -m 'copy x2 to z'
154 154 $ hg l
155 155 @ 3 copy x2 to z
156 156 | z
157 157 o 2 remove z
158 158 | z
159 159 o 1 copy x to z
160 160 | z
161 161 o 0 add x and x2 with same content
162 162 x x2
163 163 $ hg debugp1copies -r 3
164 164 x2 -> z
165 165 $ hg debugpathcopies 0 3
166 166 x2 -> z
167 167
168 168 Create x and y, then rename them both to the same name, but on different sides of a fork
169 169 $ newrepo
170 170 $ echo x > x
171 171 $ echo y > y
172 172 $ hg ci -Aqm 'add x and y'
173 173 $ hg mv x z
174 174 $ hg ci -qm 'rename x to z'
175 175 $ hg co -q 0
176 176 $ hg mv y z
177 177 $ hg ci -qm 'rename y to z'
178 178 $ hg l
179 179 @ 2 rename y to z
180 180 | y z
181 181 | o 1 rename x to z
182 182 |/ x z
183 183 o 0 add x and y
184 184 x y
185 185 $ hg debugpathcopies 1 2
186 186 z -> x
187 187 y -> z
188 188
189 189 Fork renames x to y on one side and removes x on the other
190 190 $ newrepo
191 191 $ echo x > x
192 192 $ hg ci -Aqm 'add x'
193 193 $ hg mv x y
194 194 $ hg ci -m 'rename x to y'
195 195 $ hg co -q 0
196 196 $ hg rm x
197 197 $ hg ci -m 'remove x'
198 198 created new head
199 199 $ hg l
200 200 @ 2 remove x
201 201 | x
202 202 | o 1 rename x to y
203 203 |/ x y
204 204 o 0 add x
205 205 x
206 206 $ hg debugpathcopies 1 2
207 207
208 208 Merge rename from other branch
209 209 $ newrepo
210 210 $ echo x > x
211 211 $ hg ci -Aqm 'add x'
212 212 $ hg mv x y
213 213 $ hg ci -m 'rename x to y'
214 214 $ hg co -q 0
215 215 $ echo z > z
216 216 $ hg ci -Aqm 'add z'
217 217 $ hg merge -q 1
218 218 $ hg debugp1copies
219 219 $ hg debugp2copies
220 220 $ hg ci -m 'merge rename from p2'
221 221 $ hg l
222 222 @ 3 merge rename from p2
223 223 |\
224 224 | o 2 add z
225 225 | | z
226 226 o | 1 rename x to y
227 227 |/ x y
228 228 o 0 add x
229 229 x
230 230 Perhaps we should indicate the rename here, but `hg status` is documented to be weird during
231 231 merges, so...
232 232 $ hg debugp1copies -r 3
233 233 $ hg debugp2copies -r 3
234 234 $ hg debugpathcopies 0 3
235 235 x -> y
236 236 $ hg debugpathcopies 1 2
237 237 y -> x
238 238 $ hg debugpathcopies 1 3
239 239 $ hg debugpathcopies 2 3
240 240 x -> y
241 241
242 242 Copy file from either side in a merge
243 243 $ newrepo
244 244 $ echo x > x
245 245 $ hg ci -Aqm 'add x'
246 246 $ hg co -q null
247 247 $ echo y > y
248 248 $ hg ci -Aqm 'add y'
249 249 $ hg merge -q 0
250 250 $ hg cp y z
251 251 $ hg debugp1copies
252 252 y -> z
253 253 $ hg debugp2copies
254 254 $ hg ci -m 'copy file from p1 in merge'
255 255 $ hg co -q 1
256 256 $ hg merge -q 0
257 257 $ hg cp x z
258 258 $ hg debugp1copies
259 259 $ hg debugp2copies
260 260 x -> z
261 261 $ hg ci -qm 'copy file from p2 in merge'
262 262 $ hg l
263 263 @ 3 copy file from p2 in merge
264 264 |\ z
265 265 +---o 2 copy file from p1 in merge
266 266 | |/ z
267 267 | o 1 add y
268 268 | y
269 269 o 0 add x
270 270 x
271 271 $ hg debugp1copies -r 2
272 272 y -> z
273 273 $ hg debugp2copies -r 2
274 274 $ hg debugpathcopies 1 2
275 275 y -> z
276 276 $ hg debugpathcopies 0 2
277 277 $ hg debugp1copies -r 3
278 278 $ hg debugp2copies -r 3
279 279 x -> z
280 280 $ hg debugpathcopies 1 3
281 281 $ hg debugpathcopies 0 3
282 282 x -> z
283 283
284 284 Copy file that exists on both sides of the merge, same content on both sides
285 285 $ newrepo
286 286 $ echo x > x
287 287 $ hg ci -Aqm 'add x on branch 1'
288 288 $ hg co -q null
289 289 $ echo x > x
290 290 $ hg ci -Aqm 'add x on branch 2'
291 291 $ hg merge -q 0
292 292 $ hg cp x z
293 293 $ hg debugp1copies
294 294 x -> z
295 295 $ hg debugp2copies
296 296 $ hg ci -qm 'merge'
297 297 $ hg l
298 298 @ 2 merge
299 299 |\ z
300 300 | o 1 add x on branch 2
301 301 | x
302 302 o 0 add x on branch 1
303 303 x
304 304 $ hg debugp1copies -r 2
305 305 x -> z
306 306 $ hg debugp2copies -r 2
307 307 It's a little weird that it shows up on both sides
308 308 $ hg debugpathcopies 1 2
309 309 x -> z
310 310 $ hg debugpathcopies 0 2
311 311 x -> z (filelog !)
312 x -> z (sidedata !)
313 312
314 313 Copy file that exists on both sides of the merge, different content
315 314 $ newrepo
316 315 $ echo branch1 > x
317 316 $ hg ci -Aqm 'add x on branch 1'
318 317 $ hg co -q null
319 318 $ echo branch2 > x
320 319 $ hg ci -Aqm 'add x on branch 2'
321 320 $ hg merge -q 0
322 321 warning: conflicts while merging x! (edit, then use 'hg resolve --mark')
323 322 [1]
324 323 $ echo resolved > x
325 324 $ hg resolve -m x
326 325 (no more unresolved files)
327 326 $ hg cp x z
328 327 $ hg debugp1copies
329 328 x -> z
330 329 $ hg debugp2copies
331 330 $ hg ci -qm 'merge'
332 331 $ hg l
333 332 @ 2 merge
334 333 |\ x z
335 334 | o 1 add x on branch 2
336 335 | x
337 336 o 0 add x on branch 1
338 337 x
339 338 $ hg debugp1copies -r 2
340 339 x -> z (changeset !)
340 x -> z (sidedata !)
341 341 $ hg debugp2copies -r 2
342 x -> z (no-changeset !)
342 x -> z (no-changeset no-sidedata !)
343 343 $ hg debugpathcopies 1 2
344 344 x -> z (changeset !)
345 x -> z (sidedata !)
345 346 $ hg debugpathcopies 0 2
346 x -> z (no-changeset !)
347 x -> z (no-changeset no-sidedata !)
347 348
348 349 Copy x->y on one side of merge and copy x->z on the other side. Pathcopies from one parent
349 350 of the merge to the merge should include the copy from the other side.
350 351 $ newrepo
351 352 $ echo x > x
352 353 $ hg ci -Aqm 'add x'
353 354 $ hg cp x y
354 355 $ hg ci -qm 'copy x to y'
355 356 $ hg co -q 0
356 357 $ hg cp x z
357 358 $ hg ci -qm 'copy x to z'
358 359 $ hg merge -q 1
359 360 $ hg ci -m 'merge copy x->y and copy x->z'
360 361 $ hg l
361 362 @ 3 merge copy x->y and copy x->z
362 363 |\
363 364 | o 2 copy x to z
364 365 | | z
365 366 o | 1 copy x to y
366 367 |/ y
367 368 o 0 add x
368 369 x
369 370 $ hg debugp1copies -r 3
370 371 $ hg debugp2copies -r 3
371 372 $ hg debugpathcopies 2 3
372 373 x -> y
373 374 $ hg debugpathcopies 1 3
374 375 x -> z
375 376
376 377 Copy x to y on one side of merge, create y and rename to z on the other side.
377 378 $ newrepo
378 379 $ echo x > x
379 380 $ hg ci -Aqm 'add x'
380 381 $ hg cp x y
381 382 $ hg ci -qm 'copy x to y'
382 383 $ hg co -q 0
383 384 $ echo y > y
384 385 $ hg ci -Aqm 'add y'
385 386 $ hg mv y z
386 387 $ hg ci -m 'rename y to z'
387 388 $ hg merge -q 1
388 389 $ hg ci -m 'merge'
389 390 $ hg l
390 391 @ 4 merge
391 392 |\
392 393 | o 3 rename y to z
393 394 | | y z
394 395 | o 2 add y
395 396 | | y
396 397 o | 1 copy x to y
397 398 |/ y
398 399 o 0 add x
399 400 x
400 401 $ hg debugp1copies -r 3
401 402 y -> z
402 403 $ hg debugp2copies -r 3
403 404 $ hg debugpathcopies 2 3
404 405 y -> z
405 406 $ hg debugpathcopies 1 3
406 y -> z (no-filelog no-sidedata !)
407 y -> z (no-filelog !)
407 408
408 409 Create x and y, then rename x to z on one side of merge, and rename y to z and
409 410 modify z on the other side. When storing copies in the changeset, we don't
410 411 filter out copies whose target was created on the other side of the merge.
411 412 $ newrepo
412 413 $ echo x > x
413 414 $ echo y > y
414 415 $ hg ci -Aqm 'add x and y'
415 416 $ hg mv x z
416 417 $ hg ci -qm 'rename x to z'
417 418 $ hg co -q 0
418 419 $ hg mv y z
419 420 $ hg ci -qm 'rename y to z'
420 421 $ echo z >> z
421 422 $ hg ci -m 'modify z'
422 423 $ hg merge -q 1
423 424 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
424 425 [1]
425 426 $ echo z > z
426 427 $ hg resolve -qm z
427 428 $ hg ci -m 'merge 1 into 3'
428 429 Try merging the other direction too
429 430 $ hg co -q 1
430 431 $ hg merge -q 3
431 432 warning: conflicts while merging z! (edit, then use 'hg resolve --mark')
432 433 [1]
433 434 $ echo z > z
434 435 $ hg resolve -qm z
435 436 $ hg ci -m 'merge 3 into 1'
436 437 created new head
437 438 $ hg l
438 439 @ 5 merge 3 into 1
439 440 |\ z
440 441 +---o 4 merge 1 into 3
441 442 | |/ z
442 443 | o 3 modify z
443 444 | | z
444 445 | o 2 rename y to z
445 446 | | y z
446 447 o | 1 rename x to z
447 448 |/ x z
448 449 o 0 add x and y
449 450 x y
450 451 $ hg debugpathcopies 1 4
451 y -> z (no-filelog no-sidedata !)
452 y -> z (no-filelog !)
452 453 $ hg debugpathcopies 2 4
453 x -> z (no-filelog no-sidedata !)
454 x -> z (no-filelog !)
454 455 $ hg debugpathcopies 0 4
455 456 x -> z (filelog !)
456 x -> z (sidedata !)
457 y -> z (compatibility !)
458 y -> z (changeset !)
457 y -> z (no-filelog !)
459 458 $ hg debugpathcopies 1 5
460 y -> z (no-filelog no-sidedata !)
459 y -> z (no-filelog !)
461 460 $ hg debugpathcopies 2 5
462 x -> z (no-filelog no-sidedata !)
461 x -> z (no-filelog !)
463 462 $ hg debugpathcopies 0 5
464 463 x -> z
465 464
466 465
467 466 Test for a case in fullcopytracing algorithm where neither of the merging csets
468 467 is a descendant of the merge base. This test reflects that the algorithm
469 468 correctly finds the copies:
470 469
471 470 $ cat >> $HGRCPATH << EOF
472 471 > [experimental]
473 472 > evolution.createmarkers=True
474 473 > evolution.allowunstable=True
475 474 > EOF
476 475
477 476 $ newrepo
478 477 $ echo a > a
479 478 $ hg add a
480 479 $ hg ci -m "added a"
481 480 $ echo b > b
482 481 $ hg add b
483 482 $ hg ci -m "added b"
484 483
485 484 $ hg mv b b1
486 485 $ hg ci -m "rename b to b1"
487 486
488 487 $ hg up ".^"
489 488 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
490 489 $ echo d > d
491 490 $ hg add d
492 491 $ hg ci -m "added d"
493 492 created new head
494 493
495 494 $ echo baba >> b
496 495 $ hg ci --amend -m "added d, modified b"
497 496
498 497 $ hg l --hidden
499 498 @ 4 added d, modified b
500 499 | b d
501 500 | x 3 added d
502 501 |/ d
503 502 | o 2 rename b to b1
504 503 |/ b b1
505 504 o 1 added b
506 505 | b
507 506 o 0 added a
508 507 a
509 508
510 509 Grafting revision 4 on top of revision 2, showing that it respect the rename:
511 510
512 511 $ hg up 2 -q
513 512 $ hg graft -r 4 --base 3 --hidden
514 513 grafting 4:af28412ec03c "added d, modified b" (tip) (no-changeset !)
515 514 grafting 4:6325ca0b7a1c "added d, modified b" (tip) (changeset !)
516 515 merging b1 and b to b1
517 516
518 517 $ hg l -l1 -p
519 518 @ 5 added d, modified b
520 519 | b1
521 520 ~ diff -r 5a4825cc2926 -r 94a2f1a0e8e2 b1 (no-changeset !)
522 521 ~ diff -r 0a0ed3b3251c -r d544fb655520 b1 (changeset !)
523 522 --- a/b1 Thu Jan 01 00:00:00 1970 +0000
524 523 +++ b/b1 Thu Jan 01 00:00:00 1970 +0000
525 524 @@ -1,1 +1,2 @@
526 525 b
527 526 +baba
528 527
529 528 Test to make sure that fullcopytracing algorithm doesn't fail when neither of the
530 529 merging csets is a descendant of the base.
531 530 -------------------------------------------------------------------------------------------------
532 531
533 532 $ newrepo
534 533 $ echo a > a
535 534 $ hg add a
536 535 $ hg ci -m "added a"
537 536 $ echo b > b
538 537 $ hg add b
539 538 $ hg ci -m "added b"
540 539
541 540 $ echo foobar > willconflict
542 541 $ hg add willconflict
543 542 $ hg ci -m "added willconflict"
544 543 $ echo c > c
545 544 $ hg add c
546 545 $ hg ci -m "added c"
547 546
548 547 $ hg l
549 548 @ 3 added c
550 549 | c
551 550 o 2 added willconflict
552 551 | willconflict
553 552 o 1 added b
554 553 | b
555 554 o 0 added a
556 555 a
557 556
558 557 $ hg up ".^^"
559 558 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
560 559 $ echo d > d
561 560 $ hg add d
562 561 $ hg ci -m "added d"
563 562 created new head
564 563
565 564 $ echo barfoo > willconflict
566 565 $ hg add willconflict
567 566 $ hg ci --amend -m "added willconflict and d"
568 567
569 568 $ hg l
570 569 @ 5 added willconflict and d
571 570 | d willconflict
572 571 | o 3 added c
573 572 | | c
574 573 | o 2 added willconflict
575 574 |/ willconflict
576 575 o 1 added b
577 576 | b
578 577 o 0 added a
579 578 a
580 579
581 580 $ hg rebase -r . -d 2 -t :other
582 581 rebasing 5:5018b1509e94 "added willconflict and d" (tip) (no-changeset !)
583 582 rebasing 5:af8d273bf580 "added willconflict and d" (tip) (changeset !)
584 583
585 584 $ hg up 3 -q
586 585 $ hg l --hidden
587 586 o 6 added willconflict and d
588 587 | d willconflict
589 588 | x 5 added willconflict and d
590 589 | | d willconflict
591 590 | | x 4 added d
592 591 | |/ d
593 592 +---@ 3 added c
594 593 | | c
595 594 o | 2 added willconflict
596 595 |/ willconflict
597 596 o 1 added b
598 597 | b
599 598 o 0 added a
600 599 a
601 600
602 601 Now if we trigger a merge between revision 3 and 6 using base revision 4,
603 602 neither of the merging csets will be a descendant of the base revision:
604 603
605 604 $ hg graft -r 6 --base 4 --hidden -t :other
606 605 grafting 6:99802e4f1e46 "added willconflict and d" (tip) (no-changeset !)
607 606 grafting 6:b19f0df72728 "added willconflict and d" (tip) (changeset !)
General Comments 0
You need to be logged in to leave comments. Login now