comparison mercurial/patch.py @ 32979:66117dae87f9

patch: rewrite reversehunks (issue5337) The old reversehunks code accesses "crecord.uihunk._hunk", which is the raw recordhunk without crecord selection information, therefore "revert -i" cannot revert individual lines, aka. issue5337. The patch rewrites related logic to return the right reverse hunk for revert. Namely, 1. "fromline" and "toline" are correctly swapped [1] 2. crecord.uihunk generates a correct reverse hunk [2] Besides, reversehunks(hunks) will no longer modify its input "hunks", which is more expected. [1]: To explain why "fromline" and "toline" need to be swapped, take the following example: $ cat > a <<EOF > 1 > 2 > 3 > 4 > EOF $ cat > b <<EOF > 2 > 3 > 5 > EOF $ diff a b 1d0 <---- "1" is "fromline" and "0" is "toline" < 1 and they are swapped if diff from the reversed direction 4c3 | < 4 | --- | > 5 | | $ diff b a | 0a1 <---------+ > 1 3c4 <---- also "4c3" gets swapped to "3c4" < 5 --- > 4 [2]: This is a bit tricky. For example, given a file which is empty in working parent but has 3 lines in working copy, and the user selection: select hunk to discard [x] +1 [ ] +2 [x] +3 The user intent is to drop "1" and "3" in working copy but keep "2", so the reverse patch would be something like: -1 2 (2 is a "context line") -3 We cannot just take all selected lines and swap "-" and "+", which will be: -1 -3 That patch won't apply because of "2". So the correct way is to insert "2" as a "context line" by inserting it first then deleting it: -2 +2 Therefore, the correct revert patch is: -1 -2 +2 -3 It could be reordered to look more like a common diff hunk: -1 -2 -3 +2 Note: It's possible to return multiple hunks so there won't be lines like "-2", "+2". But the current implementation is much simpler. For deletions, like the working parent has "1\n2\n3\n" and it was changed to empty in working copy: select hunk to discard [x] -1 [ ] -2 [x] -3 The user intent is to drop the deletion of 1 and 3 (in other words, keep those lines), but still delete "2". The reverse patch is meant to be applied to working copy which is empty. So the patch would be: +1 +3 That is to say, there is no need to special handle the unselected "2" like the above insertion case.
author Jun Wu <quark@fb.com>
date Tue, 20 Jun 2017 23:22:38 -0700
parents 017ad85e5ac8
children 1d5d7e2b7ab5
comparison
equal deleted inserted replaced
32978:41b081ac2145 32979:66117dae87f9
956 def countchanges(self, hunk): 956 def countchanges(self, hunk):
957 """hunk -> (n+,n-)""" 957 """hunk -> (n+,n-)"""
958 add = len([h for h in hunk if h[0] == '+']) 958 add = len([h for h in hunk if h[0] == '+'])
959 rem = len([h for h in hunk if h[0] == '-']) 959 rem = len([h for h in hunk if h[0] == '-'])
960 return add, rem 960 return add, rem
961
962 def reversehunk(self):
963 """return another recordhunk which is the reverse of the hunk
964
965 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
966 that, swap fromline/toline and +/- signs while keep other things
967 unchanged.
968 """
969 m = {'+': '-', '-': '+'}
970 hunk = ['%s%s' % (m[l[0]], l[1:]) for l in self.hunk]
971 return recordhunk(self.header, self.toline, self.fromline, self.proc,
972 self.before, hunk, self.after)
961 973
962 def write(self, fp): 974 def write(self, fp):
963 delta = len(self.before) + len(self.after) 975 delta = len(self.before) + len(self.after)
964 if self.after and self.after[-1] == '\\ No newline at end of file\n': 976 if self.after and self.after[-1] == '\\ No newline at end of file\n':
965 delta -= 1 977 delta -= 1
1491 @@ -1,4 +1,3 @@ 1503 @@ -1,4 +1,3 @@
1492 -firstline 1504 -firstline
1493 c 1505 c
1494 1 1506 1
1495 2 1507 2
1496 @@ -1,6 +2,6 @@ 1508 @@ -2,6 +1,6 @@
1497 c 1509 c
1498 1 1510 1
1499 2 1511 2
1500 - 3 1512 - 3
1501 +4 1513 +4
1502 5 1514 5
1503 d 1515 d
1504 @@ -5,3 +6,2 @@ 1516 @@ -6,3 +5,2 @@
1505 5 1517 5
1506 d 1518 d
1507 -lastline 1519 -lastline
1508 1520
1509 ''' 1521 '''
1510 1522
1511 from . import crecord as crecordmod
1512 newhunks = [] 1523 newhunks = []
1513 for c in hunks: 1524 for c in hunks:
1514 if isinstance(c, crecordmod.uihunk): 1525 if util.safehasattr(c, 'reversehunk'):
1515 # curses hunks encapsulate the record hunk in _hunk 1526 c = c.reversehunk()
1516 c = c._hunk
1517 if isinstance(c, recordhunk):
1518 for j, line in enumerate(c.hunk):
1519 if line.startswith("-"):
1520 c.hunk[j] = "+" + c.hunk[j][1:]
1521 elif line.startswith("+"):
1522 c.hunk[j] = "-" + c.hunk[j][1:]
1523 c.added, c.removed = c.removed, c.added
1524 newhunks.append(c) 1527 newhunks.append(c)
1525 return newhunks 1528 return newhunks
1526 1529
1527 def parsepatch(originalchunks): 1530 def parsepatch(originalchunks):
1528 """patch -> [] of headers -> [] of hunks """ 1531 """patch -> [] of headers -> [] of hunks """