##// END OF EJS Templates
simplemerge: replace `**opts` passed to `simplemerge()` by keyword arguments...
Martin von Zweigbergk -
r49593:9ee70e17 default
parent child Browse files
Show More
@@ -1,118 +1,121 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
2 from __future__ import absolute_import
2 from __future__ import absolute_import
3
3
4 import getopt
4 import getopt
5 import sys
5 import sys
6
6
7 import hgdemandimport
7 import hgdemandimport
8
8
9 hgdemandimport.enable()
9 hgdemandimport.enable()
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial import (
12 from mercurial import (
13 context,
13 context,
14 error,
14 error,
15 fancyopts,
15 fancyopts,
16 pycompat,
17 simplemerge,
16 simplemerge,
18 ui as uimod,
17 ui as uimod,
19 )
18 )
20 from mercurial.utils import procutil, stringutil
19 from mercurial.utils import procutil, stringutil
21
20
22 options = [
21 options = [
23 (b'L', b'label', [], _(b'labels to use on conflict markers')),
22 (b'L', b'label', [], _(b'labels to use on conflict markers')),
24 (b'a', b'text', None, _(b'treat all files as text')),
23 (b'a', b'text', None, _(b'treat all files as text')),
25 (b'p', b'print', None, _(b'print results instead of overwriting LOCAL')),
24 (b'p', b'print', None, _(b'print results instead of overwriting LOCAL')),
26 (b'', b'no-minimal', None, _(b'no effect (DEPRECATED)')),
25 (b'', b'no-minimal', None, _(b'no effect (DEPRECATED)')),
27 (b'h', b'help', None, _(b'display help and exit')),
26 (b'h', b'help', None, _(b'display help and exit')),
28 (b'q', b'quiet', None, _(b'suppress output')),
27 (b'q', b'quiet', None, _(b'suppress output')),
29 ]
28 ]
30
29
31 usage = _(
30 usage = _(
32 b'''simplemerge [OPTS] LOCAL BASE OTHER
31 b'''simplemerge [OPTS] LOCAL BASE OTHER
33
32
34 Simple three-way file merge utility with a minimal feature set.
33 Simple three-way file merge utility with a minimal feature set.
35
34
36 Apply to LOCAL the changes necessary to go from BASE to OTHER.
35 Apply to LOCAL the changes necessary to go from BASE to OTHER.
37
36
38 By default, LOCAL is overwritten with the results of this operation.
37 By default, LOCAL is overwritten with the results of this operation.
39 '''
38 '''
40 )
39 )
41
40
42
41
43 class ParseError(Exception):
42 class ParseError(Exception):
44 """Exception raised on errors in parsing the command line."""
43 """Exception raised on errors in parsing the command line."""
45
44
46
45
47 def showhelp():
46 def showhelp():
48 procutil.stdout.write(usage)
47 procutil.stdout.write(usage)
49 procutil.stdout.write(b'\noptions:\n')
48 procutil.stdout.write(b'\noptions:\n')
50
49
51 out_opts = []
50 out_opts = []
52 for shortopt, longopt, default, desc in options:
51 for shortopt, longopt, default, desc in options:
53 out_opts.append(
52 out_opts.append(
54 (
53 (
55 b'%2s%s'
54 b'%2s%s'
56 % (
55 % (
57 shortopt and b'-%s' % shortopt,
56 shortopt and b'-%s' % shortopt,
58 longopt and b' --%s' % longopt,
57 longopt and b' --%s' % longopt,
59 ),
58 ),
60 b'%s' % desc,
59 b'%s' % desc,
61 )
60 )
62 )
61 )
63 opts_len = max([len(opt[0]) for opt in out_opts])
62 opts_len = max([len(opt[0]) for opt in out_opts])
64 for first, second in out_opts:
63 for first, second in out_opts:
65 procutil.stdout.write(b' %-*s %s\n' % (opts_len, first, second))
64 procutil.stdout.write(b' %-*s %s\n' % (opts_len, first, second))
66
65
67
66
68 try:
67 try:
69 for fp in (sys.stdin, procutil.stdout, sys.stderr):
68 for fp in (sys.stdin, procutil.stdout, sys.stderr):
70 procutil.setbinary(fp)
69 procutil.setbinary(fp)
71
70
72 opts = {}
71 opts = {}
73 try:
72 try:
74 bargv = [a.encode('utf8') for a in sys.argv[1:]]
73 bargv = [a.encode('utf8') for a in sys.argv[1:]]
75 args = fancyopts.fancyopts(bargv, options, opts)
74 args = fancyopts.fancyopts(bargv, options, opts)
76 except getopt.GetoptError as e:
75 except getopt.GetoptError as e:
77 raise ParseError(e)
76 raise ParseError(e)
78 if opts[b'help']:
77 if opts[b'help']:
79 showhelp()
78 showhelp()
80 sys.exit(0)
79 sys.exit(0)
81 if len(args) != 3:
80 if len(args) != 3:
82 raise ParseError(_(b'wrong number of arguments').decode('utf8'))
81 raise ParseError(_(b'wrong number of arguments').decode('utf8'))
82 mode = b'merge'
83 if len(opts[b'label']) > 2:
83 if len(opts[b'label']) > 2:
84 opts[b'mode'] = b'merge3'
84 mode = b'merge3'
85 local, base, other = args
85 local, base, other = args
86 overrides = opts[b'label']
86 overrides = opts[b'label']
87 if len(overrides) > 3:
87 if len(overrides) > 3:
88 raise error.InputError(b'can only specify three labels.')
88 raise error.InputError(b'can only specify three labels.')
89 labels = [local, other, base]
89 labels = [local, other, base]
90 labels[: len(overrides)] = overrides
90 labels[: len(overrides)] = overrides
91 local_input = simplemerge.MergeInput(
91 local_input = simplemerge.MergeInput(
92 context.arbitraryfilectx(local), labels[0]
92 context.arbitraryfilectx(local), labels[0]
93 )
93 )
94 other_input = simplemerge.MergeInput(
94 other_input = simplemerge.MergeInput(
95 context.arbitraryfilectx(other), labels[1]
95 context.arbitraryfilectx(other), labels[1]
96 )
96 )
97 base_input = simplemerge.MergeInput(
97 base_input = simplemerge.MergeInput(
98 context.arbitraryfilectx(base), labels[2]
98 context.arbitraryfilectx(base), labels[2]
99 )
99 )
100 sys.exit(
100 sys.exit(
101 simplemerge.simplemerge(
101 simplemerge.simplemerge(
102 uimod.ui.load(),
102 uimod.ui.load(),
103 local_input,
103 local_input,
104 base_input,
104 base_input,
105 other_input,
105 other_input,
106 **pycompat.strkwargs(opts)
106 mode,
107 quiet=opts.get(b'quiet'),
108 allow_binary=opts.get(b'text'),
109 print_result=opts.get(b'print'),
107 )
110 )
108 )
111 )
109 except ParseError as e:
112 except ParseError as e:
110 e = stringutil.forcebytestr(e)
113 e = stringutil.forcebytestr(e)
111 procutil.stdout.write(b"%s: %s\n" % (sys.argv[0].encode('utf8'), e))
114 procutil.stdout.write(b"%s: %s\n" % (sys.argv[0].encode('utf8'), e))
112 showhelp()
115 showhelp()
113 sys.exit(1)
116 sys.exit(1)
114 except error.Abort as e:
117 except error.Abort as e:
115 procutil.stderr.write(b"abort: %s\n" % e)
118 procutil.stderr.write(b"abort: %s\n" % e)
116 sys.exit(255)
119 sys.exit(255)
117 except KeyboardInterrupt:
120 except KeyboardInterrupt:
118 sys.exit(255)
121 sys.exit(255)
@@ -1,538 +1,552 b''
1 # Copyright (C) 2004, 2005 Canonical Ltd
1 # Copyright (C) 2004, 2005 Canonical Ltd
2 #
2 #
3 # This program is free software; you can redistribute it and/or modify
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
6 # (at your option) any later version.
7 #
7 #
8 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
11 # GNU General Public License for more details.
12 #
12 #
13 # You should have received a copy of the GNU General Public License
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
14 # along with this program; if not, see <http://www.gnu.org/licenses/>.
15
15
16 # mbp: "you know that thing where cvs gives you conflict markers?"
16 # mbp: "you know that thing where cvs gives you conflict markers?"
17 # s: "i hate that."
17 # s: "i hate that."
18
18
19 from __future__ import absolute_import
19 from __future__ import absolute_import
20
20
21 from .i18n import _
21 from .i18n import _
22 from .thirdparty import attr
22 from .thirdparty import attr
23 from . import (
23 from . import (
24 error,
24 error,
25 mdiff,
25 mdiff,
26 pycompat,
26 pycompat,
27 )
27 )
28 from .utils import stringutil
28 from .utils import stringutil
29
29
30
30
31 def intersect(ra, rb):
31 def intersect(ra, rb):
32 """Given two ranges return the range where they intersect or None.
32 """Given two ranges return the range where they intersect or None.
33
33
34 >>> intersect((0, 10), (0, 6))
34 >>> intersect((0, 10), (0, 6))
35 (0, 6)
35 (0, 6)
36 >>> intersect((0, 10), (5, 15))
36 >>> intersect((0, 10), (5, 15))
37 (5, 10)
37 (5, 10)
38 >>> intersect((0, 10), (10, 15))
38 >>> intersect((0, 10), (10, 15))
39 >>> intersect((0, 9), (10, 15))
39 >>> intersect((0, 9), (10, 15))
40 >>> intersect((0, 9), (7, 15))
40 >>> intersect((0, 9), (7, 15))
41 (7, 9)
41 (7, 9)
42 """
42 """
43 assert ra[0] <= ra[1]
43 assert ra[0] <= ra[1]
44 assert rb[0] <= rb[1]
44 assert rb[0] <= rb[1]
45
45
46 sa = max(ra[0], rb[0])
46 sa = max(ra[0], rb[0])
47 sb = min(ra[1], rb[1])
47 sb = min(ra[1], rb[1])
48 if sa < sb:
48 if sa < sb:
49 return sa, sb
49 return sa, sb
50 else:
50 else:
51 return None
51 return None
52
52
53
53
54 def compare_range(a, astart, aend, b, bstart, bend):
54 def compare_range(a, astart, aend, b, bstart, bend):
55 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
55 """Compare a[astart:aend] == b[bstart:bend], without slicing."""
56 if (aend - astart) != (bend - bstart):
56 if (aend - astart) != (bend - bstart):
57 return False
57 return False
58 for ia, ib in zip(
58 for ia, ib in zip(
59 pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend)
59 pycompat.xrange(astart, aend), pycompat.xrange(bstart, bend)
60 ):
60 ):
61 if a[ia] != b[ib]:
61 if a[ia] != b[ib]:
62 return False
62 return False
63 else:
63 else:
64 return True
64 return True
65
65
66
66
67 class Merge3Text(object):
67 class Merge3Text(object):
68 """3-way merge of texts.
68 """3-way merge of texts.
69
69
70 Given strings BASE, OTHER, THIS, tries to produce a combined text
70 Given strings BASE, OTHER, THIS, tries to produce a combined text
71 incorporating the changes from both BASE->OTHER and BASE->THIS."""
71 incorporating the changes from both BASE->OTHER and BASE->THIS."""
72
72
73 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
73 def __init__(self, basetext, atext, btext, base=None, a=None, b=None):
74 self.basetext = basetext
74 self.basetext = basetext
75 self.atext = atext
75 self.atext = atext
76 self.btext = btext
76 self.btext = btext
77 if base is None:
77 if base is None:
78 base = mdiff.splitnewlines(basetext)
78 base = mdiff.splitnewlines(basetext)
79 if a is None:
79 if a is None:
80 a = mdiff.splitnewlines(atext)
80 a = mdiff.splitnewlines(atext)
81 if b is None:
81 if b is None:
82 b = mdiff.splitnewlines(btext)
82 b = mdiff.splitnewlines(btext)
83 self.base = base
83 self.base = base
84 self.a = a
84 self.a = a
85 self.b = b
85 self.b = b
86
86
87 def merge_groups(self):
87 def merge_groups(self):
88 """Yield sequence of line groups. Each one is a tuple:
88 """Yield sequence of line groups. Each one is a tuple:
89
89
90 'unchanged', lines
90 'unchanged', lines
91 Lines unchanged from base
91 Lines unchanged from base
92
92
93 'a', lines
93 'a', lines
94 Lines taken from a
94 Lines taken from a
95
95
96 'same', lines
96 'same', lines
97 Lines taken from a (and equal to b)
97 Lines taken from a (and equal to b)
98
98
99 'b', lines
99 'b', lines
100 Lines taken from b
100 Lines taken from b
101
101
102 'conflict', (base_lines, a_lines, b_lines)
102 'conflict', (base_lines, a_lines, b_lines)
103 Lines from base were changed to either a or b and conflict.
103 Lines from base were changed to either a or b and conflict.
104 """
104 """
105 for t in self.merge_regions():
105 for t in self.merge_regions():
106 what = t[0]
106 what = t[0]
107 if what == b'unchanged':
107 if what == b'unchanged':
108 yield what, self.base[t[1] : t[2]]
108 yield what, self.base[t[1] : t[2]]
109 elif what == b'a' or what == b'same':
109 elif what == b'a' or what == b'same':
110 yield what, self.a[t[1] : t[2]]
110 yield what, self.a[t[1] : t[2]]
111 elif what == b'b':
111 elif what == b'b':
112 yield what, self.b[t[1] : t[2]]
112 yield what, self.b[t[1] : t[2]]
113 elif what == b'conflict':
113 elif what == b'conflict':
114 yield (
114 yield (
115 what,
115 what,
116 (
116 (
117 self.base[t[1] : t[2]],
117 self.base[t[1] : t[2]],
118 self.a[t[3] : t[4]],
118 self.a[t[3] : t[4]],
119 self.b[t[5] : t[6]],
119 self.b[t[5] : t[6]],
120 ),
120 ),
121 )
121 )
122 else:
122 else:
123 raise ValueError(what)
123 raise ValueError(what)
124
124
125 def merge_regions(self):
125 def merge_regions(self):
126 """Return sequences of matching and conflicting regions.
126 """Return sequences of matching and conflicting regions.
127
127
128 This returns tuples, where the first value says what kind we
128 This returns tuples, where the first value says what kind we
129 have:
129 have:
130
130
131 'unchanged', start, end
131 'unchanged', start, end
132 Take a region of base[start:end]
132 Take a region of base[start:end]
133
133
134 'same', astart, aend
134 'same', astart, aend
135 b and a are different from base but give the same result
135 b and a are different from base but give the same result
136
136
137 'a', start, end
137 'a', start, end
138 Non-clashing insertion from a[start:end]
138 Non-clashing insertion from a[start:end]
139
139
140 'conflict', zstart, zend, astart, aend, bstart, bend
140 'conflict', zstart, zend, astart, aend, bstart, bend
141 Conflict between a and b, with z as common ancestor
141 Conflict between a and b, with z as common ancestor
142
142
143 Method is as follows:
143 Method is as follows:
144
144
145 The two sequences align only on regions which match the base
145 The two sequences align only on regions which match the base
146 and both descendants. These are found by doing a two-way diff
146 and both descendants. These are found by doing a two-way diff
147 of each one against the base, and then finding the
147 of each one against the base, and then finding the
148 intersections between those regions. These "sync regions"
148 intersections between those regions. These "sync regions"
149 are by definition unchanged in both and easily dealt with.
149 are by definition unchanged in both and easily dealt with.
150
150
151 The regions in between can be in any of three cases:
151 The regions in between can be in any of three cases:
152 conflicted, or changed on only one side.
152 conflicted, or changed on only one side.
153 """
153 """
154
154
155 # section a[0:ia] has been disposed of, etc
155 # section a[0:ia] has been disposed of, etc
156 iz = ia = ib = 0
156 iz = ia = ib = 0
157
157
158 for region in self.find_sync_regions():
158 for region in self.find_sync_regions():
159 zmatch, zend, amatch, aend, bmatch, bend = region
159 zmatch, zend, amatch, aend, bmatch, bend = region
160 # print 'match base [%d:%d]' % (zmatch, zend)
160 # print 'match base [%d:%d]' % (zmatch, zend)
161
161
162 matchlen = zend - zmatch
162 matchlen = zend - zmatch
163 assert matchlen >= 0
163 assert matchlen >= 0
164 assert matchlen == (aend - amatch)
164 assert matchlen == (aend - amatch)
165 assert matchlen == (bend - bmatch)
165 assert matchlen == (bend - bmatch)
166
166
167 len_a = amatch - ia
167 len_a = amatch - ia
168 len_b = bmatch - ib
168 len_b = bmatch - ib
169 len_base = zmatch - iz
169 len_base = zmatch - iz
170 assert len_a >= 0
170 assert len_a >= 0
171 assert len_b >= 0
171 assert len_b >= 0
172 assert len_base >= 0
172 assert len_base >= 0
173
173
174 # print 'unmatched a=%d, b=%d' % (len_a, len_b)
174 # print 'unmatched a=%d, b=%d' % (len_a, len_b)
175
175
176 if len_a or len_b:
176 if len_a or len_b:
177 # try to avoid actually slicing the lists
177 # try to avoid actually slicing the lists
178 equal_a = compare_range(
178 equal_a = compare_range(
179 self.a, ia, amatch, self.base, iz, zmatch
179 self.a, ia, amatch, self.base, iz, zmatch
180 )
180 )
181 equal_b = compare_range(
181 equal_b = compare_range(
182 self.b, ib, bmatch, self.base, iz, zmatch
182 self.b, ib, bmatch, self.base, iz, zmatch
183 )
183 )
184 same = compare_range(self.a, ia, amatch, self.b, ib, bmatch)
184 same = compare_range(self.a, ia, amatch, self.b, ib, bmatch)
185
185
186 if same:
186 if same:
187 yield b'same', ia, amatch
187 yield b'same', ia, amatch
188 elif equal_a and not equal_b:
188 elif equal_a and not equal_b:
189 yield b'b', ib, bmatch
189 yield b'b', ib, bmatch
190 elif equal_b and not equal_a:
190 elif equal_b and not equal_a:
191 yield b'a', ia, amatch
191 yield b'a', ia, amatch
192 elif not equal_a and not equal_b:
192 elif not equal_a and not equal_b:
193 yield b'conflict', iz, zmatch, ia, amatch, ib, bmatch
193 yield b'conflict', iz, zmatch, ia, amatch, ib, bmatch
194 else:
194 else:
195 raise AssertionError(b"can't handle a=b=base but unmatched")
195 raise AssertionError(b"can't handle a=b=base but unmatched")
196
196
197 ia = amatch
197 ia = amatch
198 ib = bmatch
198 ib = bmatch
199 iz = zmatch
199 iz = zmatch
200
200
201 # if the same part of the base was deleted on both sides
201 # if the same part of the base was deleted on both sides
202 # that's OK, we can just skip it.
202 # that's OK, we can just skip it.
203
203
204 if matchlen > 0:
204 if matchlen > 0:
205 assert ia == amatch
205 assert ia == amatch
206 assert ib == bmatch
206 assert ib == bmatch
207 assert iz == zmatch
207 assert iz == zmatch
208
208
209 yield b'unchanged', zmatch, zend
209 yield b'unchanged', zmatch, zend
210 iz = zend
210 iz = zend
211 ia = aend
211 ia = aend
212 ib = bend
212 ib = bend
213
213
214 def find_sync_regions(self):
214 def find_sync_regions(self):
215 """Return a list of sync regions, where both descendants match the base.
215 """Return a list of sync regions, where both descendants match the base.
216
216
217 Generates a list of (base1, base2, a1, a2, b1, b2). There is
217 Generates a list of (base1, base2, a1, a2, b1, b2). There is
218 always a zero-length sync region at the end of all the files.
218 always a zero-length sync region at the end of all the files.
219 """
219 """
220
220
221 ia = ib = 0
221 ia = ib = 0
222 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
222 amatches = mdiff.get_matching_blocks(self.basetext, self.atext)
223 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
223 bmatches = mdiff.get_matching_blocks(self.basetext, self.btext)
224 len_a = len(amatches)
224 len_a = len(amatches)
225 len_b = len(bmatches)
225 len_b = len(bmatches)
226
226
227 sl = []
227 sl = []
228
228
229 while ia < len_a and ib < len_b:
229 while ia < len_a and ib < len_b:
230 abase, amatch, alen = amatches[ia]
230 abase, amatch, alen = amatches[ia]
231 bbase, bmatch, blen = bmatches[ib]
231 bbase, bmatch, blen = bmatches[ib]
232
232
233 # there is an unconflicted block at i; how long does it
233 # there is an unconflicted block at i; how long does it
234 # extend? until whichever one ends earlier.
234 # extend? until whichever one ends earlier.
235 i = intersect((abase, abase + alen), (bbase, bbase + blen))
235 i = intersect((abase, abase + alen), (bbase, bbase + blen))
236 if i:
236 if i:
237 intbase = i[0]
237 intbase = i[0]
238 intend = i[1]
238 intend = i[1]
239 intlen = intend - intbase
239 intlen = intend - intbase
240
240
241 # found a match of base[i[0], i[1]]; this may be less than
241 # found a match of base[i[0], i[1]]; this may be less than
242 # the region that matches in either one
242 # the region that matches in either one
243 assert intlen <= alen
243 assert intlen <= alen
244 assert intlen <= blen
244 assert intlen <= blen
245 assert abase <= intbase
245 assert abase <= intbase
246 assert bbase <= intbase
246 assert bbase <= intbase
247
247
248 asub = amatch + (intbase - abase)
248 asub = amatch + (intbase - abase)
249 bsub = bmatch + (intbase - bbase)
249 bsub = bmatch + (intbase - bbase)
250 aend = asub + intlen
250 aend = asub + intlen
251 bend = bsub + intlen
251 bend = bsub + intlen
252
252
253 assert self.base[intbase:intend] == self.a[asub:aend], (
253 assert self.base[intbase:intend] == self.a[asub:aend], (
254 self.base[intbase:intend],
254 self.base[intbase:intend],
255 self.a[asub:aend],
255 self.a[asub:aend],
256 )
256 )
257
257
258 assert self.base[intbase:intend] == self.b[bsub:bend]
258 assert self.base[intbase:intend] == self.b[bsub:bend]
259
259
260 sl.append((intbase, intend, asub, aend, bsub, bend))
260 sl.append((intbase, intend, asub, aend, bsub, bend))
261
261
262 # advance whichever one ends first in the base text
262 # advance whichever one ends first in the base text
263 if (abase + alen) < (bbase + blen):
263 if (abase + alen) < (bbase + blen):
264 ia += 1
264 ia += 1
265 else:
265 else:
266 ib += 1
266 ib += 1
267
267
268 intbase = len(self.base)
268 intbase = len(self.base)
269 abase = len(self.a)
269 abase = len(self.a)
270 bbase = len(self.b)
270 bbase = len(self.b)
271 sl.append((intbase, intbase, abase, abase, bbase, bbase))
271 sl.append((intbase, intbase, abase, abase, bbase, bbase))
272
272
273 return sl
273 return sl
274
274
275
275
276 def _verifytext(text, path, ui, opts):
276 def _verifytext(text, path, ui, quiet=False, allow_binary=False):
277 """verifies that text is non-binary (unless opts[text] is passed,
277 """verifies that text is non-binary (unless opts[text] is passed,
278 then we just warn)"""
278 then we just warn)"""
279 if stringutil.binary(text):
279 if stringutil.binary(text):
280 msg = _(b"%s looks like a binary file.") % path
280 msg = _(b"%s looks like a binary file.") % path
281 if not opts.get('quiet'):
281 if not quiet:
282 ui.warn(_(b'warning: %s\n') % msg)
282 ui.warn(_(b'warning: %s\n') % msg)
283 if not opts.get('text'):
283 if not allow_binary:
284 raise error.Abort(msg)
284 raise error.Abort(msg)
285 return text
285 return text
286
286
287
287
288 def _format_labels(*inputs):
288 def _format_labels(*inputs):
289 pad = max(len(input.label) if input.label else 0 for input in inputs)
289 pad = max(len(input.label) if input.label else 0 for input in inputs)
290 labels = []
290 labels = []
291 for input in inputs:
291 for input in inputs:
292 if input.label:
292 if input.label:
293 if input.label_detail:
293 if input.label_detail:
294 label = (
294 label = (
295 (input.label + b':').ljust(pad + 1)
295 (input.label + b':').ljust(pad + 1)
296 + b' '
296 + b' '
297 + input.label_detail
297 + input.label_detail
298 )
298 )
299 else:
299 else:
300 label = input.label
300 label = input.label
301 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
301 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
302 labels.append(stringutil.ellipsis(label, 80 - 8))
302 labels.append(stringutil.ellipsis(label, 80 - 8))
303 else:
303 else:
304 labels.append(None)
304 labels.append(None)
305 return labels
305 return labels
306
306
307
307
308 def _detect_newline(m3):
308 def _detect_newline(m3):
309 if len(m3.a) > 0:
309 if len(m3.a) > 0:
310 if m3.a[0].endswith(b'\r\n'):
310 if m3.a[0].endswith(b'\r\n'):
311 return b'\r\n'
311 return b'\r\n'
312 elif m3.a[0].endswith(b'\r'):
312 elif m3.a[0].endswith(b'\r'):
313 return b'\r'
313 return b'\r'
314 return b'\n'
314 return b'\n'
315
315
316
316
317 def _minimize(a_lines, b_lines):
317 def _minimize(a_lines, b_lines):
318 """Trim conflict regions of lines where A and B sides match.
318 """Trim conflict regions of lines where A and B sides match.
319
319
320 Lines where both A and B have made the same changes at the beginning
320 Lines where both A and B have made the same changes at the beginning
321 or the end of each merge region are eliminated from the conflict
321 or the end of each merge region are eliminated from the conflict
322 region and are instead considered the same.
322 region and are instead considered the same.
323 """
323 """
324 alen = len(a_lines)
324 alen = len(a_lines)
325 blen = len(b_lines)
325 blen = len(b_lines)
326
326
327 # find matches at the front
327 # find matches at the front
328 ii = 0
328 ii = 0
329 while ii < alen and ii < blen and a_lines[ii] == b_lines[ii]:
329 while ii < alen and ii < blen and a_lines[ii] == b_lines[ii]:
330 ii += 1
330 ii += 1
331 startmatches = ii
331 startmatches = ii
332
332
333 # find matches at the end
333 # find matches at the end
334 ii = 0
334 ii = 0
335 while ii < alen and ii < blen and a_lines[-ii - 1] == b_lines[-ii - 1]:
335 while ii < alen and ii < blen and a_lines[-ii - 1] == b_lines[-ii - 1]:
336 ii += 1
336 ii += 1
337 endmatches = ii
337 endmatches = ii
338
338
339 lines_before = a_lines[:startmatches]
339 lines_before = a_lines[:startmatches]
340 new_a_lines = a_lines[startmatches : alen - endmatches]
340 new_a_lines = a_lines[startmatches : alen - endmatches]
341 new_b_lines = b_lines[startmatches : blen - endmatches]
341 new_b_lines = b_lines[startmatches : blen - endmatches]
342 lines_after = a_lines[alen - endmatches :]
342 lines_after = a_lines[alen - endmatches :]
343 return lines_before, new_a_lines, new_b_lines, lines_after
343 return lines_before, new_a_lines, new_b_lines, lines_after
344
344
345
345
346 def render_minimized(
346 def render_minimized(
347 m3,
347 m3,
348 name_a=None,
348 name_a=None,
349 name_b=None,
349 name_b=None,
350 start_marker=b'<<<<<<<',
350 start_marker=b'<<<<<<<',
351 mid_marker=b'=======',
351 mid_marker=b'=======',
352 end_marker=b'>>>>>>>',
352 end_marker=b'>>>>>>>',
353 ):
353 ):
354 """Return merge in cvs-like form."""
354 """Return merge in cvs-like form."""
355 newline = _detect_newline(m3)
355 newline = _detect_newline(m3)
356 conflicts = False
356 conflicts = False
357 if name_a:
357 if name_a:
358 start_marker = start_marker + b' ' + name_a
358 start_marker = start_marker + b' ' + name_a
359 if name_b:
359 if name_b:
360 end_marker = end_marker + b' ' + name_b
360 end_marker = end_marker + b' ' + name_b
361 merge_groups = m3.merge_groups()
361 merge_groups = m3.merge_groups()
362 lines = []
362 lines = []
363 for what, group_lines in merge_groups:
363 for what, group_lines in merge_groups:
364 if what == b'conflict':
364 if what == b'conflict':
365 conflicts = True
365 conflicts = True
366 base_lines, a_lines, b_lines = group_lines
366 base_lines, a_lines, b_lines = group_lines
367 minimized = _minimize(a_lines, b_lines)
367 minimized = _minimize(a_lines, b_lines)
368 lines_before, a_lines, b_lines, lines_after = minimized
368 lines_before, a_lines, b_lines, lines_after = minimized
369 lines.extend(lines_before)
369 lines.extend(lines_before)
370 lines.append(start_marker + newline)
370 lines.append(start_marker + newline)
371 lines.extend(a_lines)
371 lines.extend(a_lines)
372 lines.append(mid_marker + newline)
372 lines.append(mid_marker + newline)
373 lines.extend(b_lines)
373 lines.extend(b_lines)
374 lines.append(end_marker + newline)
374 lines.append(end_marker + newline)
375 lines.extend(lines_after)
375 lines.extend(lines_after)
376 else:
376 else:
377 lines.extend(group_lines)
377 lines.extend(group_lines)
378 return lines, conflicts
378 return lines, conflicts
379
379
380
380
381 def render_merge3(m3, name_a, name_b, name_base):
381 def render_merge3(m3, name_a, name_b, name_base):
382 """Render conflicts as 3-way conflict markers."""
382 """Render conflicts as 3-way conflict markers."""
383 newline = _detect_newline(m3)
383 newline = _detect_newline(m3)
384 conflicts = False
384 conflicts = False
385 lines = []
385 lines = []
386 for what, group_lines in m3.merge_groups():
386 for what, group_lines in m3.merge_groups():
387 if what == b'conflict':
387 if what == b'conflict':
388 base_lines, a_lines, b_lines = group_lines
388 base_lines, a_lines, b_lines = group_lines
389 conflicts = True
389 conflicts = True
390 lines.append(b'<<<<<<< ' + name_a + newline)
390 lines.append(b'<<<<<<< ' + name_a + newline)
391 lines.extend(a_lines)
391 lines.extend(a_lines)
392 lines.append(b'||||||| ' + name_base + newline)
392 lines.append(b'||||||| ' + name_base + newline)
393 lines.extend(base_lines)
393 lines.extend(base_lines)
394 lines.append(b'=======' + newline)
394 lines.append(b'=======' + newline)
395 lines.extend(b_lines)
395 lines.extend(b_lines)
396 lines.append(b'>>>>>>> ' + name_b + newline)
396 lines.append(b'>>>>>>> ' + name_b + newline)
397 else:
397 else:
398 lines.extend(group_lines)
398 lines.extend(group_lines)
399 return lines, conflicts
399 return lines, conflicts
400
400
401
401
402 def render_mergediff(m3, name_a, name_b, name_base):
402 def render_mergediff(m3, name_a, name_b, name_base):
403 """Render conflicts as conflict markers with one snapshot and one diff."""
403 """Render conflicts as conflict markers with one snapshot and one diff."""
404 newline = _detect_newline(m3)
404 newline = _detect_newline(m3)
405 lines = []
405 lines = []
406 conflicts = False
406 conflicts = False
407 for what, group_lines in m3.merge_groups():
407 for what, group_lines in m3.merge_groups():
408 if what == b'conflict':
408 if what == b'conflict':
409 base_lines, a_lines, b_lines = group_lines
409 base_lines, a_lines, b_lines = group_lines
410 base_text = b''.join(base_lines)
410 base_text = b''.join(base_lines)
411 b_blocks = list(
411 b_blocks = list(
412 mdiff.allblocks(
412 mdiff.allblocks(
413 base_text,
413 base_text,
414 b''.join(b_lines),
414 b''.join(b_lines),
415 lines1=base_lines,
415 lines1=base_lines,
416 lines2=b_lines,
416 lines2=b_lines,
417 )
417 )
418 )
418 )
419 a_blocks = list(
419 a_blocks = list(
420 mdiff.allblocks(
420 mdiff.allblocks(
421 base_text,
421 base_text,
422 b''.join(a_lines),
422 b''.join(a_lines),
423 lines1=base_lines,
423 lines1=base_lines,
424 lines2=b_lines,
424 lines2=b_lines,
425 )
425 )
426 )
426 )
427
427
428 def matching_lines(blocks):
428 def matching_lines(blocks):
429 return sum(
429 return sum(
430 block[1] - block[0]
430 block[1] - block[0]
431 for block, kind in blocks
431 for block, kind in blocks
432 if kind == b'='
432 if kind == b'='
433 )
433 )
434
434
435 def diff_lines(blocks, lines1, lines2):
435 def diff_lines(blocks, lines1, lines2):
436 for block, kind in blocks:
436 for block, kind in blocks:
437 if kind == b'=':
437 if kind == b'=':
438 for line in lines1[block[0] : block[1]]:
438 for line in lines1[block[0] : block[1]]:
439 yield b' ' + line
439 yield b' ' + line
440 else:
440 else:
441 for line in lines1[block[0] : block[1]]:
441 for line in lines1[block[0] : block[1]]:
442 yield b'-' + line
442 yield b'-' + line
443 for line in lines2[block[2] : block[3]]:
443 for line in lines2[block[2] : block[3]]:
444 yield b'+' + line
444 yield b'+' + line
445
445
446 lines.append(b"<<<<<<<" + newline)
446 lines.append(b"<<<<<<<" + newline)
447 if matching_lines(a_blocks) < matching_lines(b_blocks):
447 if matching_lines(a_blocks) < matching_lines(b_blocks):
448 lines.append(b"======= " + name_a + newline)
448 lines.append(b"======= " + name_a + newline)
449 lines.extend(a_lines)
449 lines.extend(a_lines)
450 lines.append(b"------- " + name_base + newline)
450 lines.append(b"------- " + name_base + newline)
451 lines.append(b"+++++++ " + name_b + newline)
451 lines.append(b"+++++++ " + name_b + newline)
452 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
452 lines.extend(diff_lines(b_blocks, base_lines, b_lines))
453 else:
453 else:
454 lines.append(b"------- " + name_base + newline)
454 lines.append(b"------- " + name_base + newline)
455 lines.append(b"+++++++ " + name_a + newline)
455 lines.append(b"+++++++ " + name_a + newline)
456 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
456 lines.extend(diff_lines(a_blocks, base_lines, a_lines))
457 lines.append(b"======= " + name_b + newline)
457 lines.append(b"======= " + name_b + newline)
458 lines.extend(b_lines)
458 lines.extend(b_lines)
459 lines.append(b">>>>>>>" + newline)
459 lines.append(b">>>>>>>" + newline)
460 conflicts = True
460 conflicts = True
461 else:
461 else:
462 lines.extend(group_lines)
462 lines.extend(group_lines)
463 return lines, conflicts
463 return lines, conflicts
464
464
465
465
466 def _resolve(m3, sides):
466 def _resolve(m3, sides):
467 lines = []
467 lines = []
468 for what, group_lines in m3.merge_groups():
468 for what, group_lines in m3.merge_groups():
469 if what == b'conflict':
469 if what == b'conflict':
470 for side in sides:
470 for side in sides:
471 lines.extend(group_lines[side])
471 lines.extend(group_lines[side])
472 else:
472 else:
473 lines.extend(group_lines)
473 lines.extend(group_lines)
474 return lines
474 return lines
475
475
476
476
477 @attr.s
477 @attr.s
478 class MergeInput(object):
478 class MergeInput(object):
479 fctx = attr.ib()
479 fctx = attr.ib()
480 label = attr.ib(default=None)
480 label = attr.ib(default=None)
481 # If the "detail" part is set, then that is rendered after the label and
481 # If the "detail" part is set, then that is rendered after the label and
482 # separated by a ':'. The label is padded to make the ':' aligned among all
482 # separated by a ':'. The label is padded to make the ':' aligned among all
483 # merge inputs.
483 # merge inputs.
484 label_detail = attr.ib(default=None)
484 label_detail = attr.ib(default=None)
485
485
486
486
487 def simplemerge(ui, local, base, other, **opts):
487 def simplemerge(
488 ui,
489 local,
490 base,
491 other,
492 mode=b'merge',
493 quiet=False,
494 allow_binary=False,
495 print_result=False,
496 ):
488 """Performs the simplemerge algorithm.
497 """Performs the simplemerge algorithm.
489
498
490 The merged result is written into `localctx`.
499 The merged result is written into `localctx`.
491 """
500 """
492
501
493 def readctx(ctx):
502 def readctx(ctx):
494 # Merges were always run in the working copy before, which means
503 # Merges were always run in the working copy before, which means
495 # they used decoded data, if the user defined any repository
504 # they used decoded data, if the user defined any repository
496 # filters.
505 # filters.
497 #
506 #
498 # Maintain that behavior today for BC, though perhaps in the future
507 # Maintain that behavior today for BC, though perhaps in the future
499 # it'd be worth considering whether merging encoded data (what the
508 # it'd be worth considering whether merging encoded data (what the
500 # repository usually sees) might be more useful.
509 # repository usually sees) might be more useful.
501 return _verifytext(ctx.decodeddata(), ctx.path(), ui, opts)
510 return _verifytext(
511 ctx.decodeddata(),
512 ctx.path(),
513 ui,
514 quiet=quiet,
515 allow_binary=allow_binary,
516 )
502
517
503 try:
518 try:
504 localtext = readctx(local.fctx)
519 localtext = readctx(local.fctx)
505 basetext = readctx(base.fctx)
520 basetext = readctx(base.fctx)
506 othertext = readctx(other.fctx)
521 othertext = readctx(other.fctx)
507 except error.Abort:
522 except error.Abort:
508 return True
523 return True
509
524
510 m3 = Merge3Text(basetext, localtext, othertext)
525 m3 = Merge3Text(basetext, localtext, othertext)
511 conflicts = False
526 conflicts = False
512 mode = opts.get('mode', b'merge')
513 if mode == b'union':
527 if mode == b'union':
514 lines = _resolve(m3, (1, 2))
528 lines = _resolve(m3, (1, 2))
515 elif mode == b'local':
529 elif mode == b'local':
516 lines = _resolve(m3, (1,))
530 lines = _resolve(m3, (1,))
517 elif mode == b'other':
531 elif mode == b'other':
518 lines = _resolve(m3, (2,))
532 lines = _resolve(m3, (2,))
519 else:
533 else:
520 if mode == b'mergediff':
534 if mode == b'mergediff':
521 labels = _format_labels(local, other, base)
535 labels = _format_labels(local, other, base)
522 lines, conflicts = render_mergediff(m3, *labels)
536 lines, conflicts = render_mergediff(m3, *labels)
523 elif mode == b'merge3':
537 elif mode == b'merge3':
524 labels = _format_labels(local, other, base)
538 labels = _format_labels(local, other, base)
525 lines, conflicts = render_merge3(m3, *labels)
539 lines, conflicts = render_merge3(m3, *labels)
526 else:
540 else:
527 labels = _format_labels(local, other)
541 labels = _format_labels(local, other)
528 lines, conflicts = render_minimized(m3, *labels)
542 lines, conflicts = render_minimized(m3, *labels)
529
543
530 mergedtext = b''.join(lines)
544 mergedtext = b''.join(lines)
531 if opts.get('print'):
545 if print_result:
532 ui.fout.write(mergedtext)
546 ui.fout.write(mergedtext)
533 else:
547 else:
534 # local.fctx.flags() already has the merged flags (done in
548 # local.fctx.flags() already has the merged flags (done in
535 # mergestate.resolve())
549 # mergestate.resolve())
536 local.fctx.write(mergedtext, local.fctx.flags())
550 local.fctx.write(mergedtext, local.fctx.flags())
537
551
538 return conflicts
552 return conflicts
General Comments 0
You need to be logged in to leave comments. Login now